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