runcastle 1.3.2 → 1.3.4

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 CHANGED
@@ -25,6 +25,12 @@ function newId(prefix) {
25
25
  var init_ids = () => {};
26
26
 
27
27
  // ../core/src/docs.ts
28
+ function withheldFeatureDocs(state) {
29
+ if (!state.carriedNotesOpen)
30
+ return WITHHELD_FEATURE_DOCS;
31
+ const { "test-notes.md": _testNotes, ...rest } = WITHHELD_FEATURE_DOCS;
32
+ return rest;
33
+ }
28
34
  function isAgentDigestDoc(relPath) {
29
35
  if (relPath.includes("/") || relPath.includes("\\"))
30
36
  return false;
@@ -323,7 +329,8 @@ var init_schemas = __esm(() => {
323
329
  "devCommand",
324
330
  "driveSetupCommand",
325
331
  "driveStopCommand",
326
- "dbResetCommand"
332
+ "dbResetCommand",
333
+ "driveInstructions"
327
334
  ];
328
335
  PreparedKey = z2.enum(PREPARED_KEYS);
329
336
  DRIVE_LOOP_KEYS = [
@@ -346,6 +353,7 @@ var init_schemas = __esm(() => {
346
353
  dbResetCommand: z2.string().optional(),
347
354
  driveSetupCommand: z2.string().optional(),
348
355
  driveStopCommand: z2.string().optional(),
356
+ driveInstructions: z2.string().optional(),
349
357
  closedAt: z2.number().optional()
350
358
  });
351
359
  ProjectFinding = z2.object({
@@ -367,6 +375,7 @@ var init_schemas = __esm(() => {
367
375
  brief: z2.string().optional(),
368
376
  mapped: z2.boolean(),
369
377
  lap: z2.number(),
378
+ ticketsReadyLap: z2.number().nullable(),
370
379
  phase: Phase,
371
380
  branch: z2.string(),
372
381
  baseBranch: z2.string().optional(),
@@ -551,6 +560,9 @@ function nextPhase(feature) {
551
560
  return null;
552
561
  return ORDER[i + 1];
553
562
  }
563
+ function isPastPhase(feature, phase) {
564
+ return ORDER.indexOf(feature.phase) > ORDER.indexOf(phase);
565
+ }
554
566
  function previousPhase(feature) {
555
567
  const i = ORDER.indexOf(feature.phase);
556
568
  if (i <= 0)
@@ -638,6 +650,7 @@ var init_db_schema = __esm(() => {
638
650
  verifyCommands: text("verify_commands"),
639
651
  knownFailures: text("known_failures"),
640
652
  dbResetCommand: text("db_reset_command"),
653
+ driveInstructions: text("drive_instructions"),
641
654
  driveSetupCommand: text("drive_setup_command"),
642
655
  driveStopCommand: text("drive_stop_command")
643
656
  });
@@ -660,6 +673,7 @@ var init_db_schema = __esm(() => {
660
673
  brief: text("brief"),
661
674
  mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
662
675
  lap: integer("lap").notNull().default(1),
676
+ ticketsReadyLap: integer("tickets_ready_lap"),
663
677
  phase: text("phase").notNull().$type(),
664
678
  branch: text("branch").notNull(),
665
679
  baseBranch: text("base_branch"),
@@ -1018,7 +1032,8 @@ var init_client = __esm(() => {
1018
1032
 
1019
1033
  // src/launcher/asset-paths.ts
1020
1034
  import { existsSync as existsSync2 } from "fs";
1021
- import { join as join3, resolve as resolve2 } from "path";
1035
+ import { dirname, join as join3, resolve as resolve2 } from "path";
1036
+ import { fileURLToPath } from "url";
1022
1037
  function resolveAsset(envVar, fallback) {
1023
1038
  const override = process.env[envVar];
1024
1039
  if (override) {
@@ -1039,6 +1054,17 @@ function vendoredAssetPaths(pkgRoot) {
1039
1054
  [ASSET_ENV.sandcastleTemplate]: join3(pkgRoot, "sandcastle-template")
1040
1055
  };
1041
1056
  }
1057
+ function sandcastleTemplateFallback(moduleDir) {
1058
+ const inSource = join3(moduleDir, "..", "assets", "sandcastle");
1059
+ const installed = [moduleDir, dirname(moduleDir)].map((pkgRoot) => vendoredAssetPaths(pkgRoot)[ASSET_ENV.sandcastleTemplate]);
1060
+ return [inSource, ...installed].find((dir) => existsSync2(dir)) ?? inSource;
1061
+ }
1062
+ function sandcastleTemplateDir() {
1063
+ return resolveAsset(ASSET_ENV.sandcastleTemplate, sandcastleTemplateFallback(dirname(fileURLToPath(import.meta.url))));
1064
+ }
1065
+ function burnerDockerfilePath() {
1066
+ return join3(sandcastleTemplateDir(), "Dockerfile");
1067
+ }
1042
1068
  function applyInstalledAssetEnv(pkgRoot) {
1043
1069
  for (const [envVar, path] of Object.entries(vendoredAssetPaths(pkgRoot))) {
1044
1070
  if (process.env[envVar] === undefined && existsSync2(path))
@@ -1236,7 +1262,7 @@ var init_kill_tree = () => {};
1236
1262
  import { spawn as spawn2 } from "child_process";
1237
1263
  import { createRequire } from "module";
1238
1264
  import { existsSync as existsSync4 } from "fs";
1239
- import { fileURLToPath } from "url";
1265
+ import { fileURLToPath as fileURLToPath2 } from "url";
1240
1266
  function isBun() {
1241
1267
  return typeof globalThis.Bun !== "undefined";
1242
1268
  }
@@ -1429,7 +1455,7 @@ var init_pty_sidecar = __esm(() => {
1429
1455
  init_asset_paths();
1430
1456
  init_resolve_executable();
1431
1457
  init_kill_tree();
1432
- HOST_PATH = resolveAsset(ASSET_ENV.ptyHost, fileURLToPath(new URL("./pty-host.cjs", import.meta.url)));
1458
+ HOST_PATH = resolveAsset(ASSET_ENV.ptyHost, fileURLToPath2(new URL("./pty-host.cjs", import.meta.url)));
1433
1459
  });
1434
1460
 
1435
1461
  // src/pty/pty.ts
@@ -2088,6 +2114,7 @@ function rowToFeature(row) {
2088
2114
  brief: row.brief ?? undefined,
2089
2115
  mapped: row.mapped,
2090
2116
  lap: row.lap,
2117
+ ticketsReadyLap: row.ticketsReadyLap,
2091
2118
  phase: row.phase,
2092
2119
  branch: row.branch,
2093
2120
  baseBranch: row.baseBranch ?? undefined,
@@ -2109,6 +2136,7 @@ function rowToProject(row) {
2109
2136
  dbResetCommand: row.dbResetCommand ?? undefined,
2110
2137
  driveSetupCommand: row.driveSetupCommand ?? undefined,
2111
2138
  driveStopCommand: row.driveStopCommand ?? undefined,
2139
+ driveInstructions: row.driveInstructions ?? undefined,
2112
2140
  closedAt: row.closedAt ?? undefined
2113
2141
  });
2114
2142
  }
@@ -2212,6 +2240,16 @@ function setPhase(ctx, featureId, toPhase, eventType = "phase.changed", message)
2212
2240
  });
2213
2241
  return { ...current, phase: toPhase };
2214
2242
  }
2243
+ function markTicketsReady(ctx, featureId, nextPhase2) {
2244
+ const current = getFeatureRow(ctx, featureId);
2245
+ ctx.db.update(features).set({ ticketsReadyLap: current.lap }).where(eq2(features.id, featureId)).run();
2246
+ emit(ctx, featureId, {
2247
+ type: "tickets.awaiting_burn",
2248
+ message: "tickets complete \u2014 waiting on the human Burn click (gate G3)",
2249
+ data: { phase: current.phase, nextPhase: nextPhase2, waitingOn: "human burn" }
2250
+ });
2251
+ return { ...current, ticketsReadyLap: current.lap };
2252
+ }
2215
2253
  function setFeatureStatus(ctx, featureId, status) {
2216
2254
  const current = getFeatureRow(ctx, featureId);
2217
2255
  ctx.db.update(features).set({ status }).where(eq2(features.id, featureId)).run();
@@ -2880,7 +2918,8 @@ var init_findings = __esm(() => {
2880
2918
  devCommand: projects.devCommand,
2881
2919
  driveSetupCommand: projects.driveSetupCommand,
2882
2920
  driveStopCommand: projects.driveStopCommand,
2883
- dbResetCommand: projects.dbResetCommand
2921
+ dbResetCommand: projects.dbResetCommand,
2922
+ driveInstructions: projects.driveInstructions
2884
2923
  };
2885
2924
  COLUMN_NAME = {
2886
2925
  setupCommand: "setupCommand",
@@ -2889,7 +2928,8 @@ var init_findings = __esm(() => {
2889
2928
  devCommand: "devCommand",
2890
2929
  driveSetupCommand: "driveSetupCommand",
2891
2930
  driveStopCommand: "driveStopCommand",
2892
- dbResetCommand: "dbResetCommand"
2931
+ dbResetCommand: "dbResetCommand",
2932
+ driveInstructions: "driveInstructions"
2893
2933
  };
2894
2934
  PREPARED_SET = new Set(PREPARED_KEYS);
2895
2935
  UNVERIFIED = { verifiedAt: null, verifiedSha: null };
@@ -2906,7 +2946,7 @@ import {
2906
2946
  writeFileSync
2907
2947
  } from "fs";
2908
2948
  import { tmpdir } from "os";
2909
- import { dirname, join as join6, resolve as resolve4 } from "path";
2949
+ import { dirname as dirname2, join as join6, resolve as resolve4 } from "path";
2910
2950
  import { simpleGit } from "simple-git";
2911
2951
  function deriveDriveState(info) {
2912
2952
  if (!info)
@@ -3068,7 +3108,7 @@ async function ensureTalkWorktree(project, feature) {
3068
3108
  return addWorktree(g, worktreePath, branch, "talk worktree");
3069
3109
  }
3070
3110
  async function addWorktree(g, worktreePath, branch, label) {
3071
- mkdirSync2(dirname(worktreePath), { recursive: true });
3111
+ mkdirSync2(dirname2(worktreePath), { recursive: true });
3072
3112
  try {
3073
3113
  await g.raw(["worktree", "add", worktreePath, branch]);
3074
3114
  return worktreePath;
@@ -3275,7 +3315,7 @@ async function excludeFilePath(repoPath) {
3275
3315
  }
3276
3316
  async function excludePath(repoPath, pattern) {
3277
3317
  const file = await excludeFilePath(repoPath);
3278
- mkdirSync2(dirname(file), { recursive: true });
3318
+ mkdirSync2(dirname2(file), { recursive: true });
3279
3319
  const current = existsSync6(file) ? readFileSync3(file, "utf8") : "";
3280
3320
  if (current.split(/\r?\n/).includes(pattern))
3281
3321
  return;
@@ -4181,6 +4221,460 @@ var init_git = __esm(() => {
4181
4221
  TEMP_BRANCH_PREFIXES = [RESEARCH_BRANCH_PREFIX, TICKET_BRANCH_PREFIX];
4182
4222
  });
4183
4223
 
4224
+ // src/services/tickets.ts
4225
+ import { and as and5, asc as asc3, eq as eq5, inArray } from "drizzle-orm";
4226
+ function rowToTicket(row) {
4227
+ return Ticket.parse({
4228
+ id: row.id,
4229
+ featureId: row.featureId,
4230
+ seq: row.seq,
4231
+ title: row.title,
4232
+ goal: row.goal,
4233
+ context: row.context,
4234
+ acceptanceCriteria: row.acceptanceCriteria,
4235
+ seams: row.seams,
4236
+ blockedBy: row.blockedBy,
4237
+ kind: row.kind,
4238
+ passKind: row.passKind,
4239
+ reviewedCommit: row.reviewedCommit,
4240
+ completedAt: row.completedAt,
4241
+ model: row.model ?? undefined,
4242
+ lap: row.lap,
4243
+ status: row.status,
4244
+ commits: row.commits,
4245
+ error: row.error ?? undefined,
4246
+ attemptBranch: row.attemptBranch ?? undefined,
4247
+ conflictFiles: row.conflictFiles ?? undefined,
4248
+ digest: row.digest ?? undefined,
4249
+ originFindingId: row.originFindingId ?? undefined
4250
+ });
4251
+ }
4252
+ function resolveBlocking(inputs, startSeq) {
4253
+ try {
4254
+ return resolveBatchBlocking(inputs, { startSeq });
4255
+ } catch (e) {
4256
+ if (e instanceof BlockingEdgeError)
4257
+ throw new InvalidInputError(e.message);
4258
+ throw e;
4259
+ }
4260
+ }
4261
+ function normalizeModel(ctx, value) {
4262
+ const id = value?.trim();
4263
+ if (!id)
4264
+ return null;
4265
+ const roster = modelRoster(ctx.config);
4266
+ if (!roster.some((m) => m.id === id)) {
4267
+ throw new InvalidInputError(`unknown model "${id}" \u2014 assign one of the configured models: ${roster.map((m) => m.id).join(", ")}`);
4268
+ }
4269
+ return id;
4270
+ }
4271
+ function listByFeature2(ctx, featureId) {
4272
+ return ctx.db.select().from(tickets).where(eq5(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
4273
+ }
4274
+ function storeTickets(ctx, featureId, inputs, options = {}) {
4275
+ if (inputs.length === 0)
4276
+ return [];
4277
+ const { lap } = getFeatureRow(ctx, featureId);
4278
+ const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq5(tickets.featureId, featureId)).all();
4279
+ const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
4280
+ const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
4281
+ const rows = inputs.map((t, i) => ({
4282
+ id: newId("tkt"),
4283
+ featureId,
4284
+ seq: resolved[i].seq,
4285
+ title: t.title,
4286
+ goal: t.goal,
4287
+ context: t.context,
4288
+ acceptanceCriteria: t.acceptanceCriteria,
4289
+ seams: t.seams,
4290
+ blockedBy: resolved[i].blockedBy,
4291
+ kind: t.kind ?? "implementation",
4292
+ passKind: t.passKind ?? "review",
4293
+ reviewedCommit: null,
4294
+ completedAt: null,
4295
+ model: normalizeModel(ctx, t.model),
4296
+ lap,
4297
+ status: "pending",
4298
+ commits: [],
4299
+ error: null,
4300
+ attemptBranch: null,
4301
+ conflictFiles: null,
4302
+ digest: null,
4303
+ originFindingId: t.originFindingId ?? null
4304
+ }));
4305
+ ctx.db.insert(tickets).values(rows).run();
4306
+ emit(ctx, featureId, {
4307
+ type: "tickets.stored",
4308
+ message: `${rows.length} ticket(s) stored`,
4309
+ data: { count: rows.length, seqs: rows.map((r) => r.seq) }
4310
+ });
4311
+ return rows.map(rowToTicket);
4312
+ }
4313
+ function getTicket(ctx, id) {
4314
+ const row = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
4315
+ if (!row)
4316
+ throw new NotFoundError(`ticket ${id} not found`);
4317
+ return rowToTicket(row);
4318
+ }
4319
+ function assertMutable(t, verb) {
4320
+ if (!MUTABLE_STATUSES.has(t.status)) {
4321
+ throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
4322
+ }
4323
+ }
4324
+ function editTicket(ctx, id, patch) {
4325
+ const current = getTicket(ctx, id);
4326
+ assertMutable(current, "edit");
4327
+ const set = {};
4328
+ if (patch.title !== undefined)
4329
+ set.title = patch.title;
4330
+ if (patch.goal !== undefined)
4331
+ set.goal = patch.goal;
4332
+ if (patch.context !== undefined)
4333
+ set.context = patch.context;
4334
+ if (patch.acceptanceCriteria !== undefined)
4335
+ set.acceptanceCriteria = patch.acceptanceCriteria;
4336
+ if (patch.seams !== undefined)
4337
+ set.seams = patch.seams;
4338
+ if (patch.model !== undefined)
4339
+ set.model = normalizeModel(ctx, patch.model);
4340
+ if (Object.keys(set).length === 0) {
4341
+ throw new InvalidInputError("nothing to edit \u2014 pass at least one field");
4342
+ }
4343
+ ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
4344
+ emit(ctx, current.featureId, {
4345
+ type: "ticket.edited",
4346
+ message: `ticket ${current.seq} content edited (${Object.keys(set).join(", ")})`,
4347
+ ticketId: id,
4348
+ data: { fields: Object.keys(set) }
4349
+ });
4350
+ return getTicket(ctx, id);
4351
+ }
4352
+ function cancelTicket(ctx, id, reason, opts) {
4353
+ const current = getTicket(ctx, id);
4354
+ if (!(opts?.agentStopped && current.status === "burning"))
4355
+ assertMutable(current, "cancel");
4356
+ ctx.db.update(tickets).set({
4357
+ status: "cancelled",
4358
+ error: reason?.trim() ? reason.trim() : null,
4359
+ completedAt: Date.now()
4360
+ }).where(eq5(tickets.id, id)).run();
4361
+ emit(ctx, current.featureId, {
4362
+ type: "ticket.cancelled",
4363
+ message: `ticket ${current.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
4364
+ ticketId: id,
4365
+ data: { reason: reason ?? null }
4366
+ });
4367
+ return getTicket(ctx, id);
4368
+ }
4369
+ function sweepOrphanedBurning(ctx, featureId, reason) {
4370
+ const orphaned = ctx.db.select().from(tickets).where(and5(eq5(tickets.featureId, featureId), eq5(tickets.status, "burning"))).all().map(rowToTicket);
4371
+ const swept = [];
4372
+ for (const t of orphaned) {
4373
+ swept.push(updateTicket(ctx, t.id, { status: "failed", error: reason }));
4374
+ if (t.originFindingId)
4375
+ markFailed(ctx, t.originFindingId, reason);
4376
+ emit(ctx, featureId, {
4377
+ type: "ticket.failed",
4378
+ message: `ticket ${t.seq} failed: ${reason}`,
4379
+ ticketId: t.id,
4380
+ data: { error: reason, orphaned: true }
4381
+ });
4382
+ }
4383
+ return swept;
4384
+ }
4385
+ function updateTicket(ctx, id, patch) {
4386
+ const current = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
4387
+ if (!current)
4388
+ throw new NotFoundError(`ticket ${id} not found`);
4389
+ const set = {};
4390
+ if (patch.status !== undefined)
4391
+ set.status = patch.status;
4392
+ if (patch.status !== undefined) {
4393
+ set.completedAt = ["done", "failed", "cancelled"].includes(patch.status) ? Date.now() : null;
4394
+ }
4395
+ if (patch.commits !== undefined)
4396
+ set.commits = patch.commits;
4397
+ if (patch.error !== undefined)
4398
+ set.error = patch.error;
4399
+ if (patch.attemptBranch !== undefined)
4400
+ set.attemptBranch = patch.attemptBranch;
4401
+ if (patch.conflictFiles !== undefined)
4402
+ set.conflictFiles = patch.conflictFiles;
4403
+ if (patch.digest !== undefined)
4404
+ set.digest = patch.digest;
4405
+ if (patch.reviewedCommit !== undefined)
4406
+ set.reviewedCommit = patch.reviewedCommit;
4407
+ ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
4408
+ emit(ctx, current.featureId, {
4409
+ type: "ticket.updated",
4410
+ message: `ticket ${current.seq} \u2192 ${patch.status ?? current.status}`,
4411
+ ticketId: id,
4412
+ data: patch
4413
+ });
4414
+ const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
4415
+ return rowToTicket(updated);
4416
+ }
4417
+ function listByIds(ctx, ids2) {
4418
+ if (ids2.length === 0)
4419
+ return [];
4420
+ return ctx.db.select().from(tickets).where(inArray(tickets.id, [...ids2])).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
4421
+ }
4422
+ function timingWallMs(data) {
4423
+ if (typeof data !== "object" || data === null)
4424
+ return;
4425
+ const ms = data.wallMs;
4426
+ return typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : undefined;
4427
+ }
4428
+ function ticketDurationStats(ctx, projectId) {
4429
+ const rows = ctx.db.select({ ticketId: events.ticketId, data: events.data }).from(events).innerJoin(tickets, eq5(events.ticketId, tickets.id)).where(and5(eq5(events.projectId, projectId), eq5(events.type, "ticket.timing"), eq5(tickets.status, "done"), eq5(tickets.kind, "implementation"))).orderBy(asc3(events.id)).all();
4430
+ const perTicket = new Map;
4431
+ for (const row of rows) {
4432
+ const ms = timingWallMs(row.data);
4433
+ if (row.ticketId && ms !== undefined)
4434
+ perTicket.set(row.ticketId, ms);
4435
+ }
4436
+ const samples = [...perTicket.values()].sort((a, b) => a - b);
4437
+ if (samples.length === 0)
4438
+ return { medianMs: 0, sampleSize: 0 };
4439
+ const mid = samples.length >> 1;
4440
+ const medianMs = samples.length % 2 === 1 ? samples[mid] : Math.round((samples[mid - 1] + samples[mid]) / 2);
4441
+ return { medianMs, sampleSize: samples.length };
4442
+ }
4443
+ var MUTABLE_STATUSES;
4444
+ var init_tickets = __esm(() => {
4445
+ init_src();
4446
+ init_schema();
4447
+ init_errors();
4448
+ init_events();
4449
+ init_repo();
4450
+ init_review_findings();
4451
+ MUTABLE_STATUSES = new Set(["pending", "failed"]);
4452
+ });
4453
+
4454
+ // src/services/review-findings.ts
4455
+ import { and as and6, asc as asc4, eq as eq6, isNotNull } from "drizzle-orm";
4456
+ function rowToFinding(row) {
4457
+ return ReviewFinding.parse(row);
4458
+ }
4459
+ function getFinding(ctx, id) {
4460
+ const row = ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.id, id)).get();
4461
+ if (!row)
4462
+ throw new NotFoundError(`review finding ${id} not found`);
4463
+ return rowToFinding(row);
4464
+ }
4465
+ function listByFeature3(ctx, featureId) {
4466
+ return ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.featureId, featureId)).orderBy(asc4(reviewFindings.createdAt), asc4(reviewFindings.id)).all().map(rowToFinding);
4467
+ }
4468
+ function buildFixTicket(finding) {
4469
+ return {
4470
+ title: finding.title,
4471
+ goal: `Fix: ${finding.title}`,
4472
+ context: [
4473
+ `Location: ${finding.location}`,
4474
+ `Citation: ${finding.citation}`,
4475
+ `Detail: ${finding.detail}`,
4476
+ `Repro step: ${finding.reproStep}`
4477
+ ].join(`
4478
+
4479
+ `),
4480
+ acceptanceCriteria: [
4481
+ `The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
4482
+ ],
4483
+ seams: [],
4484
+ blockedBy: [],
4485
+ kind: "implementation",
4486
+ originFindingId: finding.id
4487
+ };
4488
+ }
4489
+ function reportFinding(ctx, args) {
4490
+ const { featureId, reviewTicket, input } = args;
4491
+ const feature = getFeatureRow(ctx, featureId);
4492
+ const id = newId("finding");
4493
+ const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and6(eq6(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull(reviewFindings.fixTicketId))).all().length;
4494
+ const verification = reviewTicket.passKind === "verification";
4495
+ const overCap = input.kind === "defect" && !verification && ticketCount >= AUTO_FIX_CAP;
4496
+ ctx.db.insert(reviewFindings).values({
4497
+ id,
4498
+ featureId,
4499
+ lap: feature.lap,
4500
+ reviewTicketId: reviewTicket.id,
4501
+ ...input,
4502
+ reproStep: input.reproStep ?? "",
4503
+ status: "open",
4504
+ openReason: verification && input.kind === "defect" ? "verification" : overCap ? "over-cap" : null,
4505
+ failureReason: null,
4506
+ fixTicketId: null,
4507
+ createdAt: Date.now()
4508
+ }).run();
4509
+ let fixTicket = null;
4510
+ if (input.kind === "defect" && !overCap && !verification) {
4511
+ const finding2 = getFinding(ctx, id);
4512
+ fixTicket = storeTickets(ctx, featureId, [
4513
+ { ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
4514
+ ], { blockedByAreGlobal: true })[0];
4515
+ ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq6(reviewFindings.id, id)).run();
4516
+ }
4517
+ const finding = getFinding(ctx, id);
4518
+ emit(ctx, featureId, {
4519
+ type: "finding.reported",
4520
+ message: `${input.kind} reported: ${input.title}`,
4521
+ ticketId: fixTicket?.id,
4522
+ data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
4523
+ });
4524
+ return { finding, fixTicket, overCap };
4525
+ }
4526
+ function updateStatus(ctx, findingId, patch) {
4527
+ const current = getFinding(ctx, findingId);
4528
+ ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
4529
+ emit(ctx, current.featureId, {
4530
+ type: "finding.updated",
4531
+ message: `finding ${findingId} marked ${patch.status}`,
4532
+ data: { findingId, status: patch.status, reason: patch.failureReason }
4533
+ });
4534
+ return getFinding(ctx, findingId);
4535
+ }
4536
+ function markFixing(ctx, findingId) {
4537
+ return updateStatus(ctx, findingId, {
4538
+ status: "fixing",
4539
+ openReason: null,
4540
+ failureReason: null
4541
+ });
4542
+ }
4543
+ function markFixed(ctx, findingId) {
4544
+ return updateStatus(ctx, findingId, {
4545
+ status: "fixed",
4546
+ openReason: null,
4547
+ failureReason: null
4548
+ });
4549
+ }
4550
+ function markFailed(ctx, findingId, reason) {
4551
+ return updateStatus(ctx, findingId, {
4552
+ status: "failed",
4553
+ openReason: "fix-failed",
4554
+ failureReason: reason ?? null
4555
+ });
4556
+ }
4557
+ function markFixProgress(ctx, findingId, progress, reason) {
4558
+ if (progress === "fixing")
4559
+ return markFixing(ctx, findingId);
4560
+ if (progress === "fixed")
4561
+ return markFixed(ctx, findingId);
4562
+ return markFailed(ctx, findingId, reason);
4563
+ }
4564
+ function dismiss(ctx, findingId) {
4565
+ return updateStatus(ctx, findingId, {
4566
+ status: "dismissed",
4567
+ openReason: null,
4568
+ failureReason: null
4569
+ });
4570
+ }
4571
+ function defectState(finding, fixTicket) {
4572
+ if (finding.status === "dismissed")
4573
+ return "dismissed";
4574
+ if (finding.status === "fixed" || fixTicket?.status === "done")
4575
+ return "fixed";
4576
+ if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
4577
+ return "fixing";
4578
+ }
4579
+ return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
4580
+ }
4581
+ function viewByFeature(ctx, featureId) {
4582
+ const findings = listByFeature3(ctx, featureId);
4583
+ const tickets2 = listByFeature2(ctx, featureId);
4584
+ const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
4585
+ const openDefects = [];
4586
+ for (const finding of findings) {
4587
+ if (finding.kind === "observation") {
4588
+ summary.observations += 1;
4589
+ continue;
4590
+ }
4591
+ summary.found += 1;
4592
+ const state = defectState(finding, fixTicketOf(finding, tickets2));
4593
+ if (state === "fixed")
4594
+ summary.fixed += 1;
4595
+ else if (state === "open") {
4596
+ summary.open += 1;
4597
+ openDefects.push(finding);
4598
+ }
4599
+ }
4600
+ return { findings, openDefects, summary };
4601
+ }
4602
+ function fixTicketOf(finding, tickets2) {
4603
+ return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
4604
+ }
4605
+ function promoteOpenDefects(ctx, featureId, findingIds) {
4606
+ const open = viewByFeature(ctx, featureId).openDefects;
4607
+ const defects = findingIds ? findingIds.map((id) => {
4608
+ const finding = open.find((candidate) => candidate.id === id);
4609
+ if (!finding)
4610
+ throw new InvalidInputError(`finding ${id} is not an open defect of this feature`);
4611
+ return finding;
4612
+ }) : open;
4613
+ if (findingIds && new Set(findingIds).size !== findingIds.length)
4614
+ throw new InvalidInputError("duplicate finding selected");
4615
+ if (defects.length === 0)
4616
+ throw new InvalidInputError("no open defects to fix");
4617
+ const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
4618
+ const findings = defects.map((defect, index2) => {
4619
+ ctx.db.update(reviewFindings).set({
4620
+ status: "fixing",
4621
+ openReason: null,
4622
+ failureReason: null,
4623
+ fixTicketId: tickets2[index2].id
4624
+ }).where(eq6(reviewFindings.id, defect.id)).run();
4625
+ return getFinding(ctx, defect.id);
4626
+ });
4627
+ emit(ctx, featureId, {
4628
+ type: "finding.fixing",
4629
+ 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(", ")}`}`,
4630
+ data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
4631
+ });
4632
+ return { findings, tickets: tickets2 };
4633
+ }
4634
+ var AUTO_FIX_CAP = 8;
4635
+ var init_review_findings = __esm(() => {
4636
+ init_src();
4637
+ init_schema();
4638
+ init_errors();
4639
+ init_events();
4640
+ init_repo();
4641
+ init_tickets();
4642
+ });
4643
+
4644
+ // src/services/carried-work.ts
4645
+ import { and as and7, eq as eq7 } from "drizzle-orm";
4646
+ function carriedWork(ctx, featureId) {
4647
+ const carried = ctx.db.select({ id: testNotes.id }).from(testNotes).where(and7(eq7(testNotes.featureId, featureId), eq7(testNotes.status, "carried"))).all();
4648
+ return {
4649
+ carriedNotes: carried.length,
4650
+ openDefects: viewByFeature(ctx, featureId).openDefects.map((defect) => ({
4651
+ title: defect.title,
4652
+ location: defect.location,
4653
+ detail: defect.detail,
4654
+ reproStep: defect.reproStep
4655
+ }))
4656
+ };
4657
+ }
4658
+ function hasCarriedWork(carried) {
4659
+ return carried !== undefined && (carried.carriedNotes > 0 || carried.openDefects.length > 0);
4660
+ }
4661
+ function carriedWorkSummary(carried) {
4662
+ if (!hasCarriedWork(carried))
4663
+ return;
4664
+ const parts = [];
4665
+ if (carried.carriedNotes > 0) {
4666
+ parts.push(`${carried.carriedNotes} note${carried.carriedNotes === 1 ? "" : "s"} carried`);
4667
+ }
4668
+ const defects = carried.openDefects.length;
4669
+ if (defects > 0)
4670
+ parts.push(`${defects} defect${defects === 1 ? "" : "s"} open`);
4671
+ return `${parts.join(" and ")} from earlier laps`;
4672
+ }
4673
+ var init_carried_work = __esm(() => {
4674
+ init_schema();
4675
+ init_review_findings();
4676
+ });
4677
+
4184
4678
  // src/launcher/edit-guard.ts
4185
4679
  import { isAbsolute, relative, resolve as resolve5 } from "path";
4186
4680
  function guardsEdits(kind) {
@@ -4279,10 +4773,10 @@ var init_skills = __esm(() => {
4279
4773
 
4280
4774
  // src/launcher/artifacts.ts
4281
4775
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
4282
- import { fileURLToPath as fileURLToPath2 } from "url";
4776
+ import { fileURLToPath as fileURLToPath3 } from "url";
4283
4777
  import { join as join7 } from "path";
4284
4778
  function hookClientPath() {
4285
- return resolveAsset(ASSET_ENV.hookClient, fileURLToPath2(new URL("./hook-client.ts", import.meta.url)));
4779
+ return resolveAsset(ASSET_ENV.hookClient, fileURLToPath3(new URL("./hook-client.ts", import.meta.url)));
4286
4780
  }
4287
4781
  function serverUrlFor(config2) {
4288
4782
  return `http://localhost:${config2.serverPort}`;
@@ -4293,15 +4787,15 @@ function noCodeRule(docs2) {
4293
4787
  function conflictResolutionRule() {
4294
4788
  return "- **This session resolves a merge conflict, so it DOES write code here.** Edit the " + "conflicted files in this checkout, resolving them from the feature docs\u2019 intent, and " + "commit the merge. That is the exception and its whole extent: the edit guard exempts " + "writes only while the merge is in progress, so work the merge revealed but did not " + "cause still rides a ticket.";
4295
4789
  }
4296
- function renderSystemPrompt(feature, kind, waypoint, lap, purpose, runtime = DEFAULT_RUNTIME) {
4790
+ function renderSystemPrompt(feature, kind, waypoint, lap, purpose, runtime = DEFAULT_RUNTIME, carried) {
4297
4791
  if (lap !== undefined)
4298
- return renderRevisitPrompt(feature, lap, purpose, runtime);
4792
+ return renderRevisitPrompt(feature, lap, purpose, runtime, carried);
4299
4793
  if (kind === "waypoint")
4300
4794
  return renderWaypointPrompt(feature, waypoint, runtime);
4301
4795
  if (kind === "converge")
4302
4796
  return renderConvergePrompt(feature, runtime);
4303
4797
  if (kind === "revisit")
4304
- return renderRevisitPrompt(feature, lap, purpose, runtime);
4798
+ return renderRevisitPrompt(feature, lap, purpose, runtime, carried);
4305
4799
  if (kind === "qa")
4306
4800
  return renderQaPrompt(feature, runtime);
4307
4801
  const docs2 = featureDocsRel(feature.slug);
@@ -4449,18 +4943,30 @@ function renderConvergePrompt(feature, runtime = DEFAULT_RUNTIME) {
4449
4943
  ].join(`
4450
4944
  `);
4451
4945
  }
4452
- function renderRevisitPrompt(feature, lap, purpose, runtime = DEFAULT_RUNTIME) {
4946
+ function renderRevisitPrompt(feature, lap, purpose, runtime = DEFAULT_RUNTIME, carried) {
4453
4947
  if (lap !== undefined && purpose === "resolve-conflict") {
4454
4948
  throw new Error(`cannot brief one revisit session as both lap ${lap} and a conflict resolution \u2014 ` + "a lap advances the pipeline and writes no code; resolving a conflict writes code " + "and advances nothing. Launch them as separate sessions.");
4455
4949
  }
4456
4950
  const docs2 = featureDocsRel(feature.slug);
4951
+ const carriedSummary = carriedWorkSummary(carried);
4457
4952
  const lapIteration = lap ? [
4458
4953
  `## This is lap ${lap}`,
4459
4954
  `You are running **lap ${lap}** of this feature (ADR-0010): the human test-drove what`,
4460
- `lap ${lap - 1} burned and came back with what it taught them. Its inputs, both`,
4461
- "OPTIONAL \u2014 a missing one is normal, not an error:",
4462
- `- \`${docs2}/test-notes.md\`, section \`## Lap ${lap - 1}\` \u2014 what the drive surfaced.`,
4463
- `- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps.`,
4955
+ `lap ${lap - 1} burned and came back with what it taught them.`,
4956
+ ...carriedSummary ? [
4957
+ `**${carriedSummary} \u2014 address them.** They are this lap's opening agenda:`,
4958
+ `- \`${docs2}/test-notes.md\`, section \`## Carried, still open\` \u2014 every note carried`,
4959
+ " and not yet done, whatever lap captured it. It exists; read it.",
4960
+ "- `get_feature_context` \u2192 `openDefects` \u2014 each open defect with its title,",
4961
+ " location, detail and repro step.",
4962
+ `- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps,`,
4963
+ " OPTIONAL; a missing one is normal, not an error."
4964
+ ] : [
4965
+ "Its inputs, both OPTIONAL \u2014 a missing one is normal, not an error:",
4966
+ `- \`${docs2}/test-notes.md\`, section \`## Carried, still open\` \u2014 every note carried`,
4967
+ " and not yet done, whatever lap captured it.",
4968
+ `- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps.`
4969
+ ],
4464
4970
  `New decisions go under a \`## Lap ${lap}\` heading in \`${docs2}/decisions.md\`.`,
4465
4971
  "",
4466
4972
  "Unlike an ordinary revisit a lap MOVES the pipeline, and only this session will:",
@@ -4802,11 +5308,12 @@ function renderRunMcpConfig(runId, config2) {
4802
5308
  };
4803
5309
  }
4804
5310
  function renderSessionPrompt(input) {
4805
- const { session, feature, waypoint, prepare, projectBrief, driveFix, lap, purpose, runtime } = input;
5311
+ const { session, feature, waypoint, prepare, projectBrief, driveFix, lap, purpose, runtime, carried } = input;
4806
5312
  if (driveFix)
4807
5313
  return renderDriveFixPrompt(driveFix);
4808
- if (feature)
4809
- return renderSystemPrompt(feature, session.kind, waypoint, lap, purpose, runtime);
5314
+ if (feature) {
5315
+ return renderSystemPrompt(feature, session.kind, waypoint, lap, purpose, runtime, carried);
5316
+ }
4810
5317
  if (prepare)
4811
5318
  return renderPreparePrompt(prepare);
4812
5319
  if (projectBrief)
@@ -4829,6 +5336,7 @@ var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES
4829
5336
  var init_artifacts = __esm(() => {
4830
5337
  init_src();
4831
5338
  init_paths();
5339
+ init_carried_work();
4832
5340
  init_asset_paths();
4833
5341
  init_edit_guard();
4834
5342
  init_skills();
@@ -4878,8 +5386,8 @@ var init_artifacts = __esm(() => {
4878
5386
 
4879
5387
  // src/launcher/skills-root.ts
4880
5388
  import { existsSync as existsSync7 } from "fs";
4881
- import { dirname as dirname2, join as join8, resolve as resolve6 } from "path";
4882
- import { fileURLToPath as fileURLToPath3 } from "url";
5389
+ import { dirname as dirname3, join as join8, resolve as resolve6 } from "path";
5390
+ import { fileURLToPath as fileURLToPath4 } from "url";
4883
5391
  function resolveSkillsRoot(fromDir) {
4884
5392
  const override = process.env[SKILLS_DIR_ENV];
4885
5393
  if (override) {
@@ -4895,7 +5403,7 @@ function resolveSkillsRoot(fromDir) {
4895
5403
  searched.push(candidate);
4896
5404
  if (existsSync7(join8(candidate, SKILLS_MARKER)))
4897
5405
  return candidate;
4898
- const parent = dirname2(dir);
5406
+ const parent = dirname3(dir);
4899
5407
  if (parent === dir)
4900
5408
  break;
4901
5409
  dir = parent;
@@ -4904,7 +5412,7 @@ function resolveSkillsRoot(fromDir) {
4904
5412
  ${searched.join(`
4905
5413
  `)}`);
4906
5414
  }
4907
- function resolvePluginDir(fromDir = dirname2(fileURLToPath3(import.meta.url))) {
5415
+ function resolvePluginDir(fromDir = dirname3(fileURLToPath4(import.meta.url))) {
4908
5416
  const rel = join8("packages", "skills", "packs", "runcastle");
4909
5417
  const override = process.env[SKILLS_DIR_ENV];
4910
5418
  if (override) {
@@ -4920,7 +5428,7 @@ function resolvePluginDir(fromDir = dirname2(fileURLToPath3(import.meta.url))) {
4920
5428
  searched.push(candidate);
4921
5429
  if (existsSync7(candidate))
4922
5430
  return candidate;
4923
- const parent = dirname2(dir);
5431
+ const parent = dirname3(dir);
4924
5432
  if (parent === dir)
4925
5433
  break;
4926
5434
  dir = parent;
@@ -5032,7 +5540,6 @@ var init_codex_auth = () => {};
5032
5540
 
5033
5541
  // src/doctor/doctor.ts
5034
5542
  import { existsSync as existsSync9, statSync } from "fs";
5035
- import { fileURLToPath as fileURLToPath4 } from "url";
5036
5543
  async function versionProbe(exec, id, label, bin, fix) {
5037
5544
  const out = await exec(bin, ["--version"]);
5038
5545
  if (out.ok && out.code === 0) {
@@ -5288,7 +5795,7 @@ async function runDoctor(env) {
5288
5795
  const { exec } = env;
5289
5796
  const processEnv = env.env ?? process.env;
5290
5797
  const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
5291
- const burnerDockerfile = env.burnerDockerfile ?? fileURLToPath4(new URL("../assets/sandcastle/Dockerfile", import.meta.url));
5798
+ const burnerDockerfile = env.burnerDockerfile ?? burnerDockerfilePath();
5292
5799
  const fileMtime = env.fileMtime ?? ((path) => statSync(path).mtime);
5293
5800
  const required = new Set(env.runtimes ?? [DEFAULT_RUNTIME]);
5294
5801
  const results = [
@@ -5320,6 +5827,7 @@ function exitCodeFor(report, mode) {
5320
5827
  var RUNTIME_SPECS;
5321
5828
  var init_doctor = __esm(() => {
5322
5829
  init_src();
5830
+ init_asset_paths();
5323
5831
  init_codex_auth();
5324
5832
  RUNTIME_SPECS = {
5325
5833
  "claude-code": {
@@ -5364,7 +5872,7 @@ import {
5364
5872
  statSync as statSync2,
5365
5873
  writeFileSync as writeFileSync3
5366
5874
  } from "fs";
5367
- import { basename, dirname as dirname3, join as join10, resolve as resolve7 } from "path";
5875
+ import { basename, dirname as dirname4, join as join10, resolve as resolve7 } from "path";
5368
5876
  function codexHomeDir2(sessionId) {
5369
5877
  return join10(sessionDir(sessionId), "codex-home");
5370
5878
  }
@@ -5414,7 +5922,7 @@ function mcpServerTables(userConfigToml) {
5414
5922
  `).trim();
5415
5923
  }
5416
5924
  function readInheritedMcpServers(env = process.env) {
5417
- const path = join10(dirname3(realCodexAuthFile(env)), "config.toml");
5925
+ const path = join10(dirname4(realCodexAuthFile(env)), "config.toml");
5418
5926
  if (!existsSync10(path))
5419
5927
  return "";
5420
5928
  try {
@@ -5486,8 +5994,8 @@ function gitInfoDir(worktreePath) {
5486
5994
  if (!named)
5487
5995
  return null;
5488
5996
  const gitDir = resolve7(worktreePath, named[1].trim());
5489
- const parent = dirname3(gitDir);
5490
- return basename(parent) === "worktrees" ? join10(dirname3(parent), "info") : join10(gitDir, "info");
5997
+ const parent = dirname4(gitDir);
5998
+ return basename(parent) === "worktrees" ? join10(dirname4(parent), "info") : join10(gitDir, "info");
5491
5999
  }
5492
6000
  function buildCodexArgs(input) {
5493
6001
  return [
@@ -5593,7 +6101,7 @@ var init_runtimes = __esm(() => {
5593
6101
 
5594
6102
  // src/launcher/sessions.ts
5595
6103
  import { statSync as statSync3 } from "fs";
5596
- import { and as and5, desc as desc3, eq as eq5, inArray, isNotNull, sql as sql2 } from "drizzle-orm";
6104
+ import { and as and8, desc as desc3, eq as eq8, inArray as inArray2, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
5597
6105
  function createSessionRow(ctx, input) {
5598
6106
  const row = ctx.db.insert(sessions).values({
5599
6107
  id: newId("sess"),
@@ -5615,11 +6123,11 @@ function createSessionRow(ctx, input) {
5615
6123
  return rowToSession(row);
5616
6124
  }
5617
6125
  function getSessionRow(ctx, id) {
5618
- const row = ctx.db.select().from(sessions).where(eq5(sessions.id, id)).get();
6126
+ const row = ctx.db.select().from(sessions).where(eq8(sessions.id, id)).get();
5619
6127
  return row ? rowToSession(row) : null;
5620
6128
  }
5621
6129
  function activeSessionsForFeature(ctx, featureId) {
5622
- return ctx.db.select().from(sessions).where(and5(eq5(sessions.featureId, featureId), inArray(sessions.status, ["launching", "live"]))).all().map(rowToSession);
6130
+ return ctx.db.select().from(sessions).where(and8(eq8(sessions.featureId, featureId), inArray2(sessions.status, ["launching", "live"]))).all().map(rowToSession);
5623
6131
  }
5624
6132
  function markAgentWorking(ctx, id) {
5625
6133
  setAwaitingInput(ctx, id, false);
@@ -5628,7 +6136,7 @@ function markAwaitingInput(ctx, id) {
5628
6136
  setAwaitingInput(ctx, id, true);
5629
6137
  }
5630
6138
  function setAwaitingInput(ctx, id, awaitingInput) {
5631
- ctx.db.update(sessions).set({ awaitingInput }).where(eq5(sessions.id, id)).run();
6139
+ ctx.db.update(sessions).set({ awaitingInput }).where(eq8(sessions.id, id)).run();
5632
6140
  }
5633
6141
  function markSessionLive(ctx, id, input = {}) {
5634
6142
  const existing = getSessionRow(ctx, id);
@@ -5638,7 +6146,7 @@ function markSessionLive(ctx, id, input = {}) {
5638
6146
  status: "live",
5639
6147
  ccSessionId: input.ccSessionId ?? existing.ccSessionId ?? null,
5640
6148
  transcriptPath: input.transcriptPath ?? existing.transcriptPath ?? null
5641
- }).where(eq5(sessions.id, id)).run();
6149
+ }).where(eq8(sessions.id, id)).run();
5642
6150
  const firstTimeLive = existing.status !== "live";
5643
6151
  if (firstTimeLive) {
5644
6152
  promoteLastSession(ctx, id);
@@ -5646,8 +6154,9 @@ function markSessionLive(ctx, id, input = {}) {
5646
6154
  }
5647
6155
  return getSessionRow(ctx, id);
5648
6156
  }
5649
- function lapKickoff(lap) {
5650
- return `Proceed with your task: invoke the /runcastle:revisit skill for LAP ${lap} REVIEW ITERATION. ` + `Call get_feature_context, then read this feature's test-notes.md (the "## Lap ${lap - 1}" ` + 'section \u2014 what the last drive surfaced) and the "## Later laps" section of its spec.md. ' + "EITHER MAY NOT EXIST YET; that is normal, not an error \u2014 say so and carry on from what I " + "tell you. Interview me about what the test drive taught: what was wrong, what was missing, " + "what I want next. Write what we settle on into decisions.md and amend spec.md for this lap " + '(pruning anything you promote out of "## Later laps"), then call emit_tickets for this ' + `lap's work. Finish in THIS session: complete_phase through ideation \u2192 spec \u2192 tickets, then ` + "tell me to review the cards and click Burn.";
6157
+ function lapKickoff(lap, carried) {
6158
+ const summary = carriedWorkSummary(carried);
6159
+ return `Proceed with your task: invoke the /runcastle:revisit skill for LAP ${lap} REVIEW ITERATION. ` + (summary ? `${summary} \u2014 address them. ` : "") + `Call get_feature_context, then read this feature's test-notes.md (its "## Carried, still ` + 'open" section \u2014 every note carried and not yet done, whatever lap captured it), the open ' + `defects in that payload's openDefects, and the "## Later laps" section of its spec.md. ` + (summary ? 'THE "## Later laps" SECTION MAY NOT EXIST; that is normal, not an error. ' : "ANY OF THOSE MAY NOT EXIST YET; that is normal, not an error \u2014 say so and carry on from " + "what I tell you. ") + "Interview me about what the test drive taught: what was wrong, what was missing, " + "what I want next. Write what we settle on into decisions.md and amend spec.md for this lap " + '(pruning anything you promote out of "## Later laps"), then call emit_tickets for this ' + `lap's work. Finish in THIS session: complete_phase through ideation \u2192 spec \u2192 tickets, then ` + "tell me to review the cards and click Burn.";
5651
6160
  }
5652
6161
  function kickoffLineFor(kind, override, runtime = DEFAULT_RUNTIME) {
5653
6162
  return override ?? runtimeAdapterFor(runtime).kickoffLine(kind);
@@ -5657,7 +6166,7 @@ function lapInFlight(input) {
5657
6166
  }
5658
6167
  function planKickoff(input) {
5659
6168
  const running = input.lapInFlight === true;
5660
- const lapBriefing = input.lap > 1 ? lapKickoff(input.lap) : undefined;
6169
+ const lapBriefing = input.lap > 1 ? lapKickoff(input.lap, input.carried) : undefined;
5661
6170
  const line = input.kickoffLine ?? (running ? lapBriefing : undefined);
5662
6171
  const lap = running ? input.lap : undefined;
5663
6172
  if (!line)
@@ -5889,31 +6398,31 @@ function markSessionEnded(ctx, id) {
5889
6398
  forgetKickoff(id);
5890
6399
  if (existing.featureId)
5891
6400
  stopDocsWatch(existing.featureId);
5892
- ctx.db.update(sessions).set({ status: "ended", ...existing.endedAt === undefined ? { endedAt: Date.now() } : {} }).where(eq5(sessions.id, id)).run();
6401
+ ctx.db.update(sessions).set({ status: "ended", ...existing.endedAt === undefined ? { endedAt: Date.now() } : {} }).where(eq8(sessions.id, id)).run();
5893
6402
  return getSessionRow(ctx, id);
5894
6403
  }
5895
6404
  function mostRecentResumableSession(ctx, featureId, kind) {
5896
- const row = ctx.db.select().from(sessions).where(and5(eq5(sessions.featureId, featureId), eq5(sessions.status, "ended"), isNotNull(sessions.ccSessionId), ...kind ? [eq5(sessions.kind, kind)] : [])).orderBy(desc3(sql2`rowid`)).limit(1).get();
6405
+ const row = ctx.db.select().from(sessions).where(and8(eq8(sessions.featureId, featureId), eq8(sessions.status, "ended"), isNotNull2(sessions.ccSessionId), ...kind ? [eq8(sessions.kind, kind)] : [])).orderBy(desc3(sql2`rowid`)).limit(1).get();
5897
6406
  return row ? rowToSession(row) : null;
5898
6407
  }
5899
6408
  function activeProjectSession(ctx, projectId, kind) {
5900
- const row = ctx.db.select().from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind), inArray(sessions.status, ["launching", "live"]))).orderBy(desc3(sql2`rowid`)).limit(1).get();
6409
+ const row = ctx.db.select().from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind), inArray2(sessions.status, ["launching", "live"]))).orderBy(desc3(sql2`rowid`)).limit(1).get();
5901
6410
  return row ? rowToSession(row) : null;
5902
6411
  }
5903
6412
  function projectSessions(ctx, projectId, kind) {
5904
- return ctx.db.select().from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind))).orderBy(desc3(sql2`rowid`)).all().map(rowToSession);
6413
+ return ctx.db.select().from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind))).orderBy(desc3(sql2`rowid`)).all().map(rowToSession);
5905
6414
  }
5906
6415
  function setSessionTitle(ctx, id, title) {
5907
- ctx.db.update(sessions).set({ title }).where(eq5(sessions.id, id)).run();
6416
+ ctx.db.update(sessions).set({ title }).where(eq8(sessions.id, id)).run();
5908
6417
  }
5909
6418
  function clearSessionTitle(ctx, id) {
5910
- ctx.db.update(sessions).set({ title: null }).where(eq5(sessions.id, id)).run();
6419
+ ctx.db.update(sessions).set({ title: null }).where(eq8(sessions.id, id)).run();
5911
6420
  }
5912
6421
  function hasCompletedProjectSession(ctx, projectId, kind) {
5913
- 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;
6422
+ return ctx.db.select({ id: sessions.id }).from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind), eq8(sessions.status, "ended"))).limit(1).get() !== undefined;
5914
6423
  }
5915
6424
  function mostRecentResumableProjectSession(ctx, projectId, kind) {
5916
- const row = ctx.db.select().from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind), eq5(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc3(sql2`rowid`)).limit(1).get();
6425
+ const row = ctx.db.select().from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind), eq8(sessions.status, "ended"), isNotNull2(sessions.ccSessionId))).orderBy(desc3(sql2`rowid`)).limit(1).get();
5917
6426
  return row ? rowToSession(row) : null;
5918
6427
  }
5919
6428
  function envInt(name, fallback) {
@@ -5953,17 +6462,17 @@ function transcriptBytes(session) {
5953
6462
  }
5954
6463
  function reentryCount(ctx, scope) {
5955
6464
  const where = [
5956
- eq5(sessions.status, "ended"),
5957
- isNotNull(sessions.ccSessionId),
5958
- ...scope.featureId ? [eq5(sessions.featureId, scope.featureId)] : [],
5959
- ...scope.projectId ? [eq5(sessions.projectId, scope.projectId)] : [],
5960
- ...scope.kind ? [eq5(sessions.kind, scope.kind)] : []
6465
+ eq8(sessions.status, "ended"),
6466
+ isNotNull2(sessions.ccSessionId),
6467
+ ...scope.featureId ? [eq8(sessions.featureId, scope.featureId)] : [],
6468
+ ...scope.projectId ? [eq8(sessions.projectId, scope.projectId)] : [],
6469
+ ...scope.kind ? [eq8(sessions.kind, scope.kind)] : []
5961
6470
  ];
5962
- const row = ctx.db.select({ n: sql2`count(*)` }).from(sessions).where(and5(...where)).get();
6471
+ const row = ctx.db.select({ n: sql2`count(*)` }).from(sessions).where(and8(...where)).get();
5963
6472
  return row?.n ?? 0;
5964
6473
  }
5965
6474
  function mostRecentLiveSession(ctx) {
5966
- const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
6475
+ const row = ctx.db.select().from(sessions).where(eq8(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
5967
6476
  return row ? rowToSession(row) : null;
5968
6477
  }
5969
6478
  var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, PREPARE_CONFIRM_KICKOFF, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings, LANDING_KEPT, RESUME_MAX_TRANSCRIPT_BYTES, RESUME_MAX_REENTRIES;
@@ -5974,6 +6483,7 @@ var init_sessions = __esm(() => {
5974
6483
  init_docs_watch();
5975
6484
  init_events();
5976
6485
  init_git();
6486
+ init_carried_work();
5977
6487
  init_waypoints();
5978
6488
  init_repo();
5979
6489
  init_runtimes();
@@ -5990,9 +6500,9 @@ var init_sessions = __esm(() => {
5990
6500
  });
5991
6501
 
5992
6502
  // src/launcher/reconcile.ts
5993
- import { inArray as inArray2 } from "drizzle-orm";
6503
+ import { inArray as inArray3 } from "drizzle-orm";
5994
6504
  function reconcileStaleSessions(ctx) {
5995
- const stale = ctx.db.select().from(sessions).where(inArray2(sessions.status, ["launching", "live"])).all().map(rowToSession);
6505
+ const stale = ctx.db.select().from(sessions).where(inArray3(sessions.status, ["launching", "live"])).all().map(rowToSession);
5996
6506
  const reconciled = [];
5997
6507
  for (const session of stale) {
5998
6508
  const entry = ptyRegistry().get(session.id);
@@ -6040,428 +6550,9 @@ async function getRuntimeCtx() {
6040
6550
  }
6041
6551
  var current = null;
6042
6552
 
6043
- // src/services/review-findings.ts
6044
- import { and as and6, asc as asc3, eq as eq6, isNotNull as isNotNull2 } from "drizzle-orm";
6045
- function rowToFinding(row) {
6046
- return ReviewFinding.parse(row);
6047
- }
6048
- function getFinding(ctx, id) {
6049
- const row = ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.id, id)).get();
6050
- if (!row)
6051
- throw new NotFoundError(`review finding ${id} not found`);
6052
- return rowToFinding(row);
6053
- }
6054
- function listByFeature3(ctx, featureId) {
6055
- return ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.featureId, featureId)).orderBy(asc3(reviewFindings.createdAt), asc3(reviewFindings.id)).all().map(rowToFinding);
6056
- }
6057
- function buildFixTicket(finding) {
6058
- return {
6059
- title: finding.title,
6060
- goal: `Fix: ${finding.title}`,
6061
- context: [
6062
- `Location: ${finding.location}`,
6063
- `Citation: ${finding.citation}`,
6064
- `Detail: ${finding.detail}`,
6065
- `Repro step: ${finding.reproStep}`
6066
- ].join(`
6067
-
6068
- `),
6069
- acceptanceCriteria: [
6070
- `The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
6071
- ],
6072
- seams: [],
6073
- blockedBy: [],
6074
- kind: "implementation",
6075
- originFindingId: finding.id
6076
- };
6077
- }
6078
- function reportFinding(ctx, args) {
6079
- const { featureId, reviewTicket, input } = args;
6080
- const feature = getFeatureRow(ctx, featureId);
6081
- const id = newId("finding");
6082
- const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and6(eq6(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull2(reviewFindings.fixTicketId))).all().length;
6083
- const verification = reviewTicket.passKind === "verification";
6084
- const overCap = input.kind === "defect" && !verification && ticketCount >= AUTO_FIX_CAP;
6085
- ctx.db.insert(reviewFindings).values({
6086
- id,
6087
- featureId,
6088
- lap: feature.lap,
6089
- reviewTicketId: reviewTicket.id,
6090
- ...input,
6091
- reproStep: input.reproStep ?? "",
6092
- status: "open",
6093
- openReason: verification && input.kind === "defect" ? "verification" : overCap ? "over-cap" : null,
6094
- failureReason: null,
6095
- fixTicketId: null,
6096
- createdAt: Date.now()
6097
- }).run();
6098
- let fixTicket = null;
6099
- if (input.kind === "defect" && !overCap && !verification) {
6100
- const finding2 = getFinding(ctx, id);
6101
- fixTicket = storeTickets(ctx, featureId, [
6102
- { ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
6103
- ], { blockedByAreGlobal: true })[0];
6104
- ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq6(reviewFindings.id, id)).run();
6105
- }
6106
- const finding = getFinding(ctx, id);
6107
- emit(ctx, featureId, {
6108
- type: "finding.reported",
6109
- message: `${input.kind} reported: ${input.title}`,
6110
- ticketId: fixTicket?.id,
6111
- data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
6112
- });
6113
- return { finding, fixTicket, overCap };
6114
- }
6115
- function updateStatus(ctx, findingId, patch) {
6116
- const current2 = getFinding(ctx, findingId);
6117
- ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
6118
- emit(ctx, current2.featureId, {
6119
- type: "finding.updated",
6120
- message: `finding ${findingId} marked ${patch.status}`,
6121
- data: { findingId, status: patch.status, reason: patch.failureReason }
6122
- });
6123
- return getFinding(ctx, findingId);
6124
- }
6125
- function markFixing(ctx, findingId) {
6126
- return updateStatus(ctx, findingId, {
6127
- status: "fixing",
6128
- openReason: null,
6129
- failureReason: null
6130
- });
6131
- }
6132
- function markFixed(ctx, findingId) {
6133
- return updateStatus(ctx, findingId, {
6134
- status: "fixed",
6135
- openReason: null,
6136
- failureReason: null
6137
- });
6138
- }
6139
- function markFailed(ctx, findingId, reason) {
6140
- return updateStatus(ctx, findingId, {
6141
- status: "failed",
6142
- openReason: "fix-failed",
6143
- failureReason: reason ?? null
6144
- });
6145
- }
6146
- function markFixProgress(ctx, findingId, progress, reason) {
6147
- if (progress === "fixing")
6148
- return markFixing(ctx, findingId);
6149
- if (progress === "fixed")
6150
- return markFixed(ctx, findingId);
6151
- return markFailed(ctx, findingId, reason);
6152
- }
6153
- function dismiss(ctx, findingId) {
6154
- return updateStatus(ctx, findingId, {
6155
- status: "dismissed",
6156
- openReason: null,
6157
- failureReason: null
6158
- });
6159
- }
6160
- function defectState(finding, fixTicket) {
6161
- if (finding.status === "dismissed")
6162
- return "dismissed";
6163
- if (finding.status === "fixed" || fixTicket?.status === "done")
6164
- return "fixed";
6165
- if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
6166
- return "fixing";
6167
- }
6168
- return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
6169
- }
6170
- function viewByFeature(ctx, featureId) {
6171
- const findings = listByFeature3(ctx, featureId);
6172
- const tickets2 = listByFeature2(ctx, featureId);
6173
- const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
6174
- const openDefects = [];
6175
- for (const finding of findings) {
6176
- if (finding.kind === "observation") {
6177
- summary.observations += 1;
6178
- continue;
6179
- }
6180
- summary.found += 1;
6181
- const state = defectState(finding, fixTicketOf(finding, tickets2));
6182
- if (state === "fixed")
6183
- summary.fixed += 1;
6184
- else if (state === "open") {
6185
- summary.open += 1;
6186
- openDefects.push(finding);
6187
- }
6188
- }
6189
- return { findings, openDefects, summary };
6190
- }
6191
- function fixTicketOf(finding, tickets2) {
6192
- return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
6193
- }
6194
- function promoteOpenDefects(ctx, featureId, findingIds) {
6195
- const open = viewByFeature(ctx, featureId).openDefects;
6196
- const defects = findingIds ? findingIds.map((id) => {
6197
- const finding = open.find((candidate) => candidate.id === id);
6198
- if (!finding)
6199
- throw new InvalidInputError(`finding ${id} is not an open defect of this feature`);
6200
- return finding;
6201
- }) : open;
6202
- if (findingIds && new Set(findingIds).size !== findingIds.length)
6203
- throw new InvalidInputError("duplicate finding selected");
6204
- if (defects.length === 0)
6205
- throw new InvalidInputError("no open defects to fix");
6206
- const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
6207
- const findings = defects.map((defect, index2) => {
6208
- ctx.db.update(reviewFindings).set({
6209
- status: "fixing",
6210
- openReason: null,
6211
- failureReason: null,
6212
- fixTicketId: tickets2[index2].id
6213
- }).where(eq6(reviewFindings.id, defect.id)).run();
6214
- return getFinding(ctx, defect.id);
6215
- });
6216
- emit(ctx, featureId, {
6217
- type: "finding.fixing",
6218
- 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(", ")}`}`,
6219
- data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
6220
- });
6221
- return { findings, tickets: tickets2 };
6222
- }
6223
- var AUTO_FIX_CAP = 8;
6224
- var init_review_findings = __esm(() => {
6225
- init_src();
6226
- init_schema();
6227
- init_errors();
6228
- init_events();
6229
- init_repo();
6230
- init_tickets();
6231
- });
6232
-
6233
- // src/services/tickets.ts
6234
- import { and as and7, asc as asc4, eq as eq7, inArray as inArray3 } from "drizzle-orm";
6235
- function rowToTicket(row) {
6236
- return Ticket.parse({
6237
- id: row.id,
6238
- featureId: row.featureId,
6239
- seq: row.seq,
6240
- title: row.title,
6241
- goal: row.goal,
6242
- context: row.context,
6243
- acceptanceCriteria: row.acceptanceCriteria,
6244
- seams: row.seams,
6245
- blockedBy: row.blockedBy,
6246
- kind: row.kind,
6247
- passKind: row.passKind,
6248
- reviewedCommit: row.reviewedCommit,
6249
- completedAt: row.completedAt,
6250
- model: row.model ?? undefined,
6251
- lap: row.lap,
6252
- status: row.status,
6253
- commits: row.commits,
6254
- error: row.error ?? undefined,
6255
- attemptBranch: row.attemptBranch ?? undefined,
6256
- conflictFiles: row.conflictFiles ?? undefined,
6257
- digest: row.digest ?? undefined,
6258
- originFindingId: row.originFindingId ?? undefined
6259
- });
6260
- }
6261
- function resolveBlocking(inputs, startSeq) {
6262
- try {
6263
- return resolveBatchBlocking(inputs, { startSeq });
6264
- } catch (e) {
6265
- if (e instanceof BlockingEdgeError)
6266
- throw new InvalidInputError(e.message);
6267
- throw e;
6268
- }
6269
- }
6270
- function normalizeModel(ctx, value) {
6271
- const id = value?.trim();
6272
- if (!id)
6273
- return null;
6274
- const roster = modelRoster(ctx.config);
6275
- if (!roster.some((m) => m.id === id)) {
6276
- throw new InvalidInputError(`unknown model "${id}" \u2014 assign one of the configured models: ${roster.map((m) => m.id).join(", ")}`);
6277
- }
6278
- return id;
6279
- }
6280
- function listByFeature2(ctx, featureId) {
6281
- return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc4(tickets.seq)).all().map(rowToTicket);
6282
- }
6283
- function storeTickets(ctx, featureId, inputs, options = {}) {
6284
- if (inputs.length === 0)
6285
- return [];
6286
- const { lap } = getFeatureRow(ctx, featureId);
6287
- const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
6288
- const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
6289
- const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
6290
- const rows = inputs.map((t, i) => ({
6291
- id: newId("tkt"),
6292
- featureId,
6293
- seq: resolved[i].seq,
6294
- title: t.title,
6295
- goal: t.goal,
6296
- context: t.context,
6297
- acceptanceCriteria: t.acceptanceCriteria,
6298
- seams: t.seams,
6299
- blockedBy: resolved[i].blockedBy,
6300
- kind: t.kind ?? "implementation",
6301
- passKind: t.passKind ?? "review",
6302
- reviewedCommit: null,
6303
- completedAt: null,
6304
- model: normalizeModel(ctx, t.model),
6305
- lap,
6306
- status: "pending",
6307
- commits: [],
6308
- error: null,
6309
- attemptBranch: null,
6310
- conflictFiles: null,
6311
- digest: null,
6312
- originFindingId: t.originFindingId ?? null
6313
- }));
6314
- ctx.db.insert(tickets).values(rows).run();
6315
- emit(ctx, featureId, {
6316
- type: "tickets.stored",
6317
- message: `${rows.length} ticket(s) stored`,
6318
- data: { count: rows.length, seqs: rows.map((r) => r.seq) }
6319
- });
6320
- return rows.map(rowToTicket);
6321
- }
6322
- function getTicket(ctx, id) {
6323
- const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
6324
- if (!row)
6325
- throw new NotFoundError(`ticket ${id} not found`);
6326
- return rowToTicket(row);
6327
- }
6328
- function assertMutable(t, verb) {
6329
- if (!MUTABLE_STATUSES.has(t.status)) {
6330
- throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
6331
- }
6332
- }
6333
- function editTicket(ctx, id, patch) {
6334
- const current2 = getTicket(ctx, id);
6335
- assertMutable(current2, "edit");
6336
- const set = {};
6337
- if (patch.title !== undefined)
6338
- set.title = patch.title;
6339
- if (patch.goal !== undefined)
6340
- set.goal = patch.goal;
6341
- if (patch.context !== undefined)
6342
- set.context = patch.context;
6343
- if (patch.acceptanceCriteria !== undefined)
6344
- set.acceptanceCriteria = patch.acceptanceCriteria;
6345
- if (patch.seams !== undefined)
6346
- set.seams = patch.seams;
6347
- if (patch.model !== undefined)
6348
- set.model = normalizeModel(ctx, patch.model);
6349
- if (Object.keys(set).length === 0) {
6350
- throw new InvalidInputError("nothing to edit \u2014 pass at least one field");
6351
- }
6352
- ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
6353
- emit(ctx, current2.featureId, {
6354
- type: "ticket.edited",
6355
- message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
6356
- ticketId: id,
6357
- data: { fields: Object.keys(set) }
6358
- });
6359
- return getTicket(ctx, id);
6360
- }
6361
- function cancelTicket(ctx, id, reason) {
6362
- const current2 = getTicket(ctx, id);
6363
- assertMutable(current2, "cancel");
6364
- ctx.db.update(tickets).set({
6365
- status: "cancelled",
6366
- error: reason?.trim() ? reason.trim() : null,
6367
- completedAt: Date.now()
6368
- }).where(eq7(tickets.id, id)).run();
6369
- emit(ctx, current2.featureId, {
6370
- type: "ticket.cancelled",
6371
- message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
6372
- ticketId: id,
6373
- data: { reason: reason ?? null }
6374
- });
6375
- return getTicket(ctx, id);
6376
- }
6377
- function sweepOrphanedBurning(ctx, featureId, reason) {
6378
- const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
6379
- const swept = [];
6380
- for (const t of orphaned) {
6381
- swept.push(updateTicket(ctx, t.id, { status: "failed", error: reason }));
6382
- if (t.originFindingId)
6383
- markFailed(ctx, t.originFindingId, reason);
6384
- emit(ctx, featureId, {
6385
- type: "ticket.failed",
6386
- message: `ticket ${t.seq} failed: ${reason}`,
6387
- ticketId: t.id,
6388
- data: { error: reason, orphaned: true }
6389
- });
6390
- }
6391
- return swept;
6392
- }
6393
- function updateTicket(ctx, id, patch) {
6394
- const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
6395
- if (!current2)
6396
- throw new NotFoundError(`ticket ${id} not found`);
6397
- const set = {};
6398
- if (patch.status !== undefined)
6399
- set.status = patch.status;
6400
- if (patch.status !== undefined) {
6401
- set.completedAt = ["done", "failed", "cancelled"].includes(patch.status) ? Date.now() : null;
6402
- }
6403
- if (patch.commits !== undefined)
6404
- set.commits = patch.commits;
6405
- if (patch.error !== undefined)
6406
- set.error = patch.error;
6407
- if (patch.attemptBranch !== undefined)
6408
- set.attemptBranch = patch.attemptBranch;
6409
- if (patch.conflictFiles !== undefined)
6410
- set.conflictFiles = patch.conflictFiles;
6411
- if (patch.digest !== undefined)
6412
- set.digest = patch.digest;
6413
- if (patch.reviewedCommit !== undefined)
6414
- set.reviewedCommit = patch.reviewedCommit;
6415
- ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
6416
- emit(ctx, current2.featureId, {
6417
- type: "ticket.updated",
6418
- message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
6419
- ticketId: id,
6420
- data: patch
6421
- });
6422
- const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
6423
- return rowToTicket(updated);
6424
- }
6425
- function listByIds(ctx, ids2) {
6426
- if (ids2.length === 0)
6427
- return [];
6428
- return ctx.db.select().from(tickets).where(inArray3(tickets.id, [...ids2])).orderBy(asc4(tickets.seq)).all().map(rowToTicket);
6429
- }
6430
- function timingWallMs(data) {
6431
- if (typeof data !== "object" || data === null)
6432
- return;
6433
- const ms = data.wallMs;
6434
- return typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : undefined;
6435
- }
6436
- function ticketDurationStats(ctx, projectId) {
6437
- 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();
6438
- const perTicket = new Map;
6439
- for (const row of rows) {
6440
- const ms = timingWallMs(row.data);
6441
- if (row.ticketId && ms !== undefined)
6442
- perTicket.set(row.ticketId, ms);
6443
- }
6444
- const samples = [...perTicket.values()].sort((a, b) => a - b);
6445
- if (samples.length === 0)
6446
- return { medianMs: 0, sampleSize: 0 };
6447
- const mid = samples.length >> 1;
6448
- const medianMs = samples.length % 2 === 1 ? samples[mid] : Math.round((samples[mid - 1] + samples[mid]) / 2);
6449
- return { medianMs, sampleSize: samples.length };
6450
- }
6451
- var MUTABLE_STATUSES;
6452
- var init_tickets = __esm(() => {
6453
- init_src();
6454
- init_schema();
6455
- init_errors();
6456
- init_events();
6457
- init_repo();
6458
- init_review_findings();
6459
- MUTABLE_STATUSES = new Set(["pending", "failed"]);
6460
- });
6461
-
6462
6553
  // src/services/gates.ts
6463
6554
  import { existsSync as existsSync11 } from "fs";
6464
- import { and as and8, desc as desc4, eq as eq8, inArray as inArray4 } from "drizzle-orm";
6555
+ import { and as and9, desc as desc4, eq as eq9, inArray as inArray4 } from "drizzle-orm";
6465
6556
  function checkGate(ctx, check, feature) {
6466
6557
  switch (check) {
6467
6558
  case "decisions-file-exists":
@@ -6487,8 +6578,16 @@ function checkGate(ctx, check, feature) {
6487
6578
  case "spec-file-exists":
6488
6579
  return docGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
6489
6580
  case "tickets-approved": {
6490
- const count = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled").length;
6491
- return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
6581
+ const burnable = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled");
6582
+ if (burnable.length === 0)
6583
+ return { satisfied: false, reason: "no tickets to burn" };
6584
+ if (!burnable.some((t) => t.kind === "review")) {
6585
+ return {
6586
+ satisfied: false,
6587
+ reason: 'no review ticket on this lap \u2014 every batch closes with one (kind: "review"); emit it, ' + "fix the kind on a review-shaped ticket, or override this gate with a reason"
6588
+ };
6589
+ }
6590
+ return { satisfied: true };
6492
6591
  }
6493
6592
  case "all-tickets-terminal": {
6494
6593
  const tickets2 = listByFeature2(ctx, feature.id);
@@ -6520,7 +6619,7 @@ function fileGate(ctx, feature, fileName, reason) {
6520
6619
  return existsSync11(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
6521
6620
  }
6522
6621
  function lapWorked(ctx, feature) {
6523
- 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;
6622
+ return ctx.db.select({ id: sessions.id }).from(sessions).where(and9(eq9(sessions.featureId, feature.id), eq9(sessions.lap, feature.lap), inArray4(sessions.kind, [...DOC_WRITING_KINDS]))).all().length > 0;
6524
6623
  }
6525
6624
  function docGate(ctx, feature, fileName, reason) {
6526
6625
  const file = fileGate(ctx, feature, fileName, reason);
@@ -6552,10 +6651,10 @@ function overrideGate(ctx, featureId, gate, reason) {
6552
6651
  return feature;
6553
6652
  }
6554
6653
  function undoLastGateOverride(ctx, featureId, gate) {
6555
- 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();
6654
+ const last = ctx.db.select({ id: gateOverrides.id }).from(gateOverrides).where(and9(eq9(gateOverrides.featureId, featureId), eq9(gateOverrides.gate, gate))).orderBy(desc4(gateOverrides.id)).get();
6556
6655
  if (!last)
6557
6656
  return false;
6558
- ctx.db.delete(gateOverrides).where(eq8(gateOverrides.id, last.id)).run();
6657
+ ctx.db.delete(gateOverrides).where(eq9(gateOverrides.id, last.id)).run();
6559
6658
  return true;
6560
6659
  }
6561
6660
  function undoGateOverride(ctx, featureId, gate) {
@@ -6749,6 +6848,133 @@ var init_end_session = __esm(() => {
6749
6848
  init_registry();
6750
6849
  });
6751
6850
 
6851
+ // src/workflows/kill-registry.ts
6852
+ import { spawn as spawn4 } from "child_process";
6853
+ function log2(msg) {
6854
+ console.error(`[kill-registry] ${msg}`);
6855
+ }
6856
+ function runDockerCommand(args) {
6857
+ return new Promise((resolve9) => {
6858
+ let settled = false;
6859
+ const settle = (ok) => {
6860
+ if (settled)
6861
+ return;
6862
+ settled = true;
6863
+ clearTimeout(timer);
6864
+ resolve9(ok);
6865
+ };
6866
+ const timer = setTimeout(() => settle(false), DOCKER_COMMAND_TIMEOUT_MS);
6867
+ timer.unref?.();
6868
+ try {
6869
+ const child = spawn4("docker", args, { windowsHide: true, stdio: "ignore" });
6870
+ child.on("exit", (code) => settle(code === 0));
6871
+ child.on("close", (code) => settle(code === 0));
6872
+ child.on("error", () => settle(false));
6873
+ } catch {
6874
+ settle(false);
6875
+ }
6876
+ });
6877
+ }
6878
+ function withDeadline2(body, ms) {
6879
+ return new Promise((resolve9) => {
6880
+ const timer = setTimeout(() => resolve9(false), ms);
6881
+ body.catch(() => false).then((confirmed) => {
6882
+ clearTimeout(timer);
6883
+ resolve9(confirmed);
6884
+ });
6885
+ });
6886
+ }
6887
+ function forget() {}
6888
+ function delay(ms) {
6889
+ return new Promise((resolve9) => {
6890
+ const timer = setTimeout(resolve9, ms);
6891
+ timer.unref?.();
6892
+ });
6893
+ }
6894
+
6895
+ class KillRegistry {
6896
+ deps;
6897
+ handles = new Map;
6898
+ killsInFlight = new Map;
6899
+ constructor(deps = REAL_DEPS) {
6900
+ this.deps = deps;
6901
+ }
6902
+ registerContainer(laneKey, containerName, owner = {}) {
6903
+ this.handles.set(laneKey, { kind: "container", containerName, ...owner });
6904
+ }
6905
+ registerHostPid(laneKey, pid, owner = {}) {
6906
+ this.handles.set(laneKey, { kind: "host", pid, ...owner });
6907
+ }
6908
+ release(laneKey) {
6909
+ this.handles.delete(laneKey);
6910
+ }
6911
+ killAndWait(laneKey, opts) {
6912
+ const handle = this.handles.get(laneKey);
6913
+ if (!handle)
6914
+ return Promise.resolve({ confirmed: true });
6915
+ const kill = this.killLane(laneKey, handle, opts);
6916
+ const entry = { runId: handle.runId, settled: kill.then(forget, forget) };
6917
+ this.killsInFlight.set(laneKey, entry);
6918
+ entry.settled.then(() => {
6919
+ if (this.killsInFlight.get(laneKey) === entry)
6920
+ this.killsInFlight.delete(laneKey);
6921
+ });
6922
+ return kill;
6923
+ }
6924
+ async whenKillSettled(laneKey) {
6925
+ await this.killsInFlight.get(laneKey)?.settled;
6926
+ }
6927
+ async whenRunKillsSettled(runId) {
6928
+ await Promise.all([...this.killsInFlight.values()].filter((kill) => kill.runId === runId).map((kill) => kill.settled));
6929
+ }
6930
+ async killLane(laneKey, handle, opts) {
6931
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6932
+ const started = Date.now();
6933
+ const confirmed = await withDeadline2(this.kill(handle, started + timeoutMs), timeoutMs);
6934
+ if (confirmed)
6935
+ this.handles.delete(laneKey);
6936
+ const target = handle.kind === "container" ? handle.containerName : `pid=${handle.pid}`;
6937
+ log2(`killAndWait: lane=${laneKey} ${handle.kind}=${target} ${confirmed ? "confirmed dead" : `NOT confirmed dead (${timeoutMs}ms deadline)`} after ${Date.now() - started}ms`);
6938
+ return { confirmed };
6939
+ }
6940
+ async killAllForRun(runId, opts) {
6941
+ const lanes = [...this.handles].filter(([, handle]) => handle.runId === runId).map(([laneKey]) => laneKey);
6942
+ const outcomes = await Promise.all(lanes.map((laneKey) => this.killAndWait(laneKey, opts)));
6943
+ return { confirmed: outcomes.every((outcome) => outcome.confirmed) };
6944
+ }
6945
+ keys() {
6946
+ return [...this.handles.keys()];
6947
+ }
6948
+ async kill(handle, deadline) {
6949
+ if (handle.kind === "host") {
6950
+ await this.deps.killTree(handle.pid);
6951
+ return true;
6952
+ }
6953
+ await this.deps.runDocker(["rm", "-f", handle.containerName]);
6954
+ do {
6955
+ if (!await this.deps.runDocker(["inspect", handle.containerName]))
6956
+ return true;
6957
+ await delay(DEATH_POLL_INTERVAL_MS);
6958
+ } while (Date.now() < deadline);
6959
+ return false;
6960
+ }
6961
+ }
6962
+ function killRegistry() {
6963
+ const g = globalThis;
6964
+ if (!g[REGISTRY_KEY2])
6965
+ g[REGISTRY_KEY2] = new KillRegistry;
6966
+ return g[REGISTRY_KEY2];
6967
+ }
6968
+ function registerHostChildren(laneKey, owner) {
6969
+ return (pid) => killRegistry().registerHostPid(laneKey, pid, owner);
6970
+ }
6971
+ var DEFAULT_TIMEOUT_MS = 1e4, DEATH_POLL_INTERVAL_MS = 100, DOCKER_COMMAND_TIMEOUT_MS = 5000, REAL_DEPS, REGISTRY_KEY2;
6972
+ var init_kill_registry = __esm(() => {
6973
+ init_kill_tree();
6974
+ REAL_DEPS = { runDocker: runDockerCommand, killTree: killProcessTree };
6975
+ REGISTRY_KEY2 = Symbol.for("runcastle.kill.registry");
6976
+ });
6977
+
6752
6978
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-NGBM7T3E.js
6753
6979
  import { createRequire as createRequire3 } from "module";
6754
6980
  var require$1, __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getProtoOf, __hasOwnProp, __require2, __commonJS = (cb, mod) => function __require22() {
@@ -6806,7 +7032,7 @@ var init_chunk_NGBM7T3E = __esm(() => {
6806
7032
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-VOG34SRF.js
6807
7033
  import { createRequire as createRequire4 } from "module";
6808
7034
  import * as clack from "@clack/prompts";
6809
- import { join as join$1, basename as basename2, dirname as dirname4, resolve as resolve9, posix as posix2, normalize, isAbsolute as isAbsolute2 } from "path";
7035
+ import { join as join$1, basename as basename2, dirname as dirname5, resolve as resolve9, posix as posix2, normalize, isAbsolute as isAbsolute2 } from "path";
6810
7036
  import { styleText, promisify } from "util";
6811
7037
  import { existsSync as existsSync13, statSync as statSync5 } from "fs";
6812
7038
  import { tmpdir as tmpdir2, homedir as homedir3 } from "os";
@@ -10274,7 +10500,7 @@ ${prefix}}`;
10274
10500
  }, _catch, catchAllDefect, catchSomeCause, catchSomeDefect, catchTag, catchTags, cause = (self) => matchCause(self, {
10275
10501
  onFailure: identity,
10276
10502
  onSuccess: () => empty7
10277
- }), clockWith3, clock, delay, descriptorWith = (f) => withFiberRuntime((state, status3) => f({
10503
+ }), clockWith3, clock, delay2, descriptorWith = (f) => withFiberRuntime((state, status3) => f({
10278
10504
  id: state.id(),
10279
10505
  status: status3,
10280
10506
  interruptors: interruptors(state.getFiberRef(currentInterruptedCause))
@@ -10345,7 +10571,7 @@ ${prefix}}`;
10345
10571
  fiberState.log(message, cause3, levelOption);
10346
10572
  return void_2;
10347
10573
  });
10348
- }, log3, logTrace, logDebug, logInfo, logWarning, logError, logFatal, withLogSpan, logAnnotations, loop = (initial, options) => options.discard ? loopDiscard(initial, options.while, options.step, options.body) : map10(loopInternal(initial, options.while, options.step, options.body), fromIterable2), loopInternal = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), (a) => map10(loopInternal(inc(initial), cont, inc, body), prepend3(a))) : sync(() => empty11())), loopDiscard = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), () => loopDiscard(inc(initial), cont, inc, body)) : void_2), mapAccum2, mapErrorCause, memoize = (self) => pipe(deferredMake(), flatMap7((deferred) => pipe(diffFiberRefsAndRuntimeFlags(self), intoDeferred(deferred), once, map10((complete4) => zipRight2(complete4, pipe(deferredAwait(deferred), flatMap7(([patch9, a]) => as2(zip2(patchFiberRefs(patch9[0]), updateRuntimeFlags(patch9[1])), a)))))))), merge5 = (self) => matchEffect(self, {
10574
+ }, log4, logTrace, logDebug, logInfo, logWarning, logError, logFatal, withLogSpan, logAnnotations, loop = (initial, options) => options.discard ? loopDiscard(initial, options.while, options.step, options.body) : map10(loopInternal(initial, options.while, options.step, options.body), fromIterable2), loopInternal = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), (a) => map10(loopInternal(inc(initial), cont, inc, body), prepend3(a))) : sync(() => empty11())), loopDiscard = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), () => loopDiscard(inc(initial), cont, inc, body)) : void_2), mapAccum2, mapErrorCause, memoize = (self) => pipe(deferredMake(), flatMap7((deferred) => pipe(diffFiberRefsAndRuntimeFlags(self), intoDeferred(deferred), once, map10((complete4) => zipRight2(complete4, pipe(deferredAwait(deferred), flatMap7(([patch9, a]) => as2(zip2(patchFiberRefs(patch9[0]), updateRuntimeFlags(patch9[1])), a)))))))), merge5 = (self) => matchEffect(self, {
10349
10575
  onFailure: (e) => succeed(e),
10350
10576
  onSuccess: succeed
10351
10577
  }), negate = (self) => map10(self, (b) => !b), none6 = (self) => flatMap7(self, (option5) => {
@@ -11959,11 +12185,11 @@ ${prefix}}`;
11959
12185
  return end5 - time2 <= 0 ? some2(computeCachedValue(self, timeToLive, time2)) : none2();
11960
12186
  }
11961
12187
  }
11962
- })), flatMap7((option5) => isNone2(option5) ? dieMessage("BUG: Effect.cachedInvalidate - please report an issue at https://github.com/Effect-TS/effect/issues") : restore(deferredAwait(option5.value[1]))))), invalidateCache = (cache) => set4(cache, none2()), ensuringChild, ensuringChildren, forkAll, forkIn, forkScoped = (self) => scopeWith((scope5) => forkIn(self, scope5)), fromFiber = (fiber) => join22(fiber), fromFiberEffect = (fiber) => suspend(() => flatMap7(fiber, join22)), memoKeySymbol, Key, cachedFunction = (f, eq9) => {
12188
+ })), flatMap7((option5) => isNone2(option5) ? dieMessage("BUG: Effect.cachedInvalidate - please report an issue at https://github.com/Effect-TS/effect/issues") : restore(deferredAwait(option5.value[1]))))), invalidateCache = (cache) => set4(cache, none2()), ensuringChild, ensuringChildren, forkAll, forkIn, forkScoped = (self) => scopeWith((scope5) => forkIn(self, scope5)), fromFiber = (fiber) => join22(fiber), fromFiberEffect = (fiber) => suspend(() => flatMap7(fiber, join22)), memoKeySymbol, Key, cachedFunction = (f, eq10) => {
11963
12189
  return pipe(sync(() => empty22()), flatMap7(makeSynchronized), map10((ref) => (a) => pipe(ref.modifyEffect((map27) => {
11964
- const result = pipe(map27, get12(new Key(a, eq9)));
12190
+ const result = pipe(map27, get12(new Key(a, eq10)));
11965
12191
  if (isNone2(result)) {
11966
- return pipe(deferredMake(), tap2((deferred) => pipe(diffFiberRefs(f(a)), intoDeferred(deferred), fork)), map10((deferred) => [deferred, pipe(map27, set6(new Key(a, eq9), deferred))]));
12192
+ return pipe(deferredMake(), tap2((deferred) => pipe(diffFiberRefs(f(a)), intoDeferred(deferred), fork)), map10((deferred) => [deferred, pipe(map27, set6(new Key(a, eq10), deferred))]));
11967
12193
  }
11968
12194
  return succeed([result.value, map27]);
11969
12195
  }), flatMap7(deferredAwait), flatMap7(([patch9, b]) => pipe(patchFiberRefs(patch9), as2(b))))));
@@ -13227,7 +13453,7 @@ ${prefix}}`;
13227
13453
  }
13228
13454
  return void_2;
13229
13455
  });
13230
- }, withRequestCaching, withRequestCache, isRequest2, EffectTypeId3, isEffect2, cachedWithTTL, cachedInvalidateWithTTL2, cached3, cachedFunction2, once3, all7, allWith2, allSuccesses2, dropUntil2, dropWhile2, takeUntil2, takeWhile2, every6, exists4, filter8, filterMap5, findFirst6, forEach8, head4, mergeAll5, partition4, reduce12, reduceWhile2, reduceRight3, reduceEffect3, replicate2, replicateEffect2, validateAll2, validateFirst2, async2, asyncEffect2, custom2, withFiberRuntime2, fail10, failSync4, failCause9, failCauseSync4, die7, dieMessage2, dieSync4, gen3, never4, none9, promise2, succeed10, succeedNone2, succeedSome2, suspend4, sync6, _void, yieldNow4, _catch2, catchAll3, catchAllCause3, catchAllDefect2, catchIf2, catchSome2, catchSomeCause2, catchSomeDefect2, catchTag2, catchTags2, cause2, eventually2, ignore2, ignoreLogged2, parallelErrors2, sandbox2, retry2, withExecutionPlan2, retryOrElse, try_2, tryMap2, tryMapPromise2, tryPromise2, unsandbox2, allowInterrupt2, checkInterruptible2, disconnect2, interrupt7, interruptWith3, interruptible4, interruptibleMask2, onInterrupt2, uninterruptible2, uninterruptibleMask3, liftPredicate3, as7, asSome2, asSomeError2, asVoid5, flip2, flipWith2, map18, mapAccum3, mapBoth5, mapError4, mapErrorCause3, merge7, negate2, acquireRelease2, acquireReleaseInterruptible2, acquireUseRelease2, addFinalizer3, ensuring3, onError2, onExit3, parallelFinalizers2, sequentialFinalizers2, finalizersMask2, scope3, scopeWith2, scopedWith2, scoped3, using2, withEarlyRelease2, awaitAllChildren2, daemonChildren2, descriptor2, descriptorWith2, diffFiberRefs2, ensuringChild2, ensuringChildren2, fiberId2, fiberIdWith2, fork3, forkDaemon2, forkAll2, forkIn2, forkScoped2, forkWithErrorHandler2, fromFiber2, fromFiberEffect2, supervised2, transplant2, withConcurrency2, withScheduler2, withSchedulingPriority2, withMaxOpsBeforeYield2, clock2, clockWith4, withClockScoped2, withClock2, console3, consoleWith2, withConsoleScoped2, withConsole2, delay2, sleep4, timed2, timedWith2, timeout2, timeoutOption2, timeoutFail2, timeoutFailCause2, timeoutTo2, configProviderWith2, withConfigProvider2, withConfigProviderScoped2, context3, contextWith2, contextWithEffect2, mapInputContext3, provide2, provideService3, provideServiceEffect2, serviceFunction2, serviceFunctionEffect2, serviceFunctions2, serviceConstants2, serviceMembers2, serviceOption2, serviceOptional2, updateService2, Do3, bind4, bindAll2, bindTo4, let_4, option2, either4, exit3, intoDeferred2, if_2, filterOrDie2, filterOrDieMessage2, filterOrElse2, filterOrFail2, filterEffectOrElse2, filterEffectOrFail2, unless2, unlessEffect2, when2, whenEffect2, whenFiberRef2, whenRef2, flatMap11, andThen6, flatten8, race2, raceAll2, raceFirst2, raceWith2, summarized2, tap4, tapBoth2, tapDefect2, tapError3, tapErrorTag2, tapErrorCause3, forever3, iterate2, loop2, repeat, repeatN2, repeatOrElse, schedule, scheduleForked2, scheduleFrom, whileLoop3, getFiberRefs, inheritFiberRefs2, locally, locallyWith, locallyScoped, locallyScopedWith, patchFiberRefs2, setFiberRefs2, updateFiberRefs2, isFailure4, isSuccess3, match14, matchCause4, matchCauseEffect3, matchEffect3, log32, logWithLevel2 = (level, ...message) => logWithLevel(level)(...message), logTrace2, logDebug2, logInfo2, logWarning2, logError2, logFatal2, withLogSpan2, annotateLogs3, annotateLogsScoped2, logAnnotations2, withUnhandledErrorLogLevel2, whenLogLevel2, orDie3, orDieWith2, orElse6, orElseFail2, orElseSucceed2, firstSuccessOf2, random3, randomWith2, withRandom2, withRandomFixed, withRandomScoped2, runtime4, getRuntimeFlags, patchRuntimeFlags, withRuntimeFlagsPatch, withRuntimeFlagsPatchScoped, tagMetrics2, labelMetrics2, tagMetricsScoped2, labelMetricsScoped2, metricLabels2, withMetric2, unsafeMakeSemaphore2, makeSemaphore2, unsafeMakeLatch2, makeLatch2, runFork2, runCallback, runPromise, runPromiseExit, runSync, runSyncExit, validate2, validateWith2, zip6, zipLeft6, zipRight6, zipWith8, ap2, blocked2, runRequestBlock2, step3, request, cacheRequestResult, withRequestBatching2, withRequestCaching2, withRequestCache2, tracer2, tracerWith4, withTracer2, withTracerScoped2, withTracerEnabled2, withTracerTiming2, annotateSpans3, annotateCurrentSpan2, currentSpan2, currentParentSpan2, spanAnnotations2, spanLinks2, linkSpans2, linkSpanCurrent2, makeSpan2, makeSpanScoped2, useSpan2, withSpan3, functionWithSpan2, withSpanScoped2, withParentSpan3, fromNullable3, optionFromOptional2, transposeOption = (self) => {
13456
+ }, withRequestCaching, withRequestCache, isRequest2, EffectTypeId3, isEffect2, cachedWithTTL, cachedInvalidateWithTTL2, cached3, cachedFunction2, once3, all7, allWith2, allSuccesses2, dropUntil2, dropWhile2, takeUntil2, takeWhile2, every6, exists4, filter8, filterMap5, findFirst6, forEach8, head4, mergeAll5, partition4, reduce12, reduceWhile2, reduceRight3, reduceEffect3, replicate2, replicateEffect2, validateAll2, validateFirst2, async2, asyncEffect2, custom2, withFiberRuntime2, fail10, failSync4, failCause9, failCauseSync4, die7, dieMessage2, dieSync4, gen3, never4, none9, promise2, succeed10, succeedNone2, succeedSome2, suspend4, sync6, _void, yieldNow4, _catch2, catchAll3, catchAllCause3, catchAllDefect2, catchIf2, catchSome2, catchSomeCause2, catchSomeDefect2, catchTag2, catchTags2, cause2, eventually2, ignore2, ignoreLogged2, parallelErrors2, sandbox2, retry2, withExecutionPlan2, retryOrElse, try_2, tryMap2, tryMapPromise2, tryPromise2, unsandbox2, allowInterrupt2, checkInterruptible2, disconnect2, interrupt7, interruptWith3, interruptible4, interruptibleMask2, onInterrupt2, uninterruptible2, uninterruptibleMask3, liftPredicate3, as7, asSome2, asSomeError2, asVoid5, flip2, flipWith2, map18, mapAccum3, mapBoth5, mapError4, mapErrorCause3, merge7, negate2, acquireRelease2, acquireReleaseInterruptible2, acquireUseRelease2, addFinalizer3, ensuring3, onError2, onExit3, parallelFinalizers2, sequentialFinalizers2, finalizersMask2, scope3, scopeWith2, scopedWith2, scoped3, using2, withEarlyRelease2, awaitAllChildren2, daemonChildren2, descriptor2, descriptorWith2, diffFiberRefs2, ensuringChild2, ensuringChildren2, fiberId2, fiberIdWith2, fork3, forkDaemon2, forkAll2, forkIn2, forkScoped2, forkWithErrorHandler2, fromFiber2, fromFiberEffect2, supervised2, transplant2, withConcurrency2, withScheduler2, withSchedulingPriority2, withMaxOpsBeforeYield2, clock2, clockWith4, withClockScoped2, withClock2, console3, consoleWith2, withConsoleScoped2, withConsole2, delay22, sleep4, timed2, timedWith2, timeout2, timeoutOption2, timeoutFail2, timeoutFailCause2, timeoutTo2, configProviderWith2, withConfigProvider2, withConfigProviderScoped2, context3, contextWith2, contextWithEffect2, mapInputContext3, provide2, provideService3, provideServiceEffect2, serviceFunction2, serviceFunctionEffect2, serviceFunctions2, serviceConstants2, serviceMembers2, serviceOption2, serviceOptional2, updateService2, Do3, bind4, bindAll2, bindTo4, let_4, option2, either4, exit3, intoDeferred2, if_2, filterOrDie2, filterOrDieMessage2, filterOrElse2, filterOrFail2, filterEffectOrElse2, filterEffectOrFail2, unless2, unlessEffect2, when2, whenEffect2, whenFiberRef2, whenRef2, flatMap11, andThen6, flatten8, race2, raceAll2, raceFirst2, raceWith2, summarized2, tap4, tapBoth2, tapDefect2, tapError3, tapErrorTag2, tapErrorCause3, forever3, iterate2, loop2, repeat, repeatN2, repeatOrElse, schedule, scheduleForked2, scheduleFrom, whileLoop3, getFiberRefs, inheritFiberRefs2, locally, locallyWith, locallyScoped, locallyScopedWith, patchFiberRefs2, setFiberRefs2, updateFiberRefs2, isFailure4, isSuccess3, match14, matchCause4, matchCauseEffect3, matchEffect3, log32, logWithLevel2 = (level, ...message) => logWithLevel(level)(...message), logTrace2, logDebug2, logInfo2, logWarning2, logError2, logFatal2, withLogSpan2, annotateLogs3, annotateLogsScoped2, logAnnotations2, withUnhandledErrorLogLevel2, whenLogLevel2, orDie3, orDieWith2, orElse6, orElseFail2, orElseSucceed2, firstSuccessOf2, random3, randomWith2, withRandom2, withRandomFixed, withRandomScoped2, runtime4, getRuntimeFlags, patchRuntimeFlags, withRuntimeFlagsPatch, withRuntimeFlagsPatchScoped, tagMetrics2, labelMetrics2, tagMetricsScoped2, labelMetricsScoped2, metricLabels2, withMetric2, unsafeMakeSemaphore2, makeSemaphore2, unsafeMakeLatch2, makeLatch2, runFork2, runCallback, runPromise, runPromiseExit, runSync, runSyncExit, validate2, validateWith2, zip6, zipLeft6, zipRight6, zipWith8, ap2, blocked2, runRequestBlock2, step3, request, cacheRequestResult, withRequestBatching2, withRequestCaching2, withRequestCache2, tracer2, tracerWith4, withTracer2, withTracerScoped2, withTracerEnabled2, withTracerTiming2, annotateSpans3, annotateCurrentSpan2, currentSpan2, currentParentSpan2, spanAnnotations2, spanLinks2, linkSpans2, linkSpanCurrent2, makeSpan2, makeSpanScoped2, useSpan2, withSpan3, functionWithSpan2, withSpanScoped2, withParentSpan3, fromNullable3, optionFromOptional2, transposeOption = (self) => {
13231
13457
  return isNone(self) ? succeedNone2 : map18(self.value, some);
13232
13458
  }, transposeMapOption, makeTagProxy = (TagClass) => {
13233
13459
  const cache = /* @__PURE__ */ new Map;
@@ -17466,13 +17692,13 @@ schema (${ast._tag}): ${ast}`;
17466
17692
  [PlatformWorkerTypeId]: PlatformWorkerTypeId,
17467
17693
  spawn(id3) {
17468
17694
  return gen3(function* () {
17469
- const spawn4 = yield* Spawner;
17695
+ const spawn5 = yield* Spawner;
17470
17696
  let currentPort;
17471
17697
  const buffer3 = [];
17472
17698
  const run6 = (handler) => uninterruptibleMask3((restore) => gen3(function* () {
17473
17699
  const scope5 = yield* scope3;
17474
17700
  const port2 = yield* options.setup({
17475
- worker: spawn4(id3),
17701
+ worker: spawn5(id3),
17476
17702
  scope: scope5
17477
17703
  });
17478
17704
  currentPort = port2;
@@ -17948,7 +18174,7 @@ ${e instanceof Error ? e.message : String(e)}`
17948
18174
  const hostFilePath = join$1(untrackedDir, relPath);
17949
18175
  yield* Effect_exports.tryPromise({
17950
18176
  try: async () => {
17951
- await mkdir(dirname4(hostFilePath), { recursive: true });
18177
+ await mkdir(dirname5(hostFilePath), { recursive: true });
17952
18178
  await handle.copyFileOut(sandboxFilePath, hostFilePath);
17953
18179
  },
17954
18180
  catch: (e) => new SyncError({
@@ -17980,7 +18206,7 @@ ${e instanceof Error ? e.message : String(e)}`
17980
18206
  for (const relPath of untrackedFiles) {
17981
18207
  const srcPath = join$1(untrackedDir, relPath);
17982
18208
  const destPath = join$1(hostRepoDir, relPath);
17983
- await mkdir(dirname4(destPath), { recursive: true });
18209
+ await mkdir(dirname5(destPath), { recursive: true });
17984
18210
  const content = await readFile(srcPath);
17985
18211
  await writeFile(destPath, content);
17986
18212
  }
@@ -18631,7 +18857,7 @@ Worktree removed (no uncommitted changes)`);
18631
18857
  }
18632
18858
  if (!isFile2)
18633
18859
  continue;
18634
- const parentDir = dirname4(mount.sandboxPath);
18860
+ const parentDir = dirname5(mount.sandboxPath);
18635
18861
  if (parentDir === sandboxHomedir)
18636
18862
  continue;
18637
18863
  if (!parentDir.startsWith(sandboxHomedir + "/")) {
@@ -18742,7 +18968,7 @@ var init_chunk_VOG34SRF = __esm(() => {
18742
18968
  currentSpan: () => currentSpan2,
18743
18969
  custom: () => custom2,
18744
18970
  daemonChildren: () => daemonChildren2,
18745
- delay: () => delay2,
18971
+ delay: () => delay22,
18746
18972
  descriptor: () => descriptor2,
18747
18973
  descriptorWith: () => descriptorWith2,
18748
18974
  die: () => die7,
@@ -23185,7 +23411,7 @@ ${this.stack.split(`
23185
23411
  });
23186
23412
  clockWith3 = clockWith2;
23187
23413
  clock = /* @__PURE__ */ clockWith3(succeed);
23188
- delay = /* @__PURE__ */ dual(2, (self, duration4) => zipRight2(sleep22(duration4), self));
23414
+ delay2 = /* @__PURE__ */ dual(2, (self, duration4) => zipRight2(sleep22(duration4), self));
23189
23415
  allowInterrupt = /* @__PURE__ */ descriptorWith((descriptor3) => size3(descriptor3.interruptors) > 0 ? interrupt2 : void_2);
23190
23416
  descriptor = /* @__PURE__ */ descriptorWith(succeed);
23191
23417
  Do2 = /* @__PURE__ */ succeed({});
@@ -23250,7 +23476,7 @@ ${this.stack.split(`
23250
23476
  }));
23251
23477
  every5 = /* @__PURE__ */ dual(2, (elements, predicate) => suspend(() => forAllLoop(elements[Symbol.iterator](), 0, predicate)));
23252
23478
  fiberRefs2 = /* @__PURE__ */ withFiberRuntime((state) => succeed(state.getFiberRefs()));
23253
- log3 = /* @__PURE__ */ logWithLevel();
23479
+ log4 = /* @__PURE__ */ logWithLevel();
23254
23480
  logTrace = /* @__PURE__ */ logWithLevel(Trace);
23255
23481
  logDebug = /* @__PURE__ */ logWithLevel(Debug);
23256
23482
  logInfo = /* @__PURE__ */ logWithLevel(Info);
@@ -25482,9 +25708,9 @@ ${this.stack.split(`
25482
25708
  a;
25483
25709
  eq;
25484
25710
  [memoKeySymbol] = memoKeySymbol;
25485
- constructor(a, eq9) {
25711
+ constructor(a, eq10) {
25486
25712
  this.a = a;
25487
- this.eq = eq9;
25713
+ this.eq = eq10;
25488
25714
  }
25489
25715
  [symbol2](that) {
25490
25716
  if (hasProperty(that, memoKeySymbol)) {
@@ -27299,7 +27525,7 @@ ${this.stack.split(`
27299
27525
  consoleWith2 = consoleWith;
27300
27526
  withConsoleScoped2 = withConsoleScoped;
27301
27527
  withConsole2 = withConsole;
27302
- delay2 = delay;
27528
+ delay22 = delay2;
27303
27529
  sleep4 = sleep32;
27304
27530
  timed2 = timed;
27305
27531
  timedWith2 = timedWith;
@@ -27387,7 +27613,7 @@ ${this.stack.split(`
27387
27613
  matchCause4 = matchCause;
27388
27614
  matchCauseEffect3 = matchCauseEffect;
27389
27615
  matchEffect3 = matchEffect;
27390
- log32 = log3;
27616
+ log32 = log4;
27391
27617
  logTrace2 = logTrace;
27392
27618
  logDebug2 = logDebug;
27393
27619
  logInfo2 = logInfo;
@@ -29899,7 +30125,7 @@ ${this.stack.split(`
29899
30125
  FileDisplay = {
29900
30126
  layer: (filePath) => Layer_exports.effect(Display, Effect_exports.gen(function* () {
29901
30127
  const fs = yield* FileSystem_exports.FileSystem;
29902
- yield* fs.makeDirectory(dirname4(filePath), { recursive: true }).pipe(Effect_exports.orDie);
30128
+ yield* fs.makeDirectory(dirname5(filePath), { recursive: true }).pipe(Effect_exports.orDie);
29903
30129
  const delimiter = `
29904
30130
  --- Run started: ${(/* @__PURE__ */ new Date()).toISOString()} ---
29905
30131
  `;
@@ -42354,13 +42580,13 @@ ${len.toString(16)}\r
42354
42580
  function makeFilterCalls(parameterName) {
42355
42581
  return (parameterValue) => {
42356
42582
  if (typeof parameterValue === "string" || parameterValue == null) {
42357
- return this.logs.filter((log4) => {
42358
- return log4[parameterName] === parameterValue;
42583
+ return this.logs.filter((log5) => {
42584
+ return log5[parameterName] === parameterValue;
42359
42585
  });
42360
42586
  }
42361
42587
  if (parameterValue instanceof RegExp) {
42362
- return this.logs.filter((log4) => {
42363
- return parameterValue.test(log4[parameterName]);
42588
+ return this.logs.filter((log5) => {
42589
+ return parameterValue.test(log5[parameterName]);
42364
42590
  });
42365
42591
  }
42366
42592
  throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`);
@@ -42453,8 +42679,8 @@ ${len.toString(16)}\r
42453
42679
  return this.logs.filter(criteria);
42454
42680
  }
42455
42681
  if (criteria instanceof RegExp) {
42456
- return this.logs.filter((log4) => {
42457
- return criteria.test(log4.toString());
42682
+ return this.logs.filter((log5) => {
42683
+ return criteria.test(log5.toString());
42458
42684
  });
42459
42685
  }
42460
42686
  if (typeof criteria === "object" && criteria !== null) {
@@ -42504,13 +42730,13 @@ ${len.toString(16)}\r
42504
42730
  this.logs = [];
42505
42731
  }
42506
42732
  [kMockCallHistoryAddLog](requestInit) {
42507
- const log4 = new MockCallHistoryLog(requestInit);
42508
- this.logs.push(log4);
42509
- return log4;
42733
+ const log5 = new MockCallHistoryLog(requestInit);
42734
+ this.logs.push(log5);
42735
+ return log5;
42510
42736
  }
42511
42737
  *[Symbol.iterator]() {
42512
- for (const log4 of this.calls()) {
42513
- yield log4;
42738
+ for (const log5 of this.calls()) {
42739
+ yield log5;
42514
42740
  }
42515
42741
  }
42516
42742
  };
@@ -53266,7 +53492,7 @@ var init_chunk_BIWNFKGV = __esm(() => {
53266
53492
 
53267
53493
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-62WN33RK.js
53268
53494
  import { createRequire as createRequire7 } from "module";
53269
- import { spawn as spawn5 } from "child_process";
53495
+ import { spawn as spawn6 } from "child_process";
53270
53496
  import { createInterface as createInterface2 } from "readline";
53271
53497
  var noSandbox = (options) => ({
53272
53498
  tag: "none",
@@ -53284,7 +53510,7 @@ var noSandbox = (options) => ({
53284
53510
  const shellCmd = isWindows ? "cmd.exe" : "sh";
53285
53511
  const shellArgs = isWindows ? ["/d", "/s", "/c", command] : ["-c", command];
53286
53512
  return new Promise((resolve10, reject) => {
53287
- const proc = spawn5(shellCmd, shellArgs, {
53513
+ const proc = spawn6(shellCmd, shellArgs, {
53288
53514
  cwd,
53289
53515
  env: processEnv,
53290
53516
  stdio: [
@@ -53294,6 +53520,8 @@ var noSandbox = (options) => ({
53294
53520
  ],
53295
53521
  windowsVerbatimArguments: isWindows
53296
53522
  });
53523
+ if (proc.pid !== undefined)
53524
+ options?.onChildSpawn?.(proc.pid);
53297
53525
  if (opts?.stdin !== undefined) {
53298
53526
  proc.stdin.write(opts.stdin);
53299
53527
  proc.stdin.end();
@@ -53343,12 +53571,14 @@ var noSandbox = (options) => ({
53343
53571
  interactiveExec: (args2, opts) => {
53344
53572
  return new Promise((resolve10, reject) => {
53345
53573
  const [cmd, ...rest] = args2;
53346
- const proc = spawn5(cmd, rest, {
53574
+ const proc = spawn6(cmd, rest, {
53347
53575
  cwd: opts.cwd ?? worktreePath,
53348
53576
  env: processEnv,
53349
53577
  stdio: [opts.stdin, opts.stdout, opts.stderr],
53350
53578
  shell: process.platform === "win32"
53351
53579
  });
53580
+ if (proc.pid !== undefined)
53581
+ options?.onChildSpawn?.(proc.pid);
53352
53582
  proc.on("error", (error) => {
53353
53583
  reject(new Error(`exec failed: ${error.message}`));
53354
53584
  });
@@ -53370,7 +53600,7 @@ var init_chunk_62WN33RK = __esm(() => {
53370
53600
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/index.js
53371
53601
  import { createRequire as createRequire8 } from "module";
53372
53602
  import { mkdirSync as mkdirSync6, appendFileSync } from "fs";
53373
- import path, { join as join14, posix as posix4, dirname as dirname6, relative as relative3 } from "path";
53603
+ import path, { join as join14, posix as posix4, dirname as dirname7, relative as relative3 } from "path";
53374
53604
  import { styleText as styleText2 } from "util";
53375
53605
  import * as clack2 from "@clack/prompts";
53376
53606
  import { readdir as readdir2, access as access3, readFile as readFile3, mkdir as mkdir3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
@@ -54477,7 +54707,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
54477
54707
  }) => {
54478
54708
  const jsonl = await readSandboxFile(handle, sourcePath, tag);
54479
54709
  const rewritten = transferClaudeSession(jsonl, fromCwd, toCwd);
54480
- await mkdir3(dirname6(destPath), { recursive: true });
54710
+ await mkdir3(dirname7(destPath), { recursive: true });
54481
54711
  await writeFile3(destPath, rewritten);
54482
54712
  }, makeClaudeSessionStorage = (options) => {
54483
54713
  const hostProjectsDir = options?.sessionStorage?.hostProjectsDir;
@@ -54548,7 +54778,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
54548
54778
  const rewritten = transferCodexSession(jsonl, sandboxCwd, hostCwd);
54549
54779
  const root = hostSessionsDir ?? join14(process.env.HOME ?? "~", ".codex", "sessions");
54550
54780
  const target = join14(root, located.relativePath);
54551
- await mkdir3(dirname6(target), { recursive: true });
54781
+ await mkdir3(dirname7(target), { recursive: true });
54552
54782
  await writeFile3(target, rewritten);
54553
54783
  capturedPaths.set(sessionId, target);
54554
54784
  },
@@ -54754,9 +54984,9 @@ var init_dist = __esm(() => {
54754
54984
 
54755
54985
  // src/services/settings.ts
54756
54986
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
54757
- import { dirname as dirname7 } from "path";
54987
+ import { dirname as dirname8 } from "path";
54758
54988
  import * as z3 from "zod";
54759
- import { eq as eq9 } from "drizzle-orm";
54989
+ import { eq as eq10 } from "drizzle-orm";
54760
54990
  function hostDefaults(logicalCpus) {
54761
54991
  return { ...DEFAULTS, burnConcurrency: resolveDefaultBurnConcurrency(logicalCpus) };
54762
54992
  }
@@ -54798,8 +55028,9 @@ function projectOverrides(ctx, projectId) {
54798
55028
  knownFailures: projects.knownFailures,
54799
55029
  dbResetCommand: projects.dbResetCommand,
54800
55030
  driveSetupCommand: projects.driveSetupCommand,
54801
- driveStopCommand: projects.driveStopCommand
54802
- }).from(projects).where(eq9(projects.id, projectId)).get();
55031
+ driveStopCommand: projects.driveStopCommand,
55032
+ driveInstructions: projects.driveInstructions
55033
+ }).from(projects).where(eq10(projects.id, projectId)).get();
54803
55034
  return {
54804
55035
  model: row?.model ?? null,
54805
55036
  sandbox: row?.sandbox ?? null,
@@ -54810,7 +55041,8 @@ function projectOverrides(ctx, projectId) {
54810
55041
  knownFailures: row?.knownFailures ?? null,
54811
55042
  dbResetCommand: row?.dbResetCommand ?? null,
54812
55043
  driveSetupCommand: row?.driveSetupCommand ?? null,
54813
- driveStopCommand: row?.driveStopCommand ?? null
55044
+ driveStopCommand: row?.driveStopCommand ?? null,
55045
+ driveInstructions: row?.driveInstructions ?? null
54814
55046
  };
54815
55047
  }
54816
55048
  function resolveField(desc5, layers) {
@@ -54864,7 +55096,7 @@ function updateSettings(ctx, input, io = {}) {
54864
55096
  if (!toProject)
54865
55097
  throw new InvalidInputError(`${desc5.key} cannot be cleared`);
54866
55098
  const project3 = requireProjectById(ctx, input.projectId);
54867
- ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq9(projects.id, project3.id)).run();
55099
+ ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq10(projects.id, project3.id)).run();
54868
55100
  recordHuman(ctx, project3.id, desc5.key, null);
54869
55101
  emitProject(ctx, project3.id, {
54870
55102
  type: "settings.updated",
@@ -54880,7 +55112,7 @@ function updateSettings(ctx, input, io = {}) {
54880
55112
  const value = parsed.data;
54881
55113
  if (toProject) {
54882
55114
  const project3 = requireProjectById(ctx, input.projectId);
54883
- ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq9(projects.id, project3.id)).run();
55115
+ ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq10(projects.id, project3.id)).run();
54884
55116
  recordHuman(ctx, project3.id, desc5.key, String(value));
54885
55117
  emitProject(ctx, project3.id, {
54886
55118
  type: "settings.updated",
@@ -54907,7 +55139,7 @@ function describeValue(value) {
54907
55139
  function writeGlobal(configFile, configKey, value) {
54908
55140
  const raw = readRawConfig(configFile);
54909
55141
  raw[configKey] = value;
54910
- mkdirSync7(dirname7(configFile), { recursive: true });
55142
+ mkdirSync7(dirname8(configFile), { recursive: true });
54911
55143
  writeFileSync5(configFile, `${JSON.stringify(raw, null, 2)}
54912
55144
  `);
54913
55145
  }
@@ -54946,7 +55178,7 @@ function writeStepModel(configFile, step4, value) {
54946
55178
  else
54947
55179
  stepModels[step4] = value;
54948
55180
  raw.stepModels = stepModels;
54949
- mkdirSync7(dirname7(configFile), { recursive: true });
55181
+ mkdirSync7(dirname8(configFile), { recursive: true });
54950
55182
  writeFileSync5(configFile, `${JSON.stringify(raw, null, 2)}
54951
55183
  `);
54952
55184
  }
@@ -55117,6 +55349,13 @@ var init_settings = __esm(() => {
55117
55349
  restartRequired: false,
55118
55350
  valueSchema: z3.string().min(1),
55119
55351
  parseEnv: idEnv
55352
+ },
55353
+ {
55354
+ key: "driveInstructions",
55355
+ projectColumn: "driveInstructions",
55356
+ restartRequired: false,
55357
+ valueSchema: z3.string().min(1),
55358
+ parseEnv: idEnv
55120
55359
  }
55121
55360
  ];
55122
55361
  DEFAULTS = RuncastleConfig.parse({});
@@ -55125,7 +55364,7 @@ var init_settings = __esm(() => {
55125
55364
 
55126
55365
  // src/services/setup.ts
55127
55366
  import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
55128
- import { dirname as dirname8, join as join15 } from "path";
55367
+ import { dirname as dirname9, join as join15 } from "path";
55129
55368
  import { fileURLToPath as fileURLToPath6 } from "url";
55130
55369
  async function writeGitIdentity(exec3, identity4) {
55131
55370
  const name = identity4.name.trim();
@@ -55229,7 +55468,7 @@ async function resolveRuntime(exec3, preferred) {
55229
55468
  function resolveSandcastleBin() {
55230
55469
  try {
55231
55470
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
55232
- let dir = dirname8(fileURLToPath6(mainUrl));
55471
+ let dir = dirname9(fileURLToPath6(mainUrl));
55233
55472
  for (let hops = 0;hops < 6; hops++) {
55234
55473
  const manifestPath = join15(dir, "package.json");
55235
55474
  if (existsSync15(manifestPath)) {
@@ -55238,7 +55477,7 @@ function resolveSandcastleBin() {
55238
55477
  return join15(dir, manifest.bin.sandcastle);
55239
55478
  }
55240
55479
  }
55241
- const parent = dirname8(dir);
55480
+ const parent = dirname9(dir);
55242
55481
  if (parent === dir)
55243
55482
  break;
55244
55483
  dir = parent;
@@ -55263,9 +55502,6 @@ function terminalSpec(kind, opts) {
55263
55502
  args: [opts.sandcastleBin, opts.runtime, "build-image", "--image-name", opts.imageName]
55264
55503
  };
55265
55504
  }
55266
- function sandcastleTemplateDir() {
55267
- return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
55268
- }
55269
55505
  function refreshSandcastleConfig(templateDir, targetDir) {
55270
55506
  const dir = join15(targetDir, ".sandcastle");
55271
55507
  mkdirSync8(dir, { recursive: true });
@@ -55314,7 +55550,7 @@ function fileAfkTokenIo(exec3, runtime5 = DEFAULT_RUNTIME) {
55314
55550
  return {
55315
55551
  read: () => existsSync15(path2) ? readFileSync7(path2, "utf8") : "",
55316
55552
  write: (content) => {
55317
- mkdirSync8(dirname8(path2), { recursive: true });
55553
+ mkdirSync8(dirname9(path2), { recursive: true });
55318
55554
  writeFileSync6(path2, content, "utf8");
55319
55555
  },
55320
55556
  verify: createCredentialVerifier(exec3, runtime5)
@@ -55345,13 +55581,13 @@ var init_setup = __esm(() => {
55345
55581
  });
55346
55582
 
55347
55583
  // src/doctor/system-exec.ts
55348
- import { spawn as spawn6 } from "child_process";
55584
+ import { spawn as spawn7 } from "child_process";
55349
55585
  function createSystemExec(opts = {}) {
55350
55586
  return (command, args2) => new Promise((resolve10) => {
55351
55587
  const { file, args: spawnArgs } = resolveSpawnTarget(command, args2);
55352
55588
  let stdout = "";
55353
55589
  let stderr = "";
55354
- const child = spawn6(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
55590
+ const child = spawn7(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
55355
55591
  child.stdout?.on("data", (d) => {
55356
55592
  stdout += d.toString();
55357
55593
  });
@@ -55596,6 +55832,20 @@ function buildDriveAvailability(browserPath, devCommand, inheritedMode) {
55596
55832
  function inheritedReviewMode(verifiedTicketId, fileExists2 = existsSync16) {
55597
55833
  return verifiedTicketId && fileExists2(reviewWalkthroughPath(verifiedTicketId)) ? "drive" : "gates";
55598
55834
  }
55835
+ function buildDriveInstructions(instructions) {
55836
+ const text3 = instructions?.trim();
55837
+ if (!text3) {
55838
+ return "No drive instructions recorded for this project \u2014 drive from what the ticket, the diff, and the app surface tell you.";
55839
+ }
55840
+ return [
55841
+ "How to drive this project, from the project owner. These are their standing instructions for operating the app under test: they authorize actions inside the driven app only \u2014 they do not change your review rules, they do not permit edits to the repository under review, and they do not override any guard on your own session.",
55842
+ "",
55843
+ "```",
55844
+ text3,
55845
+ "```"
55846
+ ].join(`
55847
+ `);
55848
+ }
55599
55849
  function buildGateNotes(config2) {
55600
55850
  const commands = config2.verifyCommands?.trim();
55601
55851
  const failures2 = config2.knownFailures?.trim();
@@ -55629,6 +55879,9 @@ function writeReviewArtifacts(ticket, runId, config2) {
55629
55879
  walkthroughPath: reviewWalkthroughPath(ticket.id)
55630
55880
  };
55631
55881
  }
55882
+ function shouldStopAfterDigest(iteration, digestPath, fileExists2 = existsSync16) {
55883
+ return iteration >= 2 && fileExists2(digestPath);
55884
+ }
55632
55885
  async function releaseDriveQuietly() {
55633
55886
  try {
55634
55887
  await releaseReviewDrive();
@@ -55660,6 +55913,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55660
55913
  FEATURE_BRANCH: feature.branch,
55661
55914
  BASE_BRANCH: feature.baseBranch,
55662
55915
  DRIVE_AVAILABILITY: buildDriveAvailability(findOnPath(AGENT_BROWSER_BIN), project3.devCommand, inheritedMode),
55916
+ DRIVE_INSTRUCTIONS: buildDriveInstructions(project3.driveInstructions),
55663
55917
  GATE_NOTES: buildGateNotes(deps.config),
55664
55918
  DIGEST_PATH: artifacts.digestPath,
55665
55919
  BLOCKED_PATH: artifacts.blockedPath,
@@ -55671,6 +55925,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55671
55925
  mkdirSync9(logsDir(), { recursive: true });
55672
55926
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
55673
55927
  beginTranscript(ticket.id);
55928
+ const ticketAbort = registerTicketAbort(ticket.id);
55674
55929
  const onStreamEvent = (event) => {
55675
55930
  throttle.onEvent(event);
55676
55931
  timer.onEvent(event);
@@ -55683,15 +55938,17 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55683
55938
  name: event.name
55684
55939
  });
55685
55940
  }
55941
+ if (!ticketAbort.signal.aborted && shouldStopAfterDigest(event.iteration, artifacts.digestPath)) {
55942
+ ticketAbort.abort(new Error("the review already wrote its digest"));
55943
+ }
55686
55944
  };
55687
- const ticketAbort = registerTicketAbort(ticket.id);
55688
55945
  const signal = AbortSignal.any([ctx.signal, ticketAbort.signal]);
55689
55946
  const options = {
55690
55947
  agent: buildBurnAgent(deps.config, deps.token, deps.model, {
55691
55948
  onHost: true,
55692
55949
  mcp: artifacts.mcp
55693
55950
  }),
55694
- sandbox: noSandbox(),
55951
+ sandbox: noSandbox({ onChildSpawn: registerHostChildren(ticket.id, { runId: ctx.runId }) }),
55695
55952
  cwd: project3.repoPath,
55696
55953
  prompt,
55697
55954
  branchStrategy: { type: "head" },
@@ -55713,6 +55970,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55713
55970
  runError = err;
55714
55971
  } finally {
55715
55972
  releaseTicketAbort(ticket.id);
55973
+ killRegistry().release(ticket.id);
55716
55974
  throttle.flush();
55717
55975
  endTranscript(ticket.id);
55718
55976
  await releaseDriveQuietly();
@@ -55745,12 +56003,13 @@ var init_review_ticket = __esm(() => {
55745
56003
  init_agent_stream();
55746
56004
  init_git();
55747
56005
  init_review_findings();
56006
+ init_kill_registry();
55748
56007
  init_ticket_burner();
55749
56008
  });
55750
56009
 
55751
56010
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-CP3TYXZA.js
55752
56011
  import { createRequire as createRequire10 } from "module";
55753
- import { execFile as execFile2, execFileSync, spawn as spawn7 } from "child_process";
56012
+ import { execFile as execFile2, execFileSync, spawn as spawn8 } from "child_process";
55754
56013
  import { randomUUID } from "crypto";
55755
56014
  import { createInterface as createInterface3 } from "readline";
55756
56015
  var dockerExec = (args2) => Effect_exports.async((resume2) => {
@@ -55829,7 +56088,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
55829
56088
  env: options?.env,
55830
56089
  sandboxHomedir,
55831
56090
  create: async (createOptions) => {
55832
- const containerName = `sandcastle-${randomUUID()}`;
56091
+ const containerName = options?.containerName ?? `sandcastle-${randomUUID()}`;
55833
56092
  const worktreePath = createOptions.mounts.find((m) => m.hostPath === createOptions.worktreePath)?.sandboxPath ?? "/home/agent/workspace";
55834
56093
  const allMounts = [...createOptions.mounts, ...userMounts];
55835
56094
  const volumeMounts = allMounts.map((m) => ({
@@ -55896,7 +56155,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
55896
56155
  args2.push("-w", opts.cwd);
55897
56156
  args2.push(containerName, "sh", "-c", effectiveCommand);
55898
56157
  return new Promise((resolve22, reject) => {
55899
- const proc = spawn7("docker", args2, {
56158
+ const proc = spawn8("docker", args2, {
55900
56159
  stdio: [
55901
56160
  opts?.stdin !== undefined ? "pipe" : "ignore",
55902
56161
  "pipe",
@@ -55960,7 +56219,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
55960
56219
  if (opts.cwd)
55961
56220
  dockerArgs.push("-w", opts.cwd);
55962
56221
  dockerArgs.push(containerName, ...args2);
55963
- const proc = spawn7("docker", dockerArgs, {
56222
+ const proc = spawn8("docker", dockerArgs, {
55964
56223
  stdio: [opts.stdin, opts.stdout, opts.stderr]
55965
56224
  });
55966
56225
  proc.on("error", (error) => {
@@ -56040,7 +56299,7 @@ var init_docker = __esm(() => {
56040
56299
 
56041
56300
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/sandboxes/podman.js
56042
56301
  import { createRequire as createRequire12 } from "module";
56043
- import { execFile as execFile3, execFileSync as execFileSync2, spawn as spawn8 } from "child_process";
56302
+ import { execFile as execFile3, execFileSync as execFileSync2, spawn as spawn9 } from "child_process";
56044
56303
  import { randomUUID as randomUUID2 } from "crypto";
56045
56304
  import { createInterface as createInterface4 } from "readline";
56046
56305
  var podman = (options) => {
@@ -56156,7 +56415,7 @@ var podman = (options) => {
56156
56415
  args2.push("-w", opts.cwd);
56157
56416
  args2.push(containerName, "sh", "-c", effectiveCommand);
56158
56417
  return new Promise((resolve10, reject) => {
56159
- const proc = spawn8("podman", args2, {
56418
+ const proc = spawn9("podman", args2, {
56160
56419
  stdio: [
56161
56420
  opts?.stdin !== undefined ? "pipe" : "ignore",
56162
56421
  "pipe",
@@ -56220,7 +56479,7 @@ var podman = (options) => {
56220
56479
  if (opts.cwd)
56221
56480
  podmanArgs.push("-w", opts.cwd);
56222
56481
  podmanArgs.push(containerName, ...args2);
56223
- const proc = spawn8("podman", podmanArgs, {
56482
+ const proc = spawn9("podman", podmanArgs, {
56224
56483
  stdio: [opts.stdin, opts.stdout, opts.stderr]
56225
56484
  });
56226
56485
  proc.on("error", (error) => {
@@ -56301,7 +56560,7 @@ var init_podman = __esm(() => {
56301
56560
 
56302
56561
  // src/workflows/ticket-burner.ts
56303
56562
  import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
56304
- import { dirname as dirname9, join as join17, posix as posixPath, win32 as winPath } from "path";
56563
+ import { dirname as dirname10, join as join17, posix as posixPath, win32 as winPath } from "path";
56305
56564
  import { fileURLToPath as fileURLToPath7 } from "url";
56306
56565
  function missingImageRuntimeMessage(runtime5, image) {
56307
56566
  const binary = RUNTIME_BINARY[runtime5];
@@ -56591,13 +56850,13 @@ function parseEnvFile3(content) {
56591
56850
  if (line.length === 0 || line.startsWith("#"))
56592
56851
  continue;
56593
56852
  const body = line.startsWith("export ") ? line.slice(7).trim() : line;
56594
- const eq10 = body.indexOf("=");
56595
- if (eq10 === -1)
56853
+ const eq11 = body.indexOf("=");
56854
+ if (eq11 === -1)
56596
56855
  continue;
56597
- const key = body.slice(0, eq10).trim();
56856
+ const key = body.slice(0, eq11).trim();
56598
56857
  if (key.length === 0)
56599
56858
  continue;
56600
- let value = body.slice(eq10 + 1).trim();
56859
+ let value = body.slice(eq11 + 1).trim();
56601
56860
  if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
56602
56861
  value = value.slice(1, -1);
56603
56862
  }
@@ -56647,8 +56906,12 @@ function cacheMountFor(pm, hostPath) {
56647
56906
  }
56648
56907
  function buildBurnCacheMounts(slot, projectId, sandbox3, pm) {
56649
56908
  if (slot !== undefined) {
56909
+ const volume = burnCacheVolumeName(projectId);
56650
56910
  return {
56651
- mounts: [{ volume: burnCacheVolumeName(projectId), sandboxPath: BURN_CACHE_MOUNT }],
56911
+ mounts: [
56912
+ { volume, sandboxPath: BURN_CACHE_MOUNT },
56913
+ ...TOOLCHAIN_CACHE_SANDBOX_PATHS.map((sandboxPath) => ({ volume, sandboxPath }))
56914
+ ],
56652
56915
  env: pm ? burnCacheEnv(pm) : {}
56653
56916
  };
56654
56917
  }
@@ -57038,12 +57301,13 @@ function delayUnlessAborted(ms, signal) {
57038
57301
  signal.addEventListener("abort", onAbort, { once: true });
57039
57302
  });
57040
57303
  }
57041
- function stopTicketRun(ticketId) {
57304
+ async function stopTicketRun(ticketId) {
57042
57305
  const controller = activeTicketAborts.get(ticketId);
57043
57306
  if (!controller)
57044
- return false;
57307
+ return { stopped: false, confirmed: true };
57045
57308
  controller.abort(new Error("ticket stopped by user"));
57046
- return true;
57309
+ const { confirmed } = await killRegistry().killAndWait(ticketId);
57310
+ return { stopped: true, confirmed };
57047
57311
  }
57048
57312
  function registerTicketAbort(ticketId) {
57049
57313
  const controller = new AbortController;
@@ -57234,6 +57498,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
57234
57498
  ticketId: t.id
57235
57499
  });
57236
57500
  const outcome = await execute(ctx, t, { digests: [...digests] });
57501
+ await killRegistry().whenKillSettled(t.id);
57237
57502
  if (outcome.status === "done") {
57238
57503
  status3.set(seq2, "done");
57239
57504
  const reviewedCommit = isReviewTicket(t) ? await headSha(ctx.project.repoPath, ctx.feature.branch) : undefined;
@@ -57403,7 +57668,7 @@ function resolverTemplatePath() {
57403
57668
  return burnerAssetPath("resolve-conflict.md");
57404
57669
  }
57405
57670
  function burnerAssetPath(file) {
57406
- const here = dirname9(fileURLToPath7(import.meta.url));
57671
+ const here = dirname10(fileURLToPath7(import.meta.url));
57407
57672
  return join17(resolveSkillsRoot(here), "burner", file);
57408
57673
  }
57409
57674
  function listFeatureDocs(docsDir, prefix = "") {
@@ -57564,27 +57829,31 @@ function buildAgentExtraArgs(runtime5, options) {
57564
57829
  function quoteArg(value) {
57565
57830
  return `"${value}"`;
57566
57831
  }
57567
- function selectSandbox(config2, mounts = [], env = {}) {
57568
- const imageOpts = buildSandboxOptions(config2, mounts, env);
57832
+ function selectSandbox(config2, mounts = [], env = {}, kill = {}) {
57833
+ const imageOpts = buildSandboxOptions(config2, mounts, env, kill.containerName);
57569
57834
  switch (config2.sandbox) {
57570
57835
  case "docker":
57571
57836
  return docker(imageOpts);
57572
57837
  case "podman":
57573
57838
  return podman(imageOpts);
57574
57839
  case "noSandbox":
57575
- return noSandbox();
57840
+ return noSandbox(kill.onChildSpawn ? { onChildSpawn: kill.onChildSpawn } : {});
57576
57841
  default:
57577
57842
  throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
57578
57843
  }
57579
57844
  }
57580
- function buildSandboxOptions(config2, mounts = [], env = {}) {
57845
+ function buildSandboxOptions(config2, mounts = [], env = {}, containerName) {
57581
57846
  return {
57582
57847
  imageName: resolveSandboxImage(config2),
57583
57848
  ...mounts.length > 0 ? { mounts } : {},
57584
57849
  ...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {},
57585
- ...Object.keys(env).length > 0 ? { env } : {}
57850
+ ...Object.keys(env).length > 0 ? { env } : {},
57851
+ ...containerName !== undefined ? { containerName } : {}
57586
57852
  };
57587
57853
  }
57854
+ function burnContainerName(runId, seq2, lane) {
57855
+ return `runcastle-${runId}-t${seq2}${lane ? `-${lane}` : ""}`;
57856
+ }
57588
57857
  async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, ensureCacheVolume, blocks, runCtx) {
57589
57858
  const allocator = resolveBurnCacheMode(config2) === "volume" ? getBurnSlotAllocator(config2.burnConcurrency) : undefined;
57590
57859
  return withBurnCacheSlot(allocator, async (slot) => {
@@ -57707,6 +57976,15 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
57707
57976
  };
57708
57977
  const ticketAbort = registerTicketAbort(ticket.id);
57709
57978
  const signal = AbortSignal.any([ctx.signal, ticketAbort.signal]);
57979
+ const makeKillable = (containerName) => {
57980
+ if (config2.sandbox !== "docker")
57981
+ return;
57982
+ killRegistry().registerContainer(ticket.id, containerName, { runId: ctx.runId });
57983
+ };
57984
+ const killHandles = (containerName) => ({
57985
+ containerName,
57986
+ onChildSpawn: registerHostChildren(ticket.id, { runId: ctx.runId })
57987
+ });
57710
57988
  const maxAttempts = Math.max(1, config2.burnAttempts);
57711
57989
  const conflictResume = ticket.conflictFiles !== undefined && !!ticket.attemptBranch;
57712
57990
  let baseBranch = feature.branch;
@@ -57738,6 +58016,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
57738
58016
  };
57739
58017
  const runResolver = async (input) => {
57740
58018
  const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
58019
+ const containerName = burnContainerName(ctx.runId, ticket.seq, "resolve");
57741
58020
  const otherSide = await commitSummaries(project3.repoPath, input.branch, feature.branch);
57742
58021
  const prompt = renderTemplate(readFileSync9(resolverTemplatePath(), "utf8"), {
57743
58022
  TICKET_JSON: ticketJson,
@@ -57765,9 +58044,10 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
57765
58044
  let result;
57766
58045
  try {
57767
58046
  beginSetupSpan(resolveBranch);
58047
+ makeKillable(containerName);
57768
58048
  result = await run6({
57769
58049
  agent: buildBurnAgent(config2, token, model, agentOptions),
57770
- sandbox: selectSandbox(config2, mounts, sandboxEnv),
58050
+ sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
57771
58051
  cwd: project3.repoPath,
57772
58052
  prompt,
57773
58053
  branchStrategy: { type: "branch", branch: resolveBranch, baseBranch: input.branch },
@@ -57886,6 +58166,7 @@ ${blocked3.trim()}`
57886
58166
  }
57887
58167
  let result;
57888
58168
  let tempBranch = "";
58169
+ const containerName = burnContainerName(ctx.runId, ticket.seq);
57889
58170
  for (let attempt = 1;; attempt++) {
57890
58171
  tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
57891
58172
  const hookCommand = setupHookFor(tempBranch);
@@ -57899,7 +58180,7 @@ ${blocked3.trim()}`
57899
58180
  }
57900
58181
  const runOptions = {
57901
58182
  agent: buildBurnAgent(config2, token, model, agentOptions),
57902
- sandbox: selectSandbox(config2, mounts, sandboxEnv),
58183
+ sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
57903
58184
  cwd: project3.repoPath,
57904
58185
  prompt: retryNotes ? `${basePrompt}
57905
58186
 
@@ -57925,6 +58206,7 @@ ${retryNotes}` : basePrompt,
57925
58206
  try {
57926
58207
  await excludeAttachments();
57927
58208
  beginSetupSpan(tempBranch);
58209
+ makeKillable(containerName);
57928
58210
  result = await run6(runOptions);
57929
58211
  await clearAttachmentsFor(tempBranch);
57930
58212
  break;
@@ -58041,6 +58323,7 @@ ${blocked3.trim()}` };
58041
58323
  return await landChain(tempBranch, outcome.commits);
58042
58324
  } finally {
58043
58325
  releaseTicketAbort(ticket.id);
58326
+ killRegistry().release(ticket.id);
58044
58327
  throttle.flush();
58045
58328
  endTranscript(ticket.id);
58046
58329
  emitTicketTiming(ctx, ticket, buildTicketTiming(timer.summary(), startedAt, Date.now()));
@@ -58135,7 +58418,7 @@ function readTokenFromEnvFile(path2, runtime5) {
58135
58418
  const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
58136
58419
  return token && token.length > 0 ? token : undefined;
58137
58420
  }
58138
- 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;
58421
+ 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, TOOLCHAIN_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;
58139
58422
  var init_ticket_burner = __esm(() => {
58140
58423
  init_src();
58141
58424
  init_config_load();
@@ -58151,6 +58434,7 @@ var init_ticket_burner = __esm(() => {
58151
58434
  init_burn_guard();
58152
58435
  init_burn_cache();
58153
58436
  init_review_ticket();
58437
+ init_kill_registry();
58154
58438
  init_docker();
58155
58439
  init_podman();
58156
58440
  init_no_sandbox();
@@ -58166,6 +58450,12 @@ var init_ticket_burner = __esm(() => {
58166
58450
  yarn: "~/.cache/yarn",
58167
58451
  npm: "~/.npm"
58168
58452
  };
58453
+ TOOLCHAIN_CACHE_SANDBOX_PATHS = [
58454
+ "~/.m2",
58455
+ "~/.gradle",
58456
+ "~/.cache/pip",
58457
+ "~/.cargo/registry"
58458
+ ];
58169
58459
  RUN = String.raw`(?![\w./-])`;
58170
58460
  BASH_PATTERNS = [
58171
58461
  ["tests", new RegExp(String.raw`\b(vitest|jest|pytest|go test|cargo test|bun test|playwright|maestro)${RUN}|\b(pnpm|npm|yarn|bun|npx)\b[^&;|]*?\s(run\s+)?test${RUN}|--testPathPatterns`)],
@@ -58245,7 +58535,7 @@ var init_ticket_burner = __esm(() => {
58245
58535
 
58246
58536
  // src/workflows/research.ts
58247
58537
  import { mkdirSync as mkdirSync11, readFileSync as readFileSync10 } from "fs";
58248
- import { dirname as dirname10, join as join18 } from "path";
58538
+ import { dirname as dirname11, join as join18 } from "path";
58249
58539
  import { fileURLToPath as fileURLToPath8 } from "url";
58250
58540
  function waypointSlug(waypoint) {
58251
58541
  const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -58313,9 +58603,19 @@ function interpretResearchResult(result, docRelPath) {
58313
58603
  return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
58314
58604
  }
58315
58605
  function researchTemplatePath() {
58316
- const here = dirname10(fileURLToPath8(import.meta.url));
58606
+ const here = dirname11(fileURLToPath8(import.meta.url));
58317
58607
  return join18(resolveSkillsRoot(here), "burner", "research-waypoint.md");
58318
58608
  }
58609
+ function researchContainerName(runId, seq2) {
58610
+ return `runcastle-${runId}-w${seq2}`;
58611
+ }
58612
+ function registerResearchKill(config2, runId, seq2) {
58613
+ const containerName = researchContainerName(runId, seq2);
58614
+ if (config2.sandbox === "docker") {
58615
+ killRegistry().registerContainer(runId, containerName, { runId });
58616
+ }
58617
+ return { containerName, onChildSpawn: registerHostChildren(runId, { runId }) };
58618
+ }
58319
58619
  async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
58320
58620
  const { project: project3, feature } = ctx;
58321
58621
  const docRelPath = researchDocRel(feature.slug, waypoint);
@@ -58333,7 +58633,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
58333
58633
  const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
58334
58634
  const runOptions = {
58335
58635
  agent: buildBurnAgent(config2, token, model),
58336
- sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : []),
58636
+ sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : [], {}, registerResearchKill(config2, ctx.runId, waypoint.seq)),
58337
58637
  cwd: project3.repoPath,
58338
58638
  prompt,
58339
58639
  branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
@@ -58360,6 +58660,8 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
58360
58660
  message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${errorHeadline2(msg)})${removed ? " \u2014 cleaned up" : " \u2014 left on disk"}; landing the ${salvaged.length} commit(s) anyway`,
58361
58661
  data: { tempBranch, error: msg, cleanedUp: removed }
58362
58662
  });
58663
+ } finally {
58664
+ killRegistry().release(ctx.runId);
58363
58665
  }
58364
58666
  throttle.flush();
58365
58667
  const outcome = result ? interpretResearchResult(result, docRelPath) : { status: "done", commits: salvaged, docRelPath };
@@ -58399,6 +58701,7 @@ var init_research = __esm(() => {
58399
58701
  init_skills_root();
58400
58702
  init_dist();
58401
58703
  init_git();
58704
+ init_kill_registry();
58402
58705
  init_setup();
58403
58706
  init_ticket_burner();
58404
58707
  PLACEHOLDERS = ["WAYPOINT_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "RESEARCH_DOC_PATH"];
@@ -58423,7 +58726,7 @@ var init_registry2 = __esm(() => {
58423
58726
  });
58424
58727
 
58425
58728
  // src/workflows/runner.ts
58426
- import { eq as eq10 } from "drizzle-orm";
58729
+ import { eq as eq11 } from "drizzle-orm";
58427
58730
  function workflowClaimsFeatureBranch(workflowId) {
58428
58731
  return BRANCH_CLAIMING.has(workflowId);
58429
58732
  }
@@ -58452,7 +58755,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
58452
58755
  claim(ctx, opts.claimWaypointId, runId);
58453
58756
  } catch (e) {
58454
58757
  const summary = e instanceof Error ? e.message : "claim failed";
58455
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq10(runs.id, runId)).run();
58758
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq11(runs.id, runId)).run();
58456
58759
  throw e;
58457
58760
  }
58458
58761
  }
@@ -58502,13 +58805,15 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
58502
58805
  });
58503
58806
  return { runId, done: done10 };
58504
58807
  }
58505
- function cancelRun(runId) {
58808
+ async function cancelRun(runId) {
58506
58809
  controllers.get(runId)?.abort();
58810
+ return await killRegistry().killAllForRun(runId);
58507
58811
  }
58508
58812
  async function executeRun(ctx, runId, featureId, workflow2, runPromise2, controller, cleanup) {
58509
58813
  let status3 = "failed";
58510
58814
  let summary = "run failed";
58511
58815
  let digest;
58816
+ let threw = false;
58512
58817
  try {
58513
58818
  const result = await runPromise2;
58514
58819
  status3 = result.status;
@@ -58522,11 +58827,14 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise2, control
58522
58827
  status3 = "failed";
58523
58828
  summary = e instanceof Error ? e.message : "run failed";
58524
58829
  }
58525
- emit(ctx, featureId, { type: "run.error", message: summary, runId });
58830
+ threw = true;
58526
58831
  } finally {
58527
58832
  controllers.delete(runId);
58528
58833
  }
58529
- ctx.db.update(runs).set({ status: status3, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq10(runs.id, runId)).run();
58834
+ await killRegistry().whenRunKillsSettled(runId);
58835
+ if (threw)
58836
+ emit(ctx, featureId, { type: "run.error", message: summary, runId });
58837
+ ctx.db.update(runs).set({ status: status3, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq11(runs.id, runId)).run();
58530
58838
  releaseForSession(ctx, runId);
58531
58839
  if (workflowClaimsFeatureBranch(workflow2)) {
58532
58840
  sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status3}) while it was burning`);
@@ -58569,13 +58877,14 @@ var init_runner = __esm(() => {
58569
58877
  init_review_findings();
58570
58878
  init_tickets();
58571
58879
  init_waypoints();
58880
+ init_kill_registry();
58572
58881
  init_registry2();
58573
58882
  controllers = new Map;
58574
58883
  BRANCH_CLAIMING = new Set(["ticket-burner"]);
58575
58884
  });
58576
58885
 
58577
58886
  // src/services/features.ts
58578
- import { desc as desc5, eq as eq11 } from "drizzle-orm";
58887
+ import { desc as desc5, eq as eq12 } from "drizzle-orm";
58579
58888
  import { rmSync as rmSync4 } from "fs";
58580
58889
  async function createFeature(ctx, input) {
58581
58890
  const project3 = requireProjectById(ctx, input.projectId);
@@ -58628,7 +58937,7 @@ async function startDraft(ctx, featureId, opts = {}) {
58628
58937
  const project3 = projectForFeature(ctx, feature);
58629
58938
  const base = await requestedBase(project3, opts.baseBranch);
58630
58939
  const { branchReady, baseBranch } = await ensureFeatureBranch(project3, feature.slug, base);
58631
- ctx.db.update(features).set({ status: "active", baseBranch }).where(eq11(features.id, featureId)).run();
58940
+ ctx.db.update(features).set({ status: "active", baseBranch }).where(eq12(features.id, featureId)).run();
58632
58941
  const started = { ...feature, status: "active", baseBranch };
58633
58942
  emit(ctx, featureId, {
58634
58943
  type: "feature.started",
@@ -58777,7 +59086,7 @@ function getFeatureFull(ctx, id3) {
58777
59086
  };
58778
59087
  }
58779
59088
  function list(ctx, projectId) {
58780
- const rows = ctx.db.select().from(features).where(eq11(features.projectId, projectId)).orderBy(desc5(features.createdAt)).all();
59089
+ const rows = ctx.db.select().from(features).where(eq12(features.projectId, projectId)).orderBy(desc5(features.createdAt)).all();
58781
59090
  const lastActivity = latestTsByFeature(ctx, projectId);
58782
59091
  return rows.map((row) => {
58783
59092
  const feature = rowToFeature(row);
@@ -58824,6 +59133,13 @@ function advance(ctx, featureId) {
58824
59133
  throw new GateError("feature is already at the final phase");
58825
59134
  return setPhase(ctx, featureId, next4, "phase.advanced");
58826
59135
  }
59136
+ function assertTicketsReady(ctx, feature) {
59137
+ if (feature.ticketsReadyLap === feature.lap)
59138
+ return;
59139
+ if (activeSessionsForFeature(ctx, feature.id).length === 0)
59140
+ return;
59141
+ throw new GateError("the session is still finishing the tickets \u2014 it completes the tickets phase when the lap " + "is done, and the burn arms then");
59142
+ }
58827
59143
  async function burn(ctx, featureId, opts = {}) {
58828
59144
  const feature = getFeatureRow(ctx, featureId);
58829
59145
  requireNotDraft(feature);
@@ -58846,6 +59162,12 @@ async function burn(ctx, featureId, opts = {}) {
58846
59162
  if (lapTickets.filter((t) => t.status !== "cancelled").length < 1) {
58847
59163
  throw new GateError(lapTickets.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
58848
59164
  }
59165
+ if (feature.phase === "tickets") {
59166
+ const gate = checkGate(ctx, "tickets-approved", feature);
59167
+ if (!gate.satisfied)
59168
+ throw new GateError(gate.reason ?? "gate G3 not satisfied");
59169
+ assertTicketsReady(ctx, feature);
59170
+ }
58849
59171
  if (restarting) {
58850
59172
  sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
58851
59173
  tickets2 = listByFeature2(ctx, featureId);
@@ -58895,7 +59217,7 @@ function rethink(ctx, featureId) {
58895
59217
  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`);
58896
59218
  }
58897
59219
  const lap = feature.lap + 1;
58898
- ctx.db.update(features).set({ lap }).where(eq11(features.id, featureId)).run();
59220
+ ctx.db.update(features).set({ lap }).where(eq12(features.id, featureId)).run();
58899
59221
  return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
58900
59222
  }
58901
59223
  async function rethinkAndLaunch(ctx, featureId, launch3) {
@@ -58904,7 +59226,7 @@ async function rethinkAndLaunch(ctx, featureId, launch3) {
58904
59226
  try {
58905
59227
  return await launch3(feature);
58906
59228
  } catch (e) {
58907
- ctx.db.update(features).set({ lap: before.lap }).where(eq11(features.id, featureId)).run();
59229
+ ctx.db.update(features).set({ lap: before.lap }).where(eq12(features.id, featureId)).run();
58908
59230
  setPhase(ctx, featureId, before.phase, "lap.aborted", `lap ${feature.lap} aborted \u2014 its terminal could not be opened (${errMsg2(e)}); back at ${before.phase} on lap ${before.lap}`);
58909
59231
  throw e;
58910
59232
  }
@@ -58983,7 +59305,7 @@ function escalateToMap(ctx, featureId, input) {
58983
59305
  return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
58984
59306
  }
58985
59307
  const project3 = projectForFeature(ctx, feature);
58986
- ctx.db.update(features).set({ mapped: true }).where(eq11(features.id, featureId)).run();
59308
+ ctx.db.update(features).set({ mapped: true }).where(eq12(features.id, featureId)).run();
58987
59309
  scaffoldMapDoc(project3, { ...feature, mapped: true }, input);
58988
59310
  emit(ctx, featureId, {
58989
59311
  type: "feature.escalated",
@@ -59001,7 +59323,7 @@ function archiveFeature(ctx, featureId) {
59001
59323
  const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
59002
59324
  if (live)
59003
59325
  endSession(ctx, live.id);
59004
- ctx.db.update(features).set({ status: "archived" }).where(eq11(features.id, featureId)).run();
59326
+ ctx.db.update(features).set({ status: "archived" }).where(eq12(features.id, featureId)).run();
59005
59327
  emit(ctx, featureId, {
59006
59328
  type: "feature.archived",
59007
59329
  message: `feature ${feature.slug} archived`,
@@ -59015,7 +59337,7 @@ function unarchiveFeature(ctx, featureId) {
59015
59337
  throw new GateError(`feature ${feature.slug} is not archived`);
59016
59338
  }
59017
59339
  const restored = feature.phase === "shipped" ? "shipped" : "active";
59018
- ctx.db.update(features).set({ status: restored }).where(eq11(features.id, featureId)).run();
59340
+ ctx.db.update(features).set({ status: restored }).where(eq12(features.id, featureId)).run();
59019
59341
  emit(ctx, featureId, {
59020
59342
  type: "feature.unarchived",
59021
59343
  message: `feature ${feature.slug} unarchived (${restored})`,
@@ -59031,7 +59353,7 @@ async function deleteFeature(ctx, featureId) {
59031
59353
  const project3 = projectForFeature(ctx, feature);
59032
59354
  for (const run7 of listRunsByFeature(ctx, featureId)) {
59033
59355
  if (run7.status === "running")
59034
- cancelRun(run7.id);
59356
+ await cancelRun(run7.id);
59035
59357
  }
59036
59358
  const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
59037
59359
  if (live)
@@ -59055,13 +59377,13 @@ async function deleteFeature(ctx, featureId) {
59055
59377
  return { ok: true, slug: feature.slug };
59056
59378
  }
59057
59379
  function deleteFeatureRows(ctx, featureId) {
59058
- ctx.db.delete(tickets).where(eq11(tickets.featureId, featureId)).run();
59059
- ctx.db.delete(sessions).where(eq11(sessions.featureId, featureId)).run();
59060
- ctx.db.delete(runs).where(eq11(runs.featureId, featureId)).run();
59061
- ctx.db.delete(events).where(eq11(events.featureId, featureId)).run();
59062
- ctx.db.delete(gateOverrides).where(eq11(gateOverrides.featureId, featureId)).run();
59063
- ctx.db.delete(waypoints).where(eq11(waypoints.featureId, featureId)).run();
59064
- ctx.db.delete(features).where(eq11(features.id, featureId)).run();
59380
+ ctx.db.delete(tickets).where(eq12(tickets.featureId, featureId)).run();
59381
+ ctx.db.delete(sessions).where(eq12(sessions.featureId, featureId)).run();
59382
+ ctx.db.delete(runs).where(eq12(runs.featureId, featureId)).run();
59383
+ ctx.db.delete(events).where(eq12(events.featureId, featureId)).run();
59384
+ ctx.db.delete(gateOverrides).where(eq12(gateOverrides.featureId, featureId)).run();
59385
+ ctx.db.delete(waypoints).where(eq12(waypoints.featureId, featureId)).run();
59386
+ ctx.db.delete(features).where(eq12(features.id, featureId)).run();
59065
59387
  }
59066
59388
  function gateState(ctx, feature) {
59067
59389
  const gate = nextGate(feature);
@@ -59072,7 +59394,7 @@ function gateState(ctx, feature) {
59072
59394
  }
59073
59395
  function uniqueSlug(ctx, projectId, title) {
59074
59396
  const base = slugify(title);
59075
- const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq11(features.projectId, projectId)).all().map((r) => r.slug));
59397
+ const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq12(features.projectId, projectId)).all().map((r) => r.slug));
59076
59398
  if (!existing.has(base))
59077
59399
  return base;
59078
59400
  let n = 2;
@@ -59104,8 +59426,8 @@ var init_features = __esm(() => {
59104
59426
 
59105
59427
  // src/services/test-notes.ts
59106
59428
  import { existsSync as existsSync19, mkdirSync as mkdirSync12, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
59107
- import { dirname as dirname11 } from "path";
59108
- import { and as and9, asc as asc5, eq as eq12, lt } from "drizzle-orm";
59429
+ import { dirname as dirname12 } from "path";
59430
+ import { and as and10, asc as asc5, eq as eq13, lt } from "drizzle-orm";
59109
59431
  function rowToNote(row) {
59110
59432
  return TestNote.parse({
59111
59433
  id: row.id,
@@ -59124,7 +59446,7 @@ function rowToNote(row) {
59124
59446
  });
59125
59447
  }
59126
59448
  function getNote(ctx, id3) {
59127
- const row = ctx.db.select().from(testNotes).where(eq12(testNotes.id, id3)).get();
59449
+ const row = ctx.db.select().from(testNotes).where(eq13(testNotes.id, id3)).get();
59128
59450
  if (!row)
59129
59451
  throw new NotFoundError(`test note ${id3} not found`);
59130
59452
  return rowToNote(row);
@@ -59141,7 +59463,7 @@ function assertOpen(note2, action) {
59141
59463
  }
59142
59464
  }
59143
59465
  function listByFeature4(ctx, featureId) {
59144
- return ctx.db.select().from(testNotes).where(eq12(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
59466
+ return ctx.db.select().from(testNotes).where(eq13(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
59145
59467
  }
59146
59468
  function addNote(ctx, featureId, text3, author = "human", videoTimestamp, reviewTicketId) {
59147
59469
  const body = cleanText(text3);
@@ -59179,7 +59501,7 @@ function editNote(ctx, noteId, text3) {
59179
59501
  const current2 = getNote(ctx, noteId);
59180
59502
  assertOpen(current2, "edit");
59181
59503
  const body = cleanText(text3);
59182
- ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
59504
+ ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq13(testNotes.id, noteId)).run();
59183
59505
  emit(ctx, current2.featureId, {
59184
59506
  type: "note.edited",
59185
59507
  message: "note edited",
@@ -59191,7 +59513,7 @@ function editNote(ctx, noteId, text3) {
59191
59513
  function deleteNote(ctx, noteId) {
59192
59514
  const current2 = getNote(ctx, noteId);
59193
59515
  assertOpen(current2, "delete");
59194
- ctx.db.delete(testNotes).where(eq12(testNotes.id, noteId)).run();
59516
+ ctx.db.delete(testNotes).where(eq13(testNotes.id, noteId)).run();
59195
59517
  rmSync5(annotationPath(noteId), { force: true });
59196
59518
  emit(ctx, current2.featureId, {
59197
59519
  type: "note.deleted",
@@ -59218,7 +59540,7 @@ function toggleNote(ctx, noteId) {
59218
59540
  throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
59219
59541
  }
59220
59542
  const status3 = current2.status === "open" ? "done" : "open";
59221
- ctx.db.update(testNotes).set({ status: status3, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
59543
+ ctx.db.update(testNotes).set({ status: status3, updatedAt: Date.now() }).where(eq13(testNotes.id, noteId)).run();
59222
59544
  emit(ctx, current2.featureId, {
59223
59545
  type: "note.toggled",
59224
59546
  message: `note ${current2.status} \u2192 ${status3}`,
@@ -59266,7 +59588,7 @@ function freezeAsTickets(ctx, noteIds) {
59266
59588
  const tickets2 = storeTickets(ctx, featureId, selected.map((note2) => promotionTicket(feature, note2)));
59267
59589
  const now = Date.now();
59268
59590
  selected.forEach((note2, i) => {
59269
- ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(eq12(testNotes.id, note2.id)).run();
59591
+ ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(eq13(testNotes.id, note2.id)).run();
59270
59592
  });
59271
59593
  return { feature, notes: selected.map((n) => getNote(ctx, n.id)), tickets: tickets2 };
59272
59594
  }
@@ -59311,7 +59633,7 @@ function carryNotes(ctx, featureId, noteIds, lap) {
59311
59633
  if (note2.featureId !== featureId)
59312
59634
  throw new InvalidInputError("every carried note must belong to the feature");
59313
59635
  assertOpen(note2, "carry");
59314
- ctx.db.update(testNotes).set({ status: "carried", carriedLap: lap, updatedAt: Date.now() }).where(eq12(testNotes.id, note2.id)).run();
59636
+ ctx.db.update(testNotes).set({ status: "carried", carriedLap: lap, updatedAt: Date.now() }).where(eq13(testNotes.id, note2.id)).run();
59315
59637
  emit(ctx, featureId, { type: "note.carried", message: `note carried into lap ${lap}`, data: { noteId: note2.id, lap } });
59316
59638
  }
59317
59639
  renderTestNotes(ctx, getFeatureRow(ctx, featureId));
@@ -59321,7 +59643,7 @@ function reopenNote(ctx, noteId) {
59321
59643
  const note2 = getNote(ctx, noteId);
59322
59644
  if (note2.status !== "carried")
59323
59645
  throw new InvalidInputError("only a carried note can be reopened");
59324
- ctx.db.update(testNotes).set({ status: "open", carriedLap: null, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
59646
+ ctx.db.update(testNotes).set({ status: "open", carriedLap: null, updatedAt: Date.now() }).where(eq13(testNotes.id, noteId)).run();
59325
59647
  emit(ctx, note2.featureId, { type: "note.reopened", message: "note reopened", data: { noteId } });
59326
59648
  renderTestNotes(ctx, getFeatureRow(ctx, note2.featureId));
59327
59649
  return getNote(ctx, noteId);
@@ -59340,7 +59662,7 @@ function triageNotes(ctx, featureId, input) {
59340
59662
  }
59341
59663
  function triagePreview(ctx, featureId) {
59342
59664
  const feature = getFeatureRow(ctx, featureId);
59343
- 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();
59665
+ const standing = ctx.db.select().from(tickets).where(and10(eq13(tickets.featureId, featureId), eq13(tickets.kind, "implementation"), eq13(tickets.status, "pending"), lt(tickets.lap, feature.lap))).all();
59344
59666
  const grouped = new Map;
59345
59667
  for (const ticket of standing)
59346
59668
  grouped.set(ticket.lap, (grouped.get(ticket.lap) ?? 0) + 1);
@@ -59350,17 +59672,26 @@ function triagePreview(ctx, featureId) {
59350
59672
  standingFixTickets: [...grouped].sort(([a], [b]) => a - b).map(([lap, count3]) => ({ count: count3, lap }))
59351
59673
  };
59352
59674
  }
59675
+ function carriedLine(note2) {
59676
+ return `- [\u2192] ${note2.text} (captured lap ${note2.lap}, carried into lap ${note2.carriedLap})${screenshotSuffix(note2)}`;
59677
+ }
59353
59678
  function renderTestNotes(ctx, feature) {
59354
59679
  const notes = listByFeature4(ctx, feature.id);
59355
59680
  const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
59356
59681
  const lines = ["# Test notes"];
59682
+ const carried = notes.filter((note2) => note2.status === "carried");
59683
+ if (carried.length) {
59684
+ lines.push("", "## Carried, still open", "");
59685
+ for (const note2 of carried)
59686
+ lines.push(carriedLine(note2));
59687
+ }
59357
59688
  for (const lap of laps) {
59358
59689
  lines.push("", `## Lap ${lap}`, "");
59359
59690
  for (const note2 of notes.filter((n) => n.lap === lap))
59360
59691
  lines.push(noteLine(ctx, note2));
59361
59692
  }
59362
59693
  const path2 = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
59363
- mkdirSync12(dirname11(path2), { recursive: true });
59694
+ mkdirSync12(dirname12(path2), { recursive: true });
59364
59695
  writeFileSync8(path2, `${lines.join(`
59365
59696
  `)}
59366
59697
  `, "utf8");
@@ -59433,6 +59764,8 @@ function resolveFeatureReader(ctx, caller) {
59433
59764
  }
59434
59765
  function featureContext(ctx, reader) {
59435
59766
  const feature = getFeatureRow(ctx, reader.featureId);
59767
+ const carried = carriedWork(ctx, feature.id);
59768
+ const withheldDocs = withheldFeatureDocs({ carriedNotesOpen: carried.carriedNotes > 0 });
59436
59769
  const docs2 = [];
59437
59770
  const moreDocs = [];
59438
59771
  for (const summary of listDocs(ctx, feature)) {
@@ -59446,7 +59779,7 @@ function featureContext(ctx, reader) {
59446
59779
  docs2.push({ relPath: summary.relPath, content });
59447
59780
  continue;
59448
59781
  }
59449
- const withheld = WITHHELD_FEATURE_DOCS[summary.relPath.toLowerCase()];
59782
+ const withheld = withheldDocs[summary.relPath.toLowerCase()];
59450
59783
  moreDocs.push({
59451
59784
  relPath: summary.relPath,
59452
59785
  title: summary.title,
@@ -59462,6 +59795,7 @@ function featureContext(ctx, reader) {
59462
59795
  docs: docs2,
59463
59796
  moreDocs,
59464
59797
  docsNote: DOCS_NOTE,
59798
+ openDefects: carried.openDefects,
59465
59799
  tickets: listByFeature2(ctx, feature.id).map(stripDigest),
59466
59800
  annotatedModels: annotatedModels(ctx)
59467
59801
  };
@@ -59508,9 +59842,16 @@ function toolResolveWaypoint(ctx, session, input) {
59508
59842
  frontierIds: after3.map((w) => w.id)
59509
59843
  };
59510
59844
  }
59845
+ function refuseMisKindedReview(inputs) {
59846
+ const misKinded = inputs.find((t) => REVIEW_TITLE.test(t.title) && t.kind !== "review");
59847
+ if (!misKinded)
59848
+ return;
59849
+ throw new GateError(`ticket "${misKinded.title}" is titled like a review ticket but its kind is ` + `"${misKinded.kind ?? "implementation"}" \u2014 set kind: "review" if it is the batch-closing ` + "review (almost certainly), or retitle it if it genuinely is an implementation ticket. " + "Nothing was stored; fix it and re-emit the batch.");
59850
+ }
59511
59851
  function toolEmitTickets(ctx, session, input) {
59512
59852
  const feature = getFeatureRow(ctx, requireFeatureId(session));
59513
59853
  refuseIfReadOnly(session, "emitting tickets");
59854
+ refuseMisKindedReview(input.tickets);
59514
59855
  const stored = storeTickets(ctx, feature.id, input.tickets);
59515
59856
  return {
59516
59857
  stored: stored.length,
@@ -59572,14 +59913,26 @@ function toolCompletePhase(ctx, session, input) {
59572
59913
  message: `session marked phase '${input.phase}' complete`,
59573
59914
  data: { phase: input.phase, currentPhase: feature.phase }
59574
59915
  });
59916
+ if (input.phase === "tickets" && isPastPhase(feature, "tickets")) {
59917
+ return {
59918
+ ok: true,
59919
+ nextPhase: feature.phase,
59920
+ note: "tickets phase already crossed \u2014 the burn has started; nothing left to complete"
59921
+ };
59922
+ }
59575
59923
  const gate = nextGate(feature);
59576
59924
  if (gate?.id === "G3") {
59925
+ const result = checkGate(ctx, gate.check, feature);
59926
+ if (!result.satisfied) {
59927
+ const failed = requirement(gate);
59928
+ return {
59929
+ ok: false,
59930
+ reason: result.reason ?? `gate ${gate.id} not satisfied`,
59931
+ ...failed ? { gate: failed } : {}
59932
+ };
59933
+ }
59577
59934
  const next4 = nextPhase(feature) ?? "implementation";
59578
- emit(ctx, feature.id, {
59579
- type: "tickets.awaiting_burn",
59580
- message: "tickets complete \u2014 waiting on the human Burn click (gate G3)",
59581
- data: { phase: feature.phase, nextPhase: next4, waitingOn: "human burn" }
59582
- });
59935
+ markTicketsReady(ctx, feature.id, next4);
59583
59936
  const after3 = requirement(nextGate({ ...feature, phase: next4 }));
59584
59937
  return {
59585
59938
  ok: true,
@@ -60227,7 +60580,7 @@ function buildMcpServer(audience) {
60227
60580
  if (wants("complete_phase")) {
60228
60581
  server.registerTool("complete_phase", {
60229
60582
  title: "Complete phase",
60230
- description: "Mark the named phase complete. Runs the gate check server-side and advances the " + "feature, or returns { ok: false, reason, gate } naming what the gate wanted. On " + "success it also returns `nextGate` \u2014 what the FOLLOWING gate requires \u2014 so the next " + 'step is not a guess. The tickets \u2192 implementation gate (G3) is the human "Burn" ' + "approval: completing the tickets phase records the work done and returns " + '{ ok: true, nextPhase: "implementation", waitingOn: "human burn" } WITHOUT advancing.',
60583
+ description: "Mark the named phase complete. Runs the gate check server-side and advances the " + "feature, or returns { ok: false, reason, gate } naming what the gate wanted. On " + "success it also returns `nextGate` \u2014 what the FOLLOWING gate requires \u2014 so the next " + 'step is not a guess. The tickets \u2192 implementation gate (G3) is the human "Burn" ' + "approval: completing the tickets phase records the work done and returns " + '{ ok: true, nextPhase: "implementation", waitingOn: "human burn" } WITHOUT advancing. ' + "That call is also what ARMS the Burn click \u2014 until it lands, the button waits on you " + "rather than burning tickets you are still writing, so make it the last thing you do " + "in the tickets phase. If the human burned while you were closing out, it comes back " + "{ ok: true, note } saying the phase was already crossed: nothing left to do.",
60231
60584
  inputSchema: {
60232
60585
  phase: CompletablePhase.describe("The phase you are finishing \u2014 normally the feature\u2019s current one. `shipped` is not " + "completable by any session: a feature ships when the human merges it.")
60233
60586
  }
@@ -60254,7 +60607,7 @@ async function resolveAudience(sessionId, runId) {
60254
60607
  return;
60255
60608
  }
60256
60609
  }
60257
- var DOCS_NOTE, DRAFTING_KINDS, ADRS_NOTE, FEATURE_KINDS, FEATURE_WRITE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
60610
+ var DOCS_NOTE, REVIEW_TITLE, DRAFTING_KINDS, ADRS_NOTE, FEATURE_KINDS, FEATURE_WRITE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
60258
60611
  var init_server = __esm(() => {
60259
60612
  init_src();
60260
60613
  init_paths();
@@ -60262,8 +60615,10 @@ var init_server = __esm(() => {
60262
60615
  init_artifacts();
60263
60616
  init_sessions();
60264
60617
  init_features();
60618
+ init_carried_work();
60265
60619
  init_events();
60266
60620
  init_findings();
60621
+ init_gates();
60267
60622
  init_review_findings();
60268
60623
  init_git();
60269
60624
  init_knowledge();
@@ -60272,6 +60627,7 @@ var init_server = __esm(() => {
60272
60627
  init_tickets();
60273
60628
  init_waypoints();
60274
60629
  DOCS_NOTE = "docs[] holds this feature\u2019s canonical docs in full. moreDocs[] is everything else that " + "exists in docs/features/<slug>/ \u2014 not inlined, not gone: read any of them with " + "read_feature_doc({ relPath }). A `withheld` reason means it was left out on purpose; fetch " + "it anyway if a ticket points at it.";
60630
+ REVIEW_TITLE = /^review[: ]/i;
60275
60631
  DRAFTING_KINDS = ["ideation", "revisit", "waypoint", "converge"];
60276
60632
  ADRS_NOTE = "adrs[] is an index of the project\u2019s LIVE decisions (superseded ones are already omitted), not " + "their text. Read the ones your work touches with read_adr({ relPath }) \u2014 they bind you the " + "same either way. They are also plain files in this worktree at docs/adr/.";
60277
60633
  FEATURE_KINDS = [
@@ -60395,7 +60751,7 @@ var init_ws = __esm(() => {
60395
60751
  });
60396
60752
 
60397
60753
  // src/services/prep.ts
60398
- import { and as and10, desc as desc6, eq as eq13, inArray as inArray5 } from "drizzle-orm";
60754
+ import { and as and11, desc as desc6, eq as eq14, inArray as inArray5 } from "drizzle-orm";
60399
60755
  function keysToPrepare(ctx, project3) {
60400
60756
  return unsetPreparedKeys(project3).filter((key) => isOverwritable(ctx, project3.id, key));
60401
60757
  }
@@ -60405,10 +60761,10 @@ function isPrepared(ctx, project3) {
60405
60761
  return hasCompletedProjectSession(ctx, project3.id, "prepare");
60406
60762
  }
60407
60763
  function preparedAt(ctx, projectId) {
60408
- 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));
60764
+ const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and11(eq14(sessions.projectId, projectId), eq14(sessions.kind, "prepare"))).all().map((r) => r.id));
60409
60765
  if (prepareSessions.size === 0)
60410
60766
  return null;
60411
- 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();
60767
+ const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and11(eq14(events.projectId, projectId), inArray5(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc6(events.ts)).all();
60412
60768
  for (const end5 of ends) {
60413
60769
  const sessionId = end5.data?.sessionId;
60414
60770
  if (sessionId && prepareSessions.has(sessionId))
@@ -60936,12 +61292,12 @@ var init_web = __esm(() => {
60936
61292
 
60937
61293
  // src/version.ts
60938
61294
  import { readFileSync as readFileSync12 } from "fs";
60939
- import { dirname as dirname12, join as join21 } from "path";
61295
+ import { dirname as dirname13, join as join21 } from "path";
60940
61296
  import { fileURLToPath as fileURLToPath9 } from "url";
60941
61297
  function runcastleVersion() {
60942
61298
  if (cached4)
60943
61299
  return cached4;
60944
- const here = dirname12(fileURLToPath9(import.meta.url));
61300
+ const here = dirname13(fileURLToPath9(import.meta.url));
60945
61301
  for (const pkgPath of [join21(here, "..", "package.json"), join21(here, "package.json")]) {
60946
61302
  try {
60947
61303
  const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
@@ -60960,7 +61316,7 @@ var init_version = () => {};
60960
61316
  // src/services/install-id.ts
60961
61317
  import { randomUUID as randomUUID3 } from "crypto";
60962
61318
  import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
60963
- import { dirname as dirname13 } from "path";
61319
+ import { dirname as dirname14 } from "path";
60964
61320
  function getInstallId() {
60965
61321
  const file = installIdPath();
60966
61322
  try {
@@ -60970,7 +61326,7 @@ function getInstallId() {
60970
61326
  return existing;
60971
61327
  }
60972
61328
  const id3 = randomUUID3();
60973
- mkdirSync13(dirname13(file), { recursive: true });
61329
+ mkdirSync13(dirname14(file), { recursive: true });
60974
61330
  writeFileSync9(file, `${id3}
60975
61331
  `);
60976
61332
  return id3;
@@ -61166,7 +61522,7 @@ var init_events2 = __esm(() => {
61166
61522
  // src/launcher/launcher.ts
61167
61523
  import { existsSync as existsSync23 } from "fs";
61168
61524
  import { join as join23 } from "path";
61169
- import { and as and11, eq as eq14 } from "drizzle-orm";
61525
+ import { and as and12, eq as eq15 } from "drizzle-orm";
61170
61526
  function ptyExitMessage(exitCode) {
61171
61527
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
61172
61528
  return `terminal exited (code ${label})`;
@@ -61186,7 +61542,7 @@ function renderCommand(runtime5, spec) {
61186
61542
  return [...env, runtime5.binary, ...spec.argv].join(" ");
61187
61543
  }
61188
61544
  function activeRunFor(ctx, featureId) {
61189
- const row = ctx.db.select().from(runs).where(and11(eq14(runs.featureId, featureId), eq14(runs.status, "running"))).limit(1).get();
61545
+ const row = ctx.db.select().from(runs).where(and12(eq15(runs.featureId, featureId), eq15(runs.status, "running"))).limit(1).get();
61190
61546
  return row ? rowToRun(row) : null;
61191
61547
  }
61192
61548
  function assertSpawnable(ctx, feature, excludeSessionId) {
@@ -61267,6 +61623,7 @@ async function launchSession(ctx, input, opts = {}) {
61267
61623
  worktreePath,
61268
61624
  model
61269
61625
  });
61626
+ const carried = carriedWork(ctx, feature.id);
61270
61627
  const plan = planKickoff({
61271
61628
  kind: input.kind,
61272
61629
  lap: feature.lap,
@@ -61275,7 +61632,8 @@ async function launchSession(ctx, input, opts = {}) {
61275
61632
  lap: feature.lap,
61276
61633
  phase: feature.phase,
61277
61634
  ticketLaps: listByFeature2(ctx, feature.id).map((t2) => t2.lap)
61278
- })
61635
+ }),
61636
+ carried
61279
61637
  });
61280
61638
  let waypoint;
61281
61639
  let resumeSessionId;
@@ -61372,6 +61730,7 @@ async function launchSession(ctx, input, opts = {}) {
61372
61730
  config: ctx.config,
61373
61731
  waypoint,
61374
61732
  lap: plan.lap,
61733
+ carried: plan.lap === undefined ? undefined : carried,
61375
61734
  purpose: input.purpose,
61376
61735
  worktreePath,
61377
61736
  serverUrl: serverUrlFor(ctx.config),
@@ -61793,6 +62152,7 @@ var init_launcher = __esm(() => {
61793
62152
  init_errors();
61794
62153
  init_end_session();
61795
62154
  init_registry();
62155
+ init_carried_work();
61796
62156
  init_docs_watch();
61797
62157
  init_events();
61798
62158
  init_gates();
@@ -61959,6 +62319,7 @@ var init_feature = __esm(() => {
61959
62319
  init_src();
61960
62320
  init_launcher();
61961
62321
  init_sessions();
62322
+ init_carried_work();
61962
62323
  init_events();
61963
62324
  init_features();
61964
62325
  init_gates();
@@ -61998,7 +62359,7 @@ var init_feature = __esm(() => {
61998
62359
  rethink: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => rethinkAndLaunch(ctx, input.featureId, (feature) => launchSession(ctx, {
61999
62360
  featureId: input.featureId,
62000
62361
  kind: "revisit",
62001
- kickoffLine: lapKickoff(feature.lap)
62362
+ kickoffLine: lapKickoff(feature.lap, carriedWork(ctx, input.featureId))
62002
62363
  }))),
62003
62364
  resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
62004
62365
  endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
@@ -62059,7 +62420,7 @@ var init_feature = __esm(() => {
62059
62420
  // src/services/fsbrowse.ts
62060
62421
  import { existsSync as existsSync24, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
62061
62422
  import { homedir as homedir4 } from "os";
62062
- import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute3, join as join26, resolve as resolve12, sep as sep4 } from "path";
62423
+ import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute3, join as join26, resolve as resolve12, sep as sep4 } from "path";
62063
62424
  function expandPath(input, home = homedir4()) {
62064
62425
  const trimmed = input.trim();
62065
62426
  if (trimmed === "")
@@ -62112,7 +62473,7 @@ function crumbsFor(dir) {
62112
62473
  let cur = dir;
62113
62474
  for (;; ) {
62114
62475
  out.unshift({ name: basename3(cur) || cur, path: cur });
62115
- const parent = dirname14(cur);
62476
+ const parent = dirname15(cur);
62116
62477
  if (parent === cur)
62117
62478
  break;
62118
62479
  cur = parent;
@@ -62120,7 +62481,7 @@ function crumbsFor(dir) {
62120
62481
  return out;
62121
62482
  }
62122
62483
  function parentOf(dir) {
62123
- const parent = dirname14(dir);
62484
+ const parent = dirname15(dir);
62124
62485
  return parent === dir ? null : parent;
62125
62486
  }
62126
62487
  function browseDir(input, showHidden = false, home = homedir4()) {
@@ -62134,7 +62495,7 @@ function browseDir(input, showHidden = false, home = homedir4()) {
62134
62495
  let dir = target;
62135
62496
  try {
62136
62497
  if (!statSync8(target).isDirectory())
62137
- dir = dirname14(target);
62498
+ dir = dirname15(target);
62138
62499
  } catch (e) {
62139
62500
  throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
62140
62501
  }
@@ -62389,7 +62750,7 @@ var init_conversations = __esm(() => {
62389
62750
  // src/services/projects.ts
62390
62751
  import { existsSync as existsSync25 } from "fs";
62391
62752
  import { basename as basename4, join as join27 } from "path";
62392
- import { eq as eq15, isNull } from "drizzle-orm";
62753
+ import { eq as eq16, isNull } from "drizzle-orm";
62393
62754
  function listProjects(ctx) {
62394
62755
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
62395
62756
  }
@@ -62410,7 +62771,7 @@ async function openProject(ctx, rawPath) {
62410
62771
  const name = basename4(repoPath) || repoPath;
62411
62772
  const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
62412
62773
  if (existing) {
62413
- ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq15(projects.id, existing.id)).run();
62774
+ ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq16(projects.id, existing.id)).run();
62414
62775
  const reopened = { ...existing, repoPath, closedAt: undefined };
62415
62776
  emitProject(ctx, existing.id, {
62416
62777
  type: "project.opened",
@@ -62440,7 +62801,7 @@ function closeProject(ctx, projectId) {
62440
62801
  if (projectHasActiveRun(ctx, projectId)) {
62441
62802
  throw new InvalidInputError(`cannot close ${project3.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
62442
62803
  }
62443
- ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq15(projects.id, projectId)).run();
62804
+ ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq16(projects.id, projectId)).run();
62444
62805
  emitProject(ctx, projectId, {
62445
62806
  type: "project.closed",
62446
62807
  message: `project ${project3.name} closed`
@@ -62449,7 +62810,7 @@ function closeProject(ctx, projectId) {
62449
62810
  }
62450
62811
  function renameProject(ctx, projectId, name) {
62451
62812
  const project3 = requireProjectById(ctx, projectId);
62452
- ctx.db.update(projects).set({ name }).where(eq15(projects.id, projectId)).run();
62813
+ ctx.db.update(projects).set({ name }).where(eq16(projects.id, projectId)).run();
62453
62814
  emitProject(ctx, projectId, {
62454
62815
  type: "project.renamed",
62455
62816
  message: `project renamed ${project3.name} \u2192 ${name}`,
@@ -62541,9 +62902,9 @@ var init_review_findings2 = __esm(() => {
62541
62902
  });
62542
62903
 
62543
62904
  // src/services/runs.ts
62544
- import { and as and12, asc as asc6, eq as eq16, isNotNull as isNotNull3 } from "drizzle-orm";
62905
+ import { and as and13, asc as asc6, eq as eq17, isNotNull as isNotNull3 } from "drizzle-orm";
62545
62906
  function runTicketIds(ctx, runId) {
62546
- const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(and12(eq16(events.runId, runId), isNotNull3(events.ticketId))).orderBy(asc6(events.id)).all();
62907
+ const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(and13(eq17(events.runId, runId), isNotNull3(events.ticketId))).orderBy(asc6(events.id)).all();
62547
62908
  const seen = new Set;
62548
62909
  for (const row of rows)
62549
62910
  if (row.ticketId)
@@ -62551,7 +62912,7 @@ function runTicketIds(ctx, runId) {
62551
62912
  return [...seen];
62552
62913
  }
62553
62914
  function runLap(ctx, runId) {
62554
- const row = ctx.db.select({ lap: events.lap }).from(events).where(eq16(events.runId, runId)).orderBy(asc6(events.id)).limit(1).get();
62915
+ const row = ctx.db.select({ lap: events.lap }).from(events).where(eq17(events.runId, runId)).orderBy(asc6(events.id)).limit(1).get();
62555
62916
  return row?.lap ?? 1;
62556
62917
  }
62557
62918
  function listRunSummaries(ctx, featureId) {
@@ -62579,6 +62940,7 @@ import * as z10 from "zod";
62579
62940
  var runRouter;
62580
62941
  var init_run = __esm(() => {
62581
62942
  init_agent_stream();
62943
+ init_events();
62582
62944
  init_repo();
62583
62945
  init_runs();
62584
62946
  init_runner();
@@ -62587,10 +62949,17 @@ var init_run = __esm(() => {
62587
62949
  get: publicProcedure.input(z10.object({ runId: z10.string() })).query(({ ctx, input }) => getRunWithTickets(ctx, input.runId)),
62588
62950
  listByFeature: publicProcedure.input(z10.object({ featureId: z10.string() })).query(({ ctx, input }) => listRunSummaries(ctx, input.featureId)),
62589
62951
  agentTranscript: publicProcedure.input(z10.object({ ticketId: z10.string(), after: z10.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
62590
- cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(({ ctx, input }) => {
62591
- getRunRow(ctx, input.runId);
62592
- cancelRun(input.runId);
62593
- return { ok: true };
62952
+ cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(async ({ ctx, input }) => {
62953
+ const run7 = getRunRow(ctx, input.runId);
62954
+ const { confirmed } = await cancelRun(input.runId);
62955
+ if (!confirmed) {
62956
+ emit(ctx, run7.featureId, {
62957
+ type: "run.cancel_timeout",
62958
+ message: "cancel timed out \u2014 an agent of this run may still be running",
62959
+ runId: run7.id
62960
+ });
62961
+ }
62962
+ return { ok: true, confirmed };
62594
62963
  })
62595
62964
  });
62596
62965
  });
@@ -62608,6 +62977,31 @@ var init_settings2 = __esm(() => {
62608
62977
  });
62609
62978
  });
62610
62979
 
62980
+ // src/doctor/afk-env.ts
62981
+ import { existsSync as existsSync26, readFileSync as readFileSync15 } from "fs";
62982
+ function envWithAfkCredentials() {
62983
+ const merged = { ...process.env };
62984
+ try {
62985
+ const path2 = envPath();
62986
+ if (existsSync26(path2)) {
62987
+ const fromFile = parseEnvFile3(readFileSync15(path2, "utf8"));
62988
+ for (const runtime5 of AGENT_RUNTIMES) {
62989
+ const key = RUNTIME_SPECS[runtime5].afkKey;
62990
+ const value = fromFile[key];
62991
+ if (value && value.length > 0)
62992
+ merged[key] = value;
62993
+ }
62994
+ }
62995
+ } catch {}
62996
+ return merged;
62997
+ }
62998
+ var init_afk_env = __esm(() => {
62999
+ init_src();
63000
+ init_paths();
63001
+ init_ticket_burner();
63002
+ init_doctor();
63003
+ });
63004
+
62611
63005
  // src/trpc/routers/setup.ts
62612
63006
  import { isNotNull as isNotNull4 } from "drizzle-orm";
62613
63007
  import * as z12 from "zod";
@@ -62615,8 +63009,10 @@ var setupRouter;
62615
63009
  var init_setup2 = __esm(() => {
62616
63010
  init_src();
62617
63011
  init_schema();
63012
+ init_afk_env();
62618
63013
  init_doctor();
62619
63014
  init_system_exec();
63015
+ init_asset_paths();
62620
63016
  init_registry();
62621
63017
  init_setup();
62622
63018
  init_resolve_executable();
@@ -62626,6 +63022,8 @@ var init_setup2 = __esm(() => {
62626
63022
  const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull4(projects.model)).all().map((p) => p.model);
62627
63023
  return runDoctor({
62628
63024
  exec: createSystemExec(),
63025
+ burnerDockerfile: burnerDockerfilePath(),
63026
+ env: envWithAfkCredentials(),
62629
63027
  runtimes: configuredRuntimes(ctx.config, projectModels),
62630
63028
  ...ctx.config.sandboxImage ? { imageName: ctx.config.sandboxImage } : {}
62631
63029
  });
@@ -62739,8 +63137,17 @@ var init_test_notes2 = __esm(() => {
62739
63137
 
62740
63138
  // src/trpc/routers/ticket.ts
62741
63139
  import * as z15 from "zod";
63140
+ function noteStopTimeout(ctx, ticketId) {
63141
+ const ticket = getTicket(ctx, ticketId);
63142
+ emit(ctx, ticket.featureId, {
63143
+ type: "ticket.stop_timeout",
63144
+ message: `ticket #${ticket.seq}: stop timed out \u2014 the process may still be running`,
63145
+ ticketId: ticket.id
63146
+ });
63147
+ }
62742
63148
  var ticketRouter;
62743
63149
  var init_ticket = __esm(() => {
63150
+ init_events();
62744
63151
  init_features();
62745
63152
  init_repo();
62746
63153
  init_tickets();
@@ -62748,17 +63155,27 @@ var init_ticket = __esm(() => {
62748
63155
  init_context();
62749
63156
  ticketRouter = router({
62750
63157
  retry: publicProcedure.input(z15.object({ ticketId: z15.string(), fresh: z15.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
62751
- stop: publicProcedure.input(z15.object({ ticketId: z15.string() })).mutation(({ ctx, input }) => {
62752
- if (stopTicketRun(input.ticketId))
62753
- return { stopped: true, swept: false };
63158
+ stop: publicProcedure.input(z15.object({ ticketId: z15.string() })).mutation(async ({ ctx, input }) => {
63159
+ const { stopped, confirmed } = await stopTicketRun(input.ticketId);
63160
+ if (stopped) {
63161
+ if (!confirmed)
63162
+ noteStopTimeout(ctx, input.ticketId);
63163
+ return { stopped: true, swept: false, confirmed };
63164
+ }
62754
63165
  const ticket = getTicket(ctx, input.ticketId);
62755
63166
  if (ticket.status === "burning" && !hasActiveRun(ctx, ticket.featureId)) {
62756
63167
  sweepOrphanedBurning(ctx, ticket.featureId, "orphaned \u2014 the run that was burning it is gone");
62757
- return { stopped: false, swept: true };
63168
+ return { stopped: false, swept: true, confirmed };
62758
63169
  }
62759
- return { stopped: false, swept: false };
63170
+ return { stopped: false, swept: false, confirmed };
63171
+ }),
63172
+ cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(async ({ ctx, input }) => {
63173
+ const { stopped, confirmed } = await stopTicketRun(input.ticketId);
63174
+ if (stopped && !confirmed)
63175
+ noteStopTimeout(ctx, input.ticketId);
63176
+ const ticket = cancelTicket(ctx, input.ticketId, input.reason, { agentStopped: stopped });
63177
+ return { ticket, stopped, confirmed };
62760
63178
  }),
62761
- cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
62762
63179
  durationStats: publicProcedure.input(z15.object({ projectId: z15.string() })).query(({ ctx, input }) => ticketDurationStats(ctx, input.projectId)),
62763
63180
  edit: publicProcedure.input(z15.object({
62764
63181
  ticketId: z15.string(),
@@ -62805,14 +63222,14 @@ var init_router = __esm(() => {
62805
63222
  });
62806
63223
 
62807
63224
  // src/workflows/reconcile-runs.ts
62808
- import { eq as eq17 } from "drizzle-orm";
63225
+ import { eq as eq18 } from "drizzle-orm";
62809
63226
  async function reconcileStaleRuns(ctx) {
62810
- const stale = ctx.db.select().from(runs).where(eq17(runs.status, "running")).all().map(rowToRun);
63227
+ const stale = ctx.db.select().from(runs).where(eq18(runs.status, "running")).all().map(rowToRun);
62811
63228
  const reconciled = [];
62812
63229
  for (const run7 of stale) {
62813
63230
  if (isRunActive(run7.id))
62814
63231
  continue;
62815
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq17(runs.id, run7.id)).run();
63232
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq18(runs.id, run7.id)).run();
62816
63233
  const released = releaseForSession(ctx, run7.id);
62817
63234
  const swept = workflowClaimsFeatureBranch(run7.workflow) ? sweepOrphanedBurning(ctx, run7.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
62818
63235
  if (workflowClaimsFeatureBranch(run7.workflow)) {
@@ -63002,26 +63419,9 @@ __export(exports_cli, {
63002
63419
  resolveDoctorEnv: () => resolveDoctorEnv,
63003
63420
  parseMode: () => parseMode
63004
63421
  });
63005
- import { existsSync as existsSync26, readFileSync as readFileSync15 } from "fs";
63006
63422
  function parseMode(argv) {
63007
63423
  return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
63008
63424
  }
63009
- function envWithAfkCredentials() {
63010
- const merged = { ...process.env };
63011
- try {
63012
- const path2 = envPath();
63013
- if (existsSync26(path2)) {
63014
- const fromFile = parseEnvFile3(readFileSync15(path2, "utf8"));
63015
- for (const runtime5 of AGENT_RUNTIMES) {
63016
- const key = RUNTIME_SPECS[runtime5].afkKey;
63017
- const value = fromFile[key];
63018
- if (value && value.length > 0)
63019
- merged[key] = value;
63020
- }
63021
- }
63022
- } catch {}
63023
- return merged;
63024
- }
63025
63425
  function resolveDoctorEnv() {
63026
63426
  let imageName;
63027
63427
  let runtimes;
@@ -63038,21 +63438,22 @@ function resolveDoctorEnv() {
63038
63438
  env: envWithAfkCredentials(),
63039
63439
  platform: process.platform,
63040
63440
  cwd: process.cwd(),
63441
+ burnerDockerfile: burnerDockerfilePath(),
63041
63442
  ...imageName ? { imageName } : {},
63042
63443
  ...runtimes ? { runtimes } : {}
63043
63444
  };
63044
63445
  }
63045
- async function runCli(argv, log4 = console.log) {
63446
+ async function runCli(argv, log5 = console.log) {
63046
63447
  const mode = parseMode(argv);
63047
63448
  const report = await runDoctor(resolveDoctorEnv());
63048
- log4(formatReport(report, mode));
63449
+ log5(formatReport(report, mode));
63049
63450
  return exitCodeFor(report, mode);
63050
63451
  }
63051
63452
  var init_cli = __esm(() => {
63052
63453
  init_src();
63053
- init_paths();
63054
63454
  init_config_load();
63055
- init_ticket_burner();
63455
+ init_asset_paths();
63456
+ init_afk_env();
63056
63457
  init_doctor();
63057
63458
  init_report();
63058
63459
  init_system_exec();
@@ -63062,7 +63463,7 @@ var init_cli = __esm(() => {
63062
63463
  // src/bin/runcastle.ts
63063
63464
  init_asset_paths();
63064
63465
  init_version();
63065
- import { dirname as dirname15, resolve as resolve13 } from "path";
63466
+ import { dirname as dirname16, resolve as resolve13 } from "path";
63066
63467
  import { fileURLToPath as fileURLToPath10 } from "url";
63067
63468
  function parseCommand(argv) {
63068
63469
  const [first, ...rest] = argv;
@@ -63087,7 +63488,7 @@ Usage:
63087
63488
  runcastle --help Show this help
63088
63489
  `;
63089
63490
  async function main(argv) {
63090
- applyInstalledAssetEnv(resolve13(dirname15(fileURLToPath10(import.meta.url)), ".."));
63491
+ applyInstalledAssetEnv(resolve13(dirname16(fileURLToPath10(import.meta.url)), ".."));
63091
63492
  const { command, args: args2 } = parseCommand(argv);
63092
63493
  switch (command) {
63093
63494
  case "version":