runcastle 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/runcastle.js CHANGED
@@ -25,6 +25,12 @@ function newId(prefix) {
25
25
  var init_ids = () => {};
26
26
 
27
27
  // ../core/src/docs.ts
28
+ function withheldFeatureDocs(state) {
29
+ if (!state.carriedNotesOpen)
30
+ return WITHHELD_FEATURE_DOCS;
31
+ const { "test-notes.md": _testNotes, ...rest } = WITHHELD_FEATURE_DOCS;
32
+ return rest;
33
+ }
28
34
  function isAgentDigestDoc(relPath) {
29
35
  if (relPath.includes("/") || relPath.includes("\\"))
30
36
  return false;
@@ -323,7 +329,8 @@ var init_schemas = __esm(() => {
323
329
  "devCommand",
324
330
  "driveSetupCommand",
325
331
  "driveStopCommand",
326
- "dbResetCommand"
332
+ "dbResetCommand",
333
+ "driveInstructions"
327
334
  ];
328
335
  PreparedKey = z2.enum(PREPARED_KEYS);
329
336
  DRIVE_LOOP_KEYS = [
@@ -346,6 +353,7 @@ var init_schemas = __esm(() => {
346
353
  dbResetCommand: z2.string().optional(),
347
354
  driveSetupCommand: z2.string().optional(),
348
355
  driveStopCommand: z2.string().optional(),
356
+ driveInstructions: z2.string().optional(),
349
357
  closedAt: z2.number().optional()
350
358
  });
351
359
  ProjectFinding = z2.object({
@@ -367,6 +375,7 @@ var init_schemas = __esm(() => {
367
375
  brief: z2.string().optional(),
368
376
  mapped: z2.boolean(),
369
377
  lap: z2.number(),
378
+ ticketsReadyLap: z2.number().nullable(),
370
379
  phase: Phase,
371
380
  branch: z2.string(),
372
381
  baseBranch: z2.string().optional(),
@@ -551,6 +560,9 @@ function nextPhase(feature) {
551
560
  return null;
552
561
  return ORDER[i + 1];
553
562
  }
563
+ function isPastPhase(feature, phase) {
564
+ return ORDER.indexOf(feature.phase) > ORDER.indexOf(phase);
565
+ }
554
566
  function previousPhase(feature) {
555
567
  const i = ORDER.indexOf(feature.phase);
556
568
  if (i <= 0)
@@ -638,6 +650,7 @@ var init_db_schema = __esm(() => {
638
650
  verifyCommands: text("verify_commands"),
639
651
  knownFailures: text("known_failures"),
640
652
  dbResetCommand: text("db_reset_command"),
653
+ driveInstructions: text("drive_instructions"),
641
654
  driveSetupCommand: text("drive_setup_command"),
642
655
  driveStopCommand: text("drive_stop_command")
643
656
  });
@@ -660,6 +673,7 @@ var init_db_schema = __esm(() => {
660
673
  brief: text("brief"),
661
674
  mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
662
675
  lap: integer("lap").notNull().default(1),
676
+ ticketsReadyLap: integer("tickets_ready_lap"),
663
677
  phase: text("phase").notNull().$type(),
664
678
  branch: text("branch").notNull(),
665
679
  baseBranch: text("base_branch"),
@@ -2100,6 +2114,7 @@ function rowToFeature(row) {
2100
2114
  brief: row.brief ?? undefined,
2101
2115
  mapped: row.mapped,
2102
2116
  lap: row.lap,
2117
+ ticketsReadyLap: row.ticketsReadyLap,
2103
2118
  phase: row.phase,
2104
2119
  branch: row.branch,
2105
2120
  baseBranch: row.baseBranch ?? undefined,
@@ -2121,6 +2136,7 @@ function rowToProject(row) {
2121
2136
  dbResetCommand: row.dbResetCommand ?? undefined,
2122
2137
  driveSetupCommand: row.driveSetupCommand ?? undefined,
2123
2138
  driveStopCommand: row.driveStopCommand ?? undefined,
2139
+ driveInstructions: row.driveInstructions ?? undefined,
2124
2140
  closedAt: row.closedAt ?? undefined
2125
2141
  });
2126
2142
  }
@@ -2224,6 +2240,16 @@ function setPhase(ctx, featureId, toPhase, eventType = "phase.changed", message)
2224
2240
  });
2225
2241
  return { ...current, phase: toPhase };
2226
2242
  }
2243
+ function markTicketsReady(ctx, featureId, nextPhase2) {
2244
+ const current = getFeatureRow(ctx, featureId);
2245
+ ctx.db.update(features).set({ ticketsReadyLap: current.lap }).where(eq2(features.id, featureId)).run();
2246
+ emit(ctx, featureId, {
2247
+ type: "tickets.awaiting_burn",
2248
+ message: "tickets complete \u2014 waiting on the human Burn click (gate G3)",
2249
+ data: { phase: current.phase, nextPhase: nextPhase2, waitingOn: "human burn" }
2250
+ });
2251
+ return { ...current, ticketsReadyLap: current.lap };
2252
+ }
2227
2253
  function setFeatureStatus(ctx, featureId, status) {
2228
2254
  const current = getFeatureRow(ctx, featureId);
2229
2255
  ctx.db.update(features).set({ status }).where(eq2(features.id, featureId)).run();
@@ -2892,7 +2918,8 @@ var init_findings = __esm(() => {
2892
2918
  devCommand: projects.devCommand,
2893
2919
  driveSetupCommand: projects.driveSetupCommand,
2894
2920
  driveStopCommand: projects.driveStopCommand,
2895
- dbResetCommand: projects.dbResetCommand
2921
+ dbResetCommand: projects.dbResetCommand,
2922
+ driveInstructions: projects.driveInstructions
2896
2923
  };
2897
2924
  COLUMN_NAME = {
2898
2925
  setupCommand: "setupCommand",
@@ -2901,7 +2928,8 @@ var init_findings = __esm(() => {
2901
2928
  devCommand: "devCommand",
2902
2929
  driveSetupCommand: "driveSetupCommand",
2903
2930
  driveStopCommand: "driveStopCommand",
2904
- dbResetCommand: "dbResetCommand"
2931
+ dbResetCommand: "dbResetCommand",
2932
+ driveInstructions: "driveInstructions"
2905
2933
  };
2906
2934
  PREPARED_SET = new Set(PREPARED_KEYS);
2907
2935
  UNVERIFIED = { verifiedAt: null, verifiedSha: null };
@@ -4193,6 +4221,460 @@ var init_git = __esm(() => {
4193
4221
  TEMP_BRANCH_PREFIXES = [RESEARCH_BRANCH_PREFIX, TICKET_BRANCH_PREFIX];
4194
4222
  });
4195
4223
 
4224
+ // src/services/tickets.ts
4225
+ import { and as and5, asc as asc3, eq as eq5, inArray } from "drizzle-orm";
4226
+ function rowToTicket(row) {
4227
+ return Ticket.parse({
4228
+ id: row.id,
4229
+ featureId: row.featureId,
4230
+ seq: row.seq,
4231
+ title: row.title,
4232
+ goal: row.goal,
4233
+ context: row.context,
4234
+ acceptanceCriteria: row.acceptanceCriteria,
4235
+ seams: row.seams,
4236
+ blockedBy: row.blockedBy,
4237
+ kind: row.kind,
4238
+ passKind: row.passKind,
4239
+ reviewedCommit: row.reviewedCommit,
4240
+ completedAt: row.completedAt,
4241
+ model: row.model ?? undefined,
4242
+ lap: row.lap,
4243
+ status: row.status,
4244
+ commits: row.commits,
4245
+ error: row.error ?? undefined,
4246
+ attemptBranch: row.attemptBranch ?? undefined,
4247
+ conflictFiles: row.conflictFiles ?? undefined,
4248
+ digest: row.digest ?? undefined,
4249
+ originFindingId: row.originFindingId ?? undefined
4250
+ });
4251
+ }
4252
+ function resolveBlocking(inputs, startSeq) {
4253
+ try {
4254
+ return resolveBatchBlocking(inputs, { startSeq });
4255
+ } catch (e) {
4256
+ if (e instanceof BlockingEdgeError)
4257
+ throw new InvalidInputError(e.message);
4258
+ throw e;
4259
+ }
4260
+ }
4261
+ function normalizeModel(ctx, value) {
4262
+ const id = value?.trim();
4263
+ if (!id)
4264
+ return null;
4265
+ const roster = modelRoster(ctx.config);
4266
+ if (!roster.some((m) => m.id === id)) {
4267
+ throw new InvalidInputError(`unknown model "${id}" \u2014 assign one of the configured models: ${roster.map((m) => m.id).join(", ")}`);
4268
+ }
4269
+ return id;
4270
+ }
4271
+ function listByFeature2(ctx, featureId) {
4272
+ return ctx.db.select().from(tickets).where(eq5(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
4273
+ }
4274
+ function storeTickets(ctx, featureId, inputs, options = {}) {
4275
+ if (inputs.length === 0)
4276
+ return [];
4277
+ const { lap } = getFeatureRow(ctx, featureId);
4278
+ const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq5(tickets.featureId, featureId)).all();
4279
+ const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
4280
+ const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
4281
+ const rows = inputs.map((t, i) => ({
4282
+ id: newId("tkt"),
4283
+ featureId,
4284
+ seq: resolved[i].seq,
4285
+ title: t.title,
4286
+ goal: t.goal,
4287
+ context: t.context,
4288
+ acceptanceCriteria: t.acceptanceCriteria,
4289
+ seams: t.seams,
4290
+ blockedBy: resolved[i].blockedBy,
4291
+ kind: t.kind ?? "implementation",
4292
+ passKind: t.passKind ?? "review",
4293
+ reviewedCommit: null,
4294
+ completedAt: null,
4295
+ model: normalizeModel(ctx, t.model),
4296
+ lap,
4297
+ status: "pending",
4298
+ commits: [],
4299
+ error: null,
4300
+ attemptBranch: null,
4301
+ conflictFiles: null,
4302
+ digest: null,
4303
+ originFindingId: t.originFindingId ?? null
4304
+ }));
4305
+ ctx.db.insert(tickets).values(rows).run();
4306
+ emit(ctx, featureId, {
4307
+ type: "tickets.stored",
4308
+ message: `${rows.length} ticket(s) stored`,
4309
+ data: { count: rows.length, seqs: rows.map((r) => r.seq) }
4310
+ });
4311
+ return rows.map(rowToTicket);
4312
+ }
4313
+ function getTicket(ctx, id) {
4314
+ const row = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
4315
+ if (!row)
4316
+ throw new NotFoundError(`ticket ${id} not found`);
4317
+ return rowToTicket(row);
4318
+ }
4319
+ function assertMutable(t, verb) {
4320
+ if (!MUTABLE_STATUSES.has(t.status)) {
4321
+ throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
4322
+ }
4323
+ }
4324
+ function editTicket(ctx, id, patch) {
4325
+ const current = getTicket(ctx, id);
4326
+ assertMutable(current, "edit");
4327
+ const set = {};
4328
+ if (patch.title !== undefined)
4329
+ set.title = patch.title;
4330
+ if (patch.goal !== undefined)
4331
+ set.goal = patch.goal;
4332
+ if (patch.context !== undefined)
4333
+ set.context = patch.context;
4334
+ if (patch.acceptanceCriteria !== undefined)
4335
+ set.acceptanceCriteria = patch.acceptanceCriteria;
4336
+ if (patch.seams !== undefined)
4337
+ set.seams = patch.seams;
4338
+ if (patch.model !== undefined)
4339
+ set.model = normalizeModel(ctx, patch.model);
4340
+ if (Object.keys(set).length === 0) {
4341
+ throw new InvalidInputError("nothing to edit \u2014 pass at least one field");
4342
+ }
4343
+ ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
4344
+ emit(ctx, current.featureId, {
4345
+ type: "ticket.edited",
4346
+ message: `ticket ${current.seq} content edited (${Object.keys(set).join(", ")})`,
4347
+ ticketId: id,
4348
+ data: { fields: Object.keys(set) }
4349
+ });
4350
+ return getTicket(ctx, id);
4351
+ }
4352
+ function cancelTicket(ctx, id, reason, opts) {
4353
+ const current = getTicket(ctx, id);
4354
+ if (!(opts?.agentStopped && current.status === "burning"))
4355
+ assertMutable(current, "cancel");
4356
+ ctx.db.update(tickets).set({
4357
+ status: "cancelled",
4358
+ error: reason?.trim() ? reason.trim() : null,
4359
+ completedAt: Date.now()
4360
+ }).where(eq5(tickets.id, id)).run();
4361
+ emit(ctx, current.featureId, {
4362
+ type: "ticket.cancelled",
4363
+ message: `ticket ${current.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
4364
+ ticketId: id,
4365
+ data: { reason: reason ?? null }
4366
+ });
4367
+ return getTicket(ctx, id);
4368
+ }
4369
+ function sweepOrphanedBurning(ctx, featureId, reason) {
4370
+ const orphaned = ctx.db.select().from(tickets).where(and5(eq5(tickets.featureId, featureId), eq5(tickets.status, "burning"))).all().map(rowToTicket);
4371
+ const swept = [];
4372
+ for (const t of orphaned) {
4373
+ swept.push(updateTicket(ctx, t.id, { status: "failed", error: reason }));
4374
+ if (t.originFindingId)
4375
+ markFailed(ctx, t.originFindingId, reason);
4376
+ emit(ctx, featureId, {
4377
+ type: "ticket.failed",
4378
+ message: `ticket ${t.seq} failed: ${reason}`,
4379
+ ticketId: t.id,
4380
+ data: { error: reason, orphaned: true }
4381
+ });
4382
+ }
4383
+ return swept;
4384
+ }
4385
+ function updateTicket(ctx, id, patch) {
4386
+ const current = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
4387
+ if (!current)
4388
+ throw new NotFoundError(`ticket ${id} not found`);
4389
+ const set = {};
4390
+ if (patch.status !== undefined)
4391
+ set.status = patch.status;
4392
+ if (patch.status !== undefined) {
4393
+ set.completedAt = ["done", "failed", "cancelled"].includes(patch.status) ? Date.now() : null;
4394
+ }
4395
+ if (patch.commits !== undefined)
4396
+ set.commits = patch.commits;
4397
+ if (patch.error !== undefined)
4398
+ set.error = patch.error;
4399
+ if (patch.attemptBranch !== undefined)
4400
+ set.attemptBranch = patch.attemptBranch;
4401
+ if (patch.conflictFiles !== undefined)
4402
+ set.conflictFiles = patch.conflictFiles;
4403
+ if (patch.digest !== undefined)
4404
+ set.digest = patch.digest;
4405
+ if (patch.reviewedCommit !== undefined)
4406
+ set.reviewedCommit = patch.reviewedCommit;
4407
+ ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
4408
+ emit(ctx, current.featureId, {
4409
+ type: "ticket.updated",
4410
+ message: `ticket ${current.seq} \u2192 ${patch.status ?? current.status}`,
4411
+ ticketId: id,
4412
+ data: patch
4413
+ });
4414
+ const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
4415
+ return rowToTicket(updated);
4416
+ }
4417
+ function listByIds(ctx, ids2) {
4418
+ if (ids2.length === 0)
4419
+ return [];
4420
+ return ctx.db.select().from(tickets).where(inArray(tickets.id, [...ids2])).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
4421
+ }
4422
+ function timingWallMs(data) {
4423
+ if (typeof data !== "object" || data === null)
4424
+ return;
4425
+ const ms = data.wallMs;
4426
+ return typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : undefined;
4427
+ }
4428
+ function ticketDurationStats(ctx, projectId) {
4429
+ const rows = ctx.db.select({ ticketId: events.ticketId, data: events.data }).from(events).innerJoin(tickets, eq5(events.ticketId, tickets.id)).where(and5(eq5(events.projectId, projectId), eq5(events.type, "ticket.timing"), eq5(tickets.status, "done"), eq5(tickets.kind, "implementation"))).orderBy(asc3(events.id)).all();
4430
+ const perTicket = new Map;
4431
+ for (const row of rows) {
4432
+ const ms = timingWallMs(row.data);
4433
+ if (row.ticketId && ms !== undefined)
4434
+ perTicket.set(row.ticketId, ms);
4435
+ }
4436
+ const samples = [...perTicket.values()].sort((a, b) => a - b);
4437
+ if (samples.length === 0)
4438
+ return { medianMs: 0, sampleSize: 0 };
4439
+ const mid = samples.length >> 1;
4440
+ const medianMs = samples.length % 2 === 1 ? samples[mid] : Math.round((samples[mid - 1] + samples[mid]) / 2);
4441
+ return { medianMs, sampleSize: samples.length };
4442
+ }
4443
+ var MUTABLE_STATUSES;
4444
+ var init_tickets = __esm(() => {
4445
+ init_src();
4446
+ init_schema();
4447
+ init_errors();
4448
+ init_events();
4449
+ init_repo();
4450
+ init_review_findings();
4451
+ MUTABLE_STATUSES = new Set(["pending", "failed"]);
4452
+ });
4453
+
4454
+ // src/services/review-findings.ts
4455
+ import { and as and6, asc as asc4, eq as eq6, isNotNull } from "drizzle-orm";
4456
+ function rowToFinding(row) {
4457
+ return ReviewFinding.parse(row);
4458
+ }
4459
+ function getFinding(ctx, id) {
4460
+ const row = ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.id, id)).get();
4461
+ if (!row)
4462
+ throw new NotFoundError(`review finding ${id} not found`);
4463
+ return rowToFinding(row);
4464
+ }
4465
+ function listByFeature3(ctx, featureId) {
4466
+ return ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.featureId, featureId)).orderBy(asc4(reviewFindings.createdAt), asc4(reviewFindings.id)).all().map(rowToFinding);
4467
+ }
4468
+ function buildFixTicket(finding) {
4469
+ return {
4470
+ title: finding.title,
4471
+ goal: `Fix: ${finding.title}`,
4472
+ context: [
4473
+ `Location: ${finding.location}`,
4474
+ `Citation: ${finding.citation}`,
4475
+ `Detail: ${finding.detail}`,
4476
+ `Repro step: ${finding.reproStep}`
4477
+ ].join(`
4478
+
4479
+ `),
4480
+ acceptanceCriteria: [
4481
+ `The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
4482
+ ],
4483
+ seams: [],
4484
+ blockedBy: [],
4485
+ kind: "implementation",
4486
+ originFindingId: finding.id
4487
+ };
4488
+ }
4489
+ function reportFinding(ctx, args) {
4490
+ const { featureId, reviewTicket, input } = args;
4491
+ const feature = getFeatureRow(ctx, featureId);
4492
+ const id = newId("finding");
4493
+ const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and6(eq6(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull(reviewFindings.fixTicketId))).all().length;
4494
+ const verification = reviewTicket.passKind === "verification";
4495
+ const overCap = input.kind === "defect" && !verification && ticketCount >= AUTO_FIX_CAP;
4496
+ ctx.db.insert(reviewFindings).values({
4497
+ id,
4498
+ featureId,
4499
+ lap: feature.lap,
4500
+ reviewTicketId: reviewTicket.id,
4501
+ ...input,
4502
+ reproStep: input.reproStep ?? "",
4503
+ status: "open",
4504
+ openReason: verification && input.kind === "defect" ? "verification" : overCap ? "over-cap" : null,
4505
+ failureReason: null,
4506
+ fixTicketId: null,
4507
+ createdAt: Date.now()
4508
+ }).run();
4509
+ let fixTicket = null;
4510
+ if (input.kind === "defect" && !overCap && !verification) {
4511
+ const finding2 = getFinding(ctx, id);
4512
+ fixTicket = storeTickets(ctx, featureId, [
4513
+ { ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
4514
+ ], { blockedByAreGlobal: true })[0];
4515
+ ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq6(reviewFindings.id, id)).run();
4516
+ }
4517
+ const finding = getFinding(ctx, id);
4518
+ emit(ctx, featureId, {
4519
+ type: "finding.reported",
4520
+ message: `${input.kind} reported: ${input.title}`,
4521
+ ticketId: fixTicket?.id,
4522
+ data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
4523
+ });
4524
+ return { finding, fixTicket, overCap };
4525
+ }
4526
+ function updateStatus(ctx, findingId, patch) {
4527
+ const current = getFinding(ctx, findingId);
4528
+ ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
4529
+ emit(ctx, current.featureId, {
4530
+ type: "finding.updated",
4531
+ message: `finding ${findingId} marked ${patch.status}`,
4532
+ data: { findingId, status: patch.status, reason: patch.failureReason }
4533
+ });
4534
+ return getFinding(ctx, findingId);
4535
+ }
4536
+ function markFixing(ctx, findingId) {
4537
+ return updateStatus(ctx, findingId, {
4538
+ status: "fixing",
4539
+ openReason: null,
4540
+ failureReason: null
4541
+ });
4542
+ }
4543
+ function markFixed(ctx, findingId) {
4544
+ return updateStatus(ctx, findingId, {
4545
+ status: "fixed",
4546
+ openReason: null,
4547
+ failureReason: null
4548
+ });
4549
+ }
4550
+ function markFailed(ctx, findingId, reason) {
4551
+ return updateStatus(ctx, findingId, {
4552
+ status: "failed",
4553
+ openReason: "fix-failed",
4554
+ failureReason: reason ?? null
4555
+ });
4556
+ }
4557
+ function markFixProgress(ctx, findingId, progress, reason) {
4558
+ if (progress === "fixing")
4559
+ return markFixing(ctx, findingId);
4560
+ if (progress === "fixed")
4561
+ return markFixed(ctx, findingId);
4562
+ return markFailed(ctx, findingId, reason);
4563
+ }
4564
+ function dismiss(ctx, findingId) {
4565
+ return updateStatus(ctx, findingId, {
4566
+ status: "dismissed",
4567
+ openReason: null,
4568
+ failureReason: null
4569
+ });
4570
+ }
4571
+ function defectState(finding, fixTicket) {
4572
+ if (finding.status === "dismissed")
4573
+ return "dismissed";
4574
+ if (finding.status === "fixed" || fixTicket?.status === "done")
4575
+ return "fixed";
4576
+ if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
4577
+ return "fixing";
4578
+ }
4579
+ return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
4580
+ }
4581
+ function viewByFeature(ctx, featureId) {
4582
+ const findings = listByFeature3(ctx, featureId);
4583
+ const tickets2 = listByFeature2(ctx, featureId);
4584
+ const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
4585
+ const openDefects = [];
4586
+ for (const finding of findings) {
4587
+ if (finding.kind === "observation") {
4588
+ summary.observations += 1;
4589
+ continue;
4590
+ }
4591
+ summary.found += 1;
4592
+ const state = defectState(finding, fixTicketOf(finding, tickets2));
4593
+ if (state === "fixed")
4594
+ summary.fixed += 1;
4595
+ else if (state === "open") {
4596
+ summary.open += 1;
4597
+ openDefects.push(finding);
4598
+ }
4599
+ }
4600
+ return { findings, openDefects, summary };
4601
+ }
4602
+ function fixTicketOf(finding, tickets2) {
4603
+ return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
4604
+ }
4605
+ function promoteOpenDefects(ctx, featureId, findingIds) {
4606
+ const open = viewByFeature(ctx, featureId).openDefects;
4607
+ const defects = findingIds ? findingIds.map((id) => {
4608
+ const finding = open.find((candidate) => candidate.id === id);
4609
+ if (!finding)
4610
+ throw new InvalidInputError(`finding ${id} is not an open defect of this feature`);
4611
+ return finding;
4612
+ }) : open;
4613
+ if (findingIds && new Set(findingIds).size !== findingIds.length)
4614
+ throw new InvalidInputError("duplicate finding selected");
4615
+ if (defects.length === 0)
4616
+ throw new InvalidInputError("no open defects to fix");
4617
+ const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
4618
+ const findings = defects.map((defect, index2) => {
4619
+ ctx.db.update(reviewFindings).set({
4620
+ status: "fixing",
4621
+ openReason: null,
4622
+ failureReason: null,
4623
+ fixTicketId: tickets2[index2].id
4624
+ }).where(eq6(reviewFindings.id, defect.id)).run();
4625
+ return getFinding(ctx, defect.id);
4626
+ });
4627
+ emit(ctx, featureId, {
4628
+ type: "finding.fixing",
4629
+ message: `${findings.length} open defect${findings.length === 1 ? "" : "s"} promoted to fix ticket${tickets2.length === 1 ? ` ${tickets2[0].seq}` : `s ${tickets2.map((t) => t.seq).join(", ")}`}`,
4630
+ data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
4631
+ });
4632
+ return { findings, tickets: tickets2 };
4633
+ }
4634
+ var AUTO_FIX_CAP = 8;
4635
+ var init_review_findings = __esm(() => {
4636
+ init_src();
4637
+ init_schema();
4638
+ init_errors();
4639
+ init_events();
4640
+ init_repo();
4641
+ init_tickets();
4642
+ });
4643
+
4644
+ // src/services/carried-work.ts
4645
+ import { and as and7, eq as eq7 } from "drizzle-orm";
4646
+ function carriedWork(ctx, featureId) {
4647
+ const carried = ctx.db.select({ id: testNotes.id }).from(testNotes).where(and7(eq7(testNotes.featureId, featureId), eq7(testNotes.status, "carried"))).all();
4648
+ return {
4649
+ carriedNotes: carried.length,
4650
+ openDefects: viewByFeature(ctx, featureId).openDefects.map((defect) => ({
4651
+ title: defect.title,
4652
+ location: defect.location,
4653
+ detail: defect.detail,
4654
+ reproStep: defect.reproStep
4655
+ }))
4656
+ };
4657
+ }
4658
+ function hasCarriedWork(carried) {
4659
+ return carried !== undefined && (carried.carriedNotes > 0 || carried.openDefects.length > 0);
4660
+ }
4661
+ function carriedWorkSummary(carried) {
4662
+ if (!hasCarriedWork(carried))
4663
+ return;
4664
+ const parts = [];
4665
+ if (carried.carriedNotes > 0) {
4666
+ parts.push(`${carried.carriedNotes} note${carried.carriedNotes === 1 ? "" : "s"} carried`);
4667
+ }
4668
+ const defects = carried.openDefects.length;
4669
+ if (defects > 0)
4670
+ parts.push(`${defects} defect${defects === 1 ? "" : "s"} open`);
4671
+ return `${parts.join(" and ")} from earlier laps`;
4672
+ }
4673
+ var init_carried_work = __esm(() => {
4674
+ init_schema();
4675
+ init_review_findings();
4676
+ });
4677
+
4196
4678
  // src/launcher/edit-guard.ts
4197
4679
  import { isAbsolute, relative, resolve as resolve5 } from "path";
4198
4680
  function guardsEdits(kind) {
@@ -4305,15 +4787,15 @@ function noCodeRule(docs2) {
4305
4787
  function conflictResolutionRule() {
4306
4788
  return "- **This session resolves a merge conflict, so it DOES write code here.** Edit the " + "conflicted files in this checkout, resolving them from the feature docs\u2019 intent, and " + "commit the merge. That is the exception and its whole extent: the edit guard exempts " + "writes only while the merge is in progress, so work the merge revealed but did not " + "cause still rides a ticket.";
4307
4789
  }
4308
- function renderSystemPrompt(feature, kind, waypoint, lap, purpose, runtime = DEFAULT_RUNTIME) {
4790
+ function renderSystemPrompt(feature, kind, waypoint, lap, purpose, runtime = DEFAULT_RUNTIME, carried) {
4309
4791
  if (lap !== undefined)
4310
- return renderRevisitPrompt(feature, lap, purpose, runtime);
4792
+ return renderRevisitPrompt(feature, lap, purpose, runtime, carried);
4311
4793
  if (kind === "waypoint")
4312
4794
  return renderWaypointPrompt(feature, waypoint, runtime);
4313
4795
  if (kind === "converge")
4314
4796
  return renderConvergePrompt(feature, runtime);
4315
4797
  if (kind === "revisit")
4316
- return renderRevisitPrompt(feature, lap, purpose, runtime);
4798
+ return renderRevisitPrompt(feature, lap, purpose, runtime, carried);
4317
4799
  if (kind === "qa")
4318
4800
  return renderQaPrompt(feature, runtime);
4319
4801
  const docs2 = featureDocsRel(feature.slug);
@@ -4461,18 +4943,30 @@ function renderConvergePrompt(feature, runtime = DEFAULT_RUNTIME) {
4461
4943
  ].join(`
4462
4944
  `);
4463
4945
  }
4464
- function renderRevisitPrompt(feature, lap, purpose, runtime = DEFAULT_RUNTIME) {
4946
+ function renderRevisitPrompt(feature, lap, purpose, runtime = DEFAULT_RUNTIME, carried) {
4465
4947
  if (lap !== undefined && purpose === "resolve-conflict") {
4466
4948
  throw new Error(`cannot brief one revisit session as both lap ${lap} and a conflict resolution \u2014 ` + "a lap advances the pipeline and writes no code; resolving a conflict writes code " + "and advances nothing. Launch them as separate sessions.");
4467
4949
  }
4468
4950
  const docs2 = featureDocsRel(feature.slug);
4951
+ const carriedSummary = carriedWorkSummary(carried);
4469
4952
  const lapIteration = lap ? [
4470
4953
  `## This is lap ${lap}`,
4471
4954
  `You are running **lap ${lap}** of this feature (ADR-0010): the human test-drove what`,
4472
- `lap ${lap - 1} burned and came back with what it taught them. Its inputs, both`,
4473
- "OPTIONAL \u2014 a missing one is normal, not an error:",
4474
- `- \`${docs2}/test-notes.md\`, section \`## Lap ${lap - 1}\` \u2014 what the drive surfaced.`,
4475
- `- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps.`,
4955
+ `lap ${lap - 1} burned and came back with what it taught them.`,
4956
+ ...carriedSummary ? [
4957
+ `**${carriedSummary} \u2014 address them.** They are this lap's opening agenda:`,
4958
+ `- \`${docs2}/test-notes.md\`, section \`## Carried, still open\` \u2014 every note carried`,
4959
+ " and not yet done, whatever lap captured it. It exists; read it.",
4960
+ "- `get_feature_context` \u2192 `openDefects` \u2014 each open defect with its title,",
4961
+ " location, detail and repro step.",
4962
+ `- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps,`,
4963
+ " OPTIONAL; a missing one is normal, not an error."
4964
+ ] : [
4965
+ "Its inputs, both OPTIONAL \u2014 a missing one is normal, not an error:",
4966
+ `- \`${docs2}/test-notes.md\`, section \`## Carried, still open\` \u2014 every note carried`,
4967
+ " and not yet done, whatever lap captured it.",
4968
+ `- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps.`
4969
+ ],
4476
4970
  `New decisions go under a \`## Lap ${lap}\` heading in \`${docs2}/decisions.md\`.`,
4477
4971
  "",
4478
4972
  "Unlike an ordinary revisit a lap MOVES the pipeline, and only this session will:",
@@ -4814,11 +5308,12 @@ function renderRunMcpConfig(runId, config2) {
4814
5308
  };
4815
5309
  }
4816
5310
  function renderSessionPrompt(input) {
4817
- const { session, feature, waypoint, prepare, projectBrief, driveFix, lap, purpose, runtime } = input;
5311
+ const { session, feature, waypoint, prepare, projectBrief, driveFix, lap, purpose, runtime, carried } = input;
4818
5312
  if (driveFix)
4819
5313
  return renderDriveFixPrompt(driveFix);
4820
- if (feature)
4821
- return renderSystemPrompt(feature, session.kind, waypoint, lap, purpose, runtime);
5314
+ if (feature) {
5315
+ return renderSystemPrompt(feature, session.kind, waypoint, lap, purpose, runtime, carried);
5316
+ }
4822
5317
  if (prepare)
4823
5318
  return renderPreparePrompt(prepare);
4824
5319
  if (projectBrief)
@@ -4841,6 +5336,7 @@ var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES
4841
5336
  var init_artifacts = __esm(() => {
4842
5337
  init_src();
4843
5338
  init_paths();
5339
+ init_carried_work();
4844
5340
  init_asset_paths();
4845
5341
  init_edit_guard();
4846
5342
  init_skills();
@@ -5605,7 +6101,7 @@ var init_runtimes = __esm(() => {
5605
6101
 
5606
6102
  // src/launcher/sessions.ts
5607
6103
  import { statSync as statSync3 } from "fs";
5608
- import { and as and5, desc as desc3, eq as eq5, inArray, isNotNull, sql as sql2 } from "drizzle-orm";
6104
+ import { and as and8, desc as desc3, eq as eq8, inArray as inArray2, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
5609
6105
  function createSessionRow(ctx, input) {
5610
6106
  const row = ctx.db.insert(sessions).values({
5611
6107
  id: newId("sess"),
@@ -5627,11 +6123,11 @@ function createSessionRow(ctx, input) {
5627
6123
  return rowToSession(row);
5628
6124
  }
5629
6125
  function getSessionRow(ctx, id) {
5630
- const row = ctx.db.select().from(sessions).where(eq5(sessions.id, id)).get();
6126
+ const row = ctx.db.select().from(sessions).where(eq8(sessions.id, id)).get();
5631
6127
  return row ? rowToSession(row) : null;
5632
6128
  }
5633
6129
  function activeSessionsForFeature(ctx, featureId) {
5634
- return ctx.db.select().from(sessions).where(and5(eq5(sessions.featureId, featureId), inArray(sessions.status, ["launching", "live"]))).all().map(rowToSession);
6130
+ return ctx.db.select().from(sessions).where(and8(eq8(sessions.featureId, featureId), inArray2(sessions.status, ["launching", "live"]))).all().map(rowToSession);
5635
6131
  }
5636
6132
  function markAgentWorking(ctx, id) {
5637
6133
  setAwaitingInput(ctx, id, false);
@@ -5640,7 +6136,7 @@ function markAwaitingInput(ctx, id) {
5640
6136
  setAwaitingInput(ctx, id, true);
5641
6137
  }
5642
6138
  function setAwaitingInput(ctx, id, awaitingInput) {
5643
- ctx.db.update(sessions).set({ awaitingInput }).where(eq5(sessions.id, id)).run();
6139
+ ctx.db.update(sessions).set({ awaitingInput }).where(eq8(sessions.id, id)).run();
5644
6140
  }
5645
6141
  function markSessionLive(ctx, id, input = {}) {
5646
6142
  const existing = getSessionRow(ctx, id);
@@ -5650,7 +6146,7 @@ function markSessionLive(ctx, id, input = {}) {
5650
6146
  status: "live",
5651
6147
  ccSessionId: input.ccSessionId ?? existing.ccSessionId ?? null,
5652
6148
  transcriptPath: input.transcriptPath ?? existing.transcriptPath ?? null
5653
- }).where(eq5(sessions.id, id)).run();
6149
+ }).where(eq8(sessions.id, id)).run();
5654
6150
  const firstTimeLive = existing.status !== "live";
5655
6151
  if (firstTimeLive) {
5656
6152
  promoteLastSession(ctx, id);
@@ -5658,8 +6154,9 @@ function markSessionLive(ctx, id, input = {}) {
5658
6154
  }
5659
6155
  return getSessionRow(ctx, id);
5660
6156
  }
5661
- function lapKickoff(lap) {
5662
- return `Proceed with your task: invoke the /runcastle:revisit skill for LAP ${lap} REVIEW ITERATION. ` + `Call get_feature_context, then read this feature's test-notes.md (the "## Lap ${lap - 1}" ` + 'section \u2014 what the last drive surfaced) and the "## Later laps" section of its spec.md. ' + "EITHER MAY NOT EXIST YET; that is normal, not an error \u2014 say so and carry on from what I " + "tell you. Interview me about what the test drive taught: what was wrong, what was missing, " + "what I want next. Write what we settle on into decisions.md and amend spec.md for this lap " + '(pruning anything you promote out of "## Later laps"), then call emit_tickets for this ' + `lap's work. Finish in THIS session: complete_phase through ideation \u2192 spec \u2192 tickets, then ` + "tell me to review the cards and click Burn.";
6157
+ function lapKickoff(lap, carried) {
6158
+ const summary = carriedWorkSummary(carried);
6159
+ return `Proceed with your task: invoke the /runcastle:revisit skill for LAP ${lap} REVIEW ITERATION. ` + (summary ? `${summary} \u2014 address them. ` : "") + `Call get_feature_context, then read this feature's test-notes.md (its "## Carried, still ` + 'open" section \u2014 every note carried and not yet done, whatever lap captured it), the open ' + `defects in that payload's openDefects, and the "## Later laps" section of its spec.md. ` + (summary ? 'THE "## Later laps" SECTION MAY NOT EXIST; that is normal, not an error. ' : "ANY OF THOSE MAY NOT EXIST YET; that is normal, not an error \u2014 say so and carry on from " + "what I tell you. ") + "Interview me about what the test drive taught: what was wrong, what was missing, " + "what I want next. Write what we settle on into decisions.md and amend spec.md for this lap " + '(pruning anything you promote out of "## Later laps"), then call emit_tickets for this ' + `lap's work. Finish in THIS session: complete_phase through ideation \u2192 spec \u2192 tickets, then ` + "tell me to review the cards and click Burn.";
5663
6160
  }
5664
6161
  function kickoffLineFor(kind, override, runtime = DEFAULT_RUNTIME) {
5665
6162
  return override ?? runtimeAdapterFor(runtime).kickoffLine(kind);
@@ -5669,7 +6166,7 @@ function lapInFlight(input) {
5669
6166
  }
5670
6167
  function planKickoff(input) {
5671
6168
  const running = input.lapInFlight === true;
5672
- const lapBriefing = input.lap > 1 ? lapKickoff(input.lap) : undefined;
6169
+ const lapBriefing = input.lap > 1 ? lapKickoff(input.lap, input.carried) : undefined;
5673
6170
  const line = input.kickoffLine ?? (running ? lapBriefing : undefined);
5674
6171
  const lap = running ? input.lap : undefined;
5675
6172
  if (!line)
@@ -5901,31 +6398,31 @@ function markSessionEnded(ctx, id) {
5901
6398
  forgetKickoff(id);
5902
6399
  if (existing.featureId)
5903
6400
  stopDocsWatch(existing.featureId);
5904
- ctx.db.update(sessions).set({ status: "ended", ...existing.endedAt === undefined ? { endedAt: Date.now() } : {} }).where(eq5(sessions.id, id)).run();
6401
+ ctx.db.update(sessions).set({ status: "ended", ...existing.endedAt === undefined ? { endedAt: Date.now() } : {} }).where(eq8(sessions.id, id)).run();
5905
6402
  return getSessionRow(ctx, id);
5906
6403
  }
5907
6404
  function mostRecentResumableSession(ctx, featureId, kind) {
5908
- const row = ctx.db.select().from(sessions).where(and5(eq5(sessions.featureId, featureId), eq5(sessions.status, "ended"), isNotNull(sessions.ccSessionId), ...kind ? [eq5(sessions.kind, kind)] : [])).orderBy(desc3(sql2`rowid`)).limit(1).get();
6405
+ const row = ctx.db.select().from(sessions).where(and8(eq8(sessions.featureId, featureId), eq8(sessions.status, "ended"), isNotNull2(sessions.ccSessionId), ...kind ? [eq8(sessions.kind, kind)] : [])).orderBy(desc3(sql2`rowid`)).limit(1).get();
5909
6406
  return row ? rowToSession(row) : null;
5910
6407
  }
5911
6408
  function activeProjectSession(ctx, projectId, kind) {
5912
- const row = ctx.db.select().from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind), inArray(sessions.status, ["launching", "live"]))).orderBy(desc3(sql2`rowid`)).limit(1).get();
6409
+ const row = ctx.db.select().from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind), inArray2(sessions.status, ["launching", "live"]))).orderBy(desc3(sql2`rowid`)).limit(1).get();
5913
6410
  return row ? rowToSession(row) : null;
5914
6411
  }
5915
6412
  function projectSessions(ctx, projectId, kind) {
5916
- return ctx.db.select().from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind))).orderBy(desc3(sql2`rowid`)).all().map(rowToSession);
6413
+ return ctx.db.select().from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind))).orderBy(desc3(sql2`rowid`)).all().map(rowToSession);
5917
6414
  }
5918
6415
  function setSessionTitle(ctx, id, title) {
5919
- ctx.db.update(sessions).set({ title }).where(eq5(sessions.id, id)).run();
6416
+ ctx.db.update(sessions).set({ title }).where(eq8(sessions.id, id)).run();
5920
6417
  }
5921
6418
  function clearSessionTitle(ctx, id) {
5922
- ctx.db.update(sessions).set({ title: null }).where(eq5(sessions.id, id)).run();
6419
+ ctx.db.update(sessions).set({ title: null }).where(eq8(sessions.id, id)).run();
5923
6420
  }
5924
6421
  function hasCompletedProjectSession(ctx, projectId, kind) {
5925
- return ctx.db.select({ id: sessions.id }).from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind), eq5(sessions.status, "ended"))).limit(1).get() !== undefined;
6422
+ return ctx.db.select({ id: sessions.id }).from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind), eq8(sessions.status, "ended"))).limit(1).get() !== undefined;
5926
6423
  }
5927
6424
  function mostRecentResumableProjectSession(ctx, projectId, kind) {
5928
- const row = ctx.db.select().from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind), eq5(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc3(sql2`rowid`)).limit(1).get();
6425
+ const row = ctx.db.select().from(sessions).where(and8(eq8(sessions.projectId, projectId), eq8(sessions.kind, kind), eq8(sessions.status, "ended"), isNotNull2(sessions.ccSessionId))).orderBy(desc3(sql2`rowid`)).limit(1).get();
5929
6426
  return row ? rowToSession(row) : null;
5930
6427
  }
5931
6428
  function envInt(name, fallback) {
@@ -5965,17 +6462,17 @@ function transcriptBytes(session) {
5965
6462
  }
5966
6463
  function reentryCount(ctx, scope) {
5967
6464
  const where = [
5968
- eq5(sessions.status, "ended"),
5969
- isNotNull(sessions.ccSessionId),
5970
- ...scope.featureId ? [eq5(sessions.featureId, scope.featureId)] : [],
5971
- ...scope.projectId ? [eq5(sessions.projectId, scope.projectId)] : [],
5972
- ...scope.kind ? [eq5(sessions.kind, scope.kind)] : []
6465
+ eq8(sessions.status, "ended"),
6466
+ isNotNull2(sessions.ccSessionId),
6467
+ ...scope.featureId ? [eq8(sessions.featureId, scope.featureId)] : [],
6468
+ ...scope.projectId ? [eq8(sessions.projectId, scope.projectId)] : [],
6469
+ ...scope.kind ? [eq8(sessions.kind, scope.kind)] : []
5973
6470
  ];
5974
- const row = ctx.db.select({ n: sql2`count(*)` }).from(sessions).where(and5(...where)).get();
6471
+ const row = ctx.db.select({ n: sql2`count(*)` }).from(sessions).where(and8(...where)).get();
5975
6472
  return row?.n ?? 0;
5976
6473
  }
5977
6474
  function mostRecentLiveSession(ctx) {
5978
- const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
6475
+ const row = ctx.db.select().from(sessions).where(eq8(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
5979
6476
  return row ? rowToSession(row) : null;
5980
6477
  }
5981
6478
  var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, PREPARE_CONFIRM_KICKOFF, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings, LANDING_KEPT, RESUME_MAX_TRANSCRIPT_BYTES, RESUME_MAX_REENTRIES;
@@ -5986,6 +6483,7 @@ var init_sessions = __esm(() => {
5986
6483
  init_docs_watch();
5987
6484
  init_events();
5988
6485
  init_git();
6486
+ init_carried_work();
5989
6487
  init_waypoints();
5990
6488
  init_repo();
5991
6489
  init_runtimes();
@@ -6002,9 +6500,9 @@ var init_sessions = __esm(() => {
6002
6500
  });
6003
6501
 
6004
6502
  // src/launcher/reconcile.ts
6005
- import { inArray as inArray2 } from "drizzle-orm";
6503
+ import { inArray as inArray3 } from "drizzle-orm";
6006
6504
  function reconcileStaleSessions(ctx) {
6007
- const stale = ctx.db.select().from(sessions).where(inArray2(sessions.status, ["launching", "live"])).all().map(rowToSession);
6505
+ const stale = ctx.db.select().from(sessions).where(inArray3(sessions.status, ["launching", "live"])).all().map(rowToSession);
6008
6506
  const reconciled = [];
6009
6507
  for (const session of stale) {
6010
6508
  const entry = ptyRegistry().get(session.id);
@@ -6052,428 +6550,9 @@ async function getRuntimeCtx() {
6052
6550
  }
6053
6551
  var current = null;
6054
6552
 
6055
- // src/services/review-findings.ts
6056
- import { and as and6, asc as asc3, eq as eq6, isNotNull as isNotNull2 } from "drizzle-orm";
6057
- function rowToFinding(row) {
6058
- return ReviewFinding.parse(row);
6059
- }
6060
- function getFinding(ctx, id) {
6061
- const row = ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.id, id)).get();
6062
- if (!row)
6063
- throw new NotFoundError(`review finding ${id} not found`);
6064
- return rowToFinding(row);
6065
- }
6066
- function listByFeature3(ctx, featureId) {
6067
- return ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.featureId, featureId)).orderBy(asc3(reviewFindings.createdAt), asc3(reviewFindings.id)).all().map(rowToFinding);
6068
- }
6069
- function buildFixTicket(finding) {
6070
- return {
6071
- title: finding.title,
6072
- goal: `Fix: ${finding.title}`,
6073
- context: [
6074
- `Location: ${finding.location}`,
6075
- `Citation: ${finding.citation}`,
6076
- `Detail: ${finding.detail}`,
6077
- `Repro step: ${finding.reproStep}`
6078
- ].join(`
6079
-
6080
- `),
6081
- acceptanceCriteria: [
6082
- `The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
6083
- ],
6084
- seams: [],
6085
- blockedBy: [],
6086
- kind: "implementation",
6087
- originFindingId: finding.id
6088
- };
6089
- }
6090
- function reportFinding(ctx, args) {
6091
- const { featureId, reviewTicket, input } = args;
6092
- const feature = getFeatureRow(ctx, featureId);
6093
- const id = newId("finding");
6094
- const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and6(eq6(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull2(reviewFindings.fixTicketId))).all().length;
6095
- const verification = reviewTicket.passKind === "verification";
6096
- const overCap = input.kind === "defect" && !verification && ticketCount >= AUTO_FIX_CAP;
6097
- ctx.db.insert(reviewFindings).values({
6098
- id,
6099
- featureId,
6100
- lap: feature.lap,
6101
- reviewTicketId: reviewTicket.id,
6102
- ...input,
6103
- reproStep: input.reproStep ?? "",
6104
- status: "open",
6105
- openReason: verification && input.kind === "defect" ? "verification" : overCap ? "over-cap" : null,
6106
- failureReason: null,
6107
- fixTicketId: null,
6108
- createdAt: Date.now()
6109
- }).run();
6110
- let fixTicket = null;
6111
- if (input.kind === "defect" && !overCap && !verification) {
6112
- const finding2 = getFinding(ctx, id);
6113
- fixTicket = storeTickets(ctx, featureId, [
6114
- { ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
6115
- ], { blockedByAreGlobal: true })[0];
6116
- ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq6(reviewFindings.id, id)).run();
6117
- }
6118
- const finding = getFinding(ctx, id);
6119
- emit(ctx, featureId, {
6120
- type: "finding.reported",
6121
- message: `${input.kind} reported: ${input.title}`,
6122
- ticketId: fixTicket?.id,
6123
- data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
6124
- });
6125
- return { finding, fixTicket, overCap };
6126
- }
6127
- function updateStatus(ctx, findingId, patch) {
6128
- const current2 = getFinding(ctx, findingId);
6129
- ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
6130
- emit(ctx, current2.featureId, {
6131
- type: "finding.updated",
6132
- message: `finding ${findingId} marked ${patch.status}`,
6133
- data: { findingId, status: patch.status, reason: patch.failureReason }
6134
- });
6135
- return getFinding(ctx, findingId);
6136
- }
6137
- function markFixing(ctx, findingId) {
6138
- return updateStatus(ctx, findingId, {
6139
- status: "fixing",
6140
- openReason: null,
6141
- failureReason: null
6142
- });
6143
- }
6144
- function markFixed(ctx, findingId) {
6145
- return updateStatus(ctx, findingId, {
6146
- status: "fixed",
6147
- openReason: null,
6148
- failureReason: null
6149
- });
6150
- }
6151
- function markFailed(ctx, findingId, reason) {
6152
- return updateStatus(ctx, findingId, {
6153
- status: "failed",
6154
- openReason: "fix-failed",
6155
- failureReason: reason ?? null
6156
- });
6157
- }
6158
- function markFixProgress(ctx, findingId, progress, reason) {
6159
- if (progress === "fixing")
6160
- return markFixing(ctx, findingId);
6161
- if (progress === "fixed")
6162
- return markFixed(ctx, findingId);
6163
- return markFailed(ctx, findingId, reason);
6164
- }
6165
- function dismiss(ctx, findingId) {
6166
- return updateStatus(ctx, findingId, {
6167
- status: "dismissed",
6168
- openReason: null,
6169
- failureReason: null
6170
- });
6171
- }
6172
- function defectState(finding, fixTicket) {
6173
- if (finding.status === "dismissed")
6174
- return "dismissed";
6175
- if (finding.status === "fixed" || fixTicket?.status === "done")
6176
- return "fixed";
6177
- if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
6178
- return "fixing";
6179
- }
6180
- return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
6181
- }
6182
- function viewByFeature(ctx, featureId) {
6183
- const findings = listByFeature3(ctx, featureId);
6184
- const tickets2 = listByFeature2(ctx, featureId);
6185
- const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
6186
- const openDefects = [];
6187
- for (const finding of findings) {
6188
- if (finding.kind === "observation") {
6189
- summary.observations += 1;
6190
- continue;
6191
- }
6192
- summary.found += 1;
6193
- const state = defectState(finding, fixTicketOf(finding, tickets2));
6194
- if (state === "fixed")
6195
- summary.fixed += 1;
6196
- else if (state === "open") {
6197
- summary.open += 1;
6198
- openDefects.push(finding);
6199
- }
6200
- }
6201
- return { findings, openDefects, summary };
6202
- }
6203
- function fixTicketOf(finding, tickets2) {
6204
- return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
6205
- }
6206
- function promoteOpenDefects(ctx, featureId, findingIds) {
6207
- const open = viewByFeature(ctx, featureId).openDefects;
6208
- const defects = findingIds ? findingIds.map((id) => {
6209
- const finding = open.find((candidate) => candidate.id === id);
6210
- if (!finding)
6211
- throw new InvalidInputError(`finding ${id} is not an open defect of this feature`);
6212
- return finding;
6213
- }) : open;
6214
- if (findingIds && new Set(findingIds).size !== findingIds.length)
6215
- throw new InvalidInputError("duplicate finding selected");
6216
- if (defects.length === 0)
6217
- throw new InvalidInputError("no open defects to fix");
6218
- const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
6219
- const findings = defects.map((defect, index2) => {
6220
- ctx.db.update(reviewFindings).set({
6221
- status: "fixing",
6222
- openReason: null,
6223
- failureReason: null,
6224
- fixTicketId: tickets2[index2].id
6225
- }).where(eq6(reviewFindings.id, defect.id)).run();
6226
- return getFinding(ctx, defect.id);
6227
- });
6228
- emit(ctx, featureId, {
6229
- type: "finding.fixing",
6230
- 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(", ")}`}`,
6231
- data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
6232
- });
6233
- return { findings, tickets: tickets2 };
6234
- }
6235
- var AUTO_FIX_CAP = 8;
6236
- var init_review_findings = __esm(() => {
6237
- init_src();
6238
- init_schema();
6239
- init_errors();
6240
- init_events();
6241
- init_repo();
6242
- init_tickets();
6243
- });
6244
-
6245
- // src/services/tickets.ts
6246
- import { and as and7, asc as asc4, eq as eq7, inArray as inArray3 } from "drizzle-orm";
6247
- function rowToTicket(row) {
6248
- return Ticket.parse({
6249
- id: row.id,
6250
- featureId: row.featureId,
6251
- seq: row.seq,
6252
- title: row.title,
6253
- goal: row.goal,
6254
- context: row.context,
6255
- acceptanceCriteria: row.acceptanceCriteria,
6256
- seams: row.seams,
6257
- blockedBy: row.blockedBy,
6258
- kind: row.kind,
6259
- passKind: row.passKind,
6260
- reviewedCommit: row.reviewedCommit,
6261
- completedAt: row.completedAt,
6262
- model: row.model ?? undefined,
6263
- lap: row.lap,
6264
- status: row.status,
6265
- commits: row.commits,
6266
- error: row.error ?? undefined,
6267
- attemptBranch: row.attemptBranch ?? undefined,
6268
- conflictFiles: row.conflictFiles ?? undefined,
6269
- digest: row.digest ?? undefined,
6270
- originFindingId: row.originFindingId ?? undefined
6271
- });
6272
- }
6273
- function resolveBlocking(inputs, startSeq) {
6274
- try {
6275
- return resolveBatchBlocking(inputs, { startSeq });
6276
- } catch (e) {
6277
- if (e instanceof BlockingEdgeError)
6278
- throw new InvalidInputError(e.message);
6279
- throw e;
6280
- }
6281
- }
6282
- function normalizeModel(ctx, value) {
6283
- const id = value?.trim();
6284
- if (!id)
6285
- return null;
6286
- const roster = modelRoster(ctx.config);
6287
- if (!roster.some((m) => m.id === id)) {
6288
- throw new InvalidInputError(`unknown model "${id}" \u2014 assign one of the configured models: ${roster.map((m) => m.id).join(", ")}`);
6289
- }
6290
- return id;
6291
- }
6292
- function listByFeature2(ctx, featureId) {
6293
- return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc4(tickets.seq)).all().map(rowToTicket);
6294
- }
6295
- function storeTickets(ctx, featureId, inputs, options = {}) {
6296
- if (inputs.length === 0)
6297
- return [];
6298
- const { lap } = getFeatureRow(ctx, featureId);
6299
- const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
6300
- const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
6301
- const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
6302
- const rows = inputs.map((t, i) => ({
6303
- id: newId("tkt"),
6304
- featureId,
6305
- seq: resolved[i].seq,
6306
- title: t.title,
6307
- goal: t.goal,
6308
- context: t.context,
6309
- acceptanceCriteria: t.acceptanceCriteria,
6310
- seams: t.seams,
6311
- blockedBy: resolved[i].blockedBy,
6312
- kind: t.kind ?? "implementation",
6313
- passKind: t.passKind ?? "review",
6314
- reviewedCommit: null,
6315
- completedAt: null,
6316
- model: normalizeModel(ctx, t.model),
6317
- lap,
6318
- status: "pending",
6319
- commits: [],
6320
- error: null,
6321
- attemptBranch: null,
6322
- conflictFiles: null,
6323
- digest: null,
6324
- originFindingId: t.originFindingId ?? null
6325
- }));
6326
- ctx.db.insert(tickets).values(rows).run();
6327
- emit(ctx, featureId, {
6328
- type: "tickets.stored",
6329
- message: `${rows.length} ticket(s) stored`,
6330
- data: { count: rows.length, seqs: rows.map((r) => r.seq) }
6331
- });
6332
- return rows.map(rowToTicket);
6333
- }
6334
- function getTicket(ctx, id) {
6335
- const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
6336
- if (!row)
6337
- throw new NotFoundError(`ticket ${id} not found`);
6338
- return rowToTicket(row);
6339
- }
6340
- function assertMutable(t, verb) {
6341
- if (!MUTABLE_STATUSES.has(t.status)) {
6342
- 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"}`);
6343
- }
6344
- }
6345
- function editTicket(ctx, id, patch) {
6346
- const current2 = getTicket(ctx, id);
6347
- assertMutable(current2, "edit");
6348
- const set = {};
6349
- if (patch.title !== undefined)
6350
- set.title = patch.title;
6351
- if (patch.goal !== undefined)
6352
- set.goal = patch.goal;
6353
- if (patch.context !== undefined)
6354
- set.context = patch.context;
6355
- if (patch.acceptanceCriteria !== undefined)
6356
- set.acceptanceCriteria = patch.acceptanceCriteria;
6357
- if (patch.seams !== undefined)
6358
- set.seams = patch.seams;
6359
- if (patch.model !== undefined)
6360
- set.model = normalizeModel(ctx, patch.model);
6361
- if (Object.keys(set).length === 0) {
6362
- throw new InvalidInputError("nothing to edit \u2014 pass at least one field");
6363
- }
6364
- ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
6365
- emit(ctx, current2.featureId, {
6366
- type: "ticket.edited",
6367
- message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
6368
- ticketId: id,
6369
- data: { fields: Object.keys(set) }
6370
- });
6371
- return getTicket(ctx, id);
6372
- }
6373
- function cancelTicket(ctx, id, reason) {
6374
- const current2 = getTicket(ctx, id);
6375
- assertMutable(current2, "cancel");
6376
- ctx.db.update(tickets).set({
6377
- status: "cancelled",
6378
- error: reason?.trim() ? reason.trim() : null,
6379
- completedAt: Date.now()
6380
- }).where(eq7(tickets.id, id)).run();
6381
- emit(ctx, current2.featureId, {
6382
- type: "ticket.cancelled",
6383
- message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
6384
- ticketId: id,
6385
- data: { reason: reason ?? null }
6386
- });
6387
- return getTicket(ctx, id);
6388
- }
6389
- function sweepOrphanedBurning(ctx, featureId, reason) {
6390
- const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
6391
- const swept = [];
6392
- for (const t of orphaned) {
6393
- swept.push(updateTicket(ctx, t.id, { status: "failed", error: reason }));
6394
- if (t.originFindingId)
6395
- markFailed(ctx, t.originFindingId, reason);
6396
- emit(ctx, featureId, {
6397
- type: "ticket.failed",
6398
- message: `ticket ${t.seq} failed: ${reason}`,
6399
- ticketId: t.id,
6400
- data: { error: reason, orphaned: true }
6401
- });
6402
- }
6403
- return swept;
6404
- }
6405
- function updateTicket(ctx, id, patch) {
6406
- const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
6407
- if (!current2)
6408
- throw new NotFoundError(`ticket ${id} not found`);
6409
- const set = {};
6410
- if (patch.status !== undefined)
6411
- set.status = patch.status;
6412
- if (patch.status !== undefined) {
6413
- set.completedAt = ["done", "failed", "cancelled"].includes(patch.status) ? Date.now() : null;
6414
- }
6415
- if (patch.commits !== undefined)
6416
- set.commits = patch.commits;
6417
- if (patch.error !== undefined)
6418
- set.error = patch.error;
6419
- if (patch.attemptBranch !== undefined)
6420
- set.attemptBranch = patch.attemptBranch;
6421
- if (patch.conflictFiles !== undefined)
6422
- set.conflictFiles = patch.conflictFiles;
6423
- if (patch.digest !== undefined)
6424
- set.digest = patch.digest;
6425
- if (patch.reviewedCommit !== undefined)
6426
- set.reviewedCommit = patch.reviewedCommit;
6427
- ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
6428
- emit(ctx, current2.featureId, {
6429
- type: "ticket.updated",
6430
- message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
6431
- ticketId: id,
6432
- data: patch
6433
- });
6434
- const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
6435
- return rowToTicket(updated);
6436
- }
6437
- function listByIds(ctx, ids2) {
6438
- if (ids2.length === 0)
6439
- return [];
6440
- return ctx.db.select().from(tickets).where(inArray3(tickets.id, [...ids2])).orderBy(asc4(tickets.seq)).all().map(rowToTicket);
6441
- }
6442
- function timingWallMs(data) {
6443
- if (typeof data !== "object" || data === null)
6444
- return;
6445
- const ms = data.wallMs;
6446
- return typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : undefined;
6447
- }
6448
- function ticketDurationStats(ctx, projectId) {
6449
- 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();
6450
- const perTicket = new Map;
6451
- for (const row of rows) {
6452
- const ms = timingWallMs(row.data);
6453
- if (row.ticketId && ms !== undefined)
6454
- perTicket.set(row.ticketId, ms);
6455
- }
6456
- const samples = [...perTicket.values()].sort((a, b) => a - b);
6457
- if (samples.length === 0)
6458
- return { medianMs: 0, sampleSize: 0 };
6459
- const mid = samples.length >> 1;
6460
- const medianMs = samples.length % 2 === 1 ? samples[mid] : Math.round((samples[mid - 1] + samples[mid]) / 2);
6461
- return { medianMs, sampleSize: samples.length };
6462
- }
6463
- var MUTABLE_STATUSES;
6464
- var init_tickets = __esm(() => {
6465
- init_src();
6466
- init_schema();
6467
- init_errors();
6468
- init_events();
6469
- init_repo();
6470
- init_review_findings();
6471
- MUTABLE_STATUSES = new Set(["pending", "failed"]);
6472
- });
6473
-
6474
6553
  // src/services/gates.ts
6475
6554
  import { existsSync as existsSync11 } from "fs";
6476
- import { and as and8, desc as desc4, eq as eq8, inArray as inArray4 } from "drizzle-orm";
6555
+ import { and as and9, desc as desc4, eq as eq9, inArray as inArray4 } from "drizzle-orm";
6477
6556
  function checkGate(ctx, check, feature) {
6478
6557
  switch (check) {
6479
6558
  case "decisions-file-exists":
@@ -6499,8 +6578,16 @@ function checkGate(ctx, check, feature) {
6499
6578
  case "spec-file-exists":
6500
6579
  return docGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
6501
6580
  case "tickets-approved": {
6502
- const count = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled").length;
6503
- return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
6581
+ const burnable = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled");
6582
+ if (burnable.length === 0)
6583
+ return { satisfied: false, reason: "no tickets to burn" };
6584
+ if (!burnable.some((t) => t.kind === "review")) {
6585
+ return {
6586
+ satisfied: false,
6587
+ reason: 'no review ticket on this lap \u2014 every batch closes with one (kind: "review"); emit it, ' + "fix the kind on a review-shaped ticket, or override this gate with a reason"
6588
+ };
6589
+ }
6590
+ return { satisfied: true };
6504
6591
  }
6505
6592
  case "all-tickets-terminal": {
6506
6593
  const tickets2 = listByFeature2(ctx, feature.id);
@@ -6532,7 +6619,7 @@ function fileGate(ctx, feature, fileName, reason) {
6532
6619
  return existsSync11(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
6533
6620
  }
6534
6621
  function lapWorked(ctx, feature) {
6535
- return ctx.db.select({ id: sessions.id }).from(sessions).where(and8(eq8(sessions.featureId, feature.id), eq8(sessions.lap, feature.lap), inArray4(sessions.kind, [...DOC_WRITING_KINDS]))).all().length > 0;
6622
+ return ctx.db.select({ id: sessions.id }).from(sessions).where(and9(eq9(sessions.featureId, feature.id), eq9(sessions.lap, feature.lap), inArray4(sessions.kind, [...DOC_WRITING_KINDS]))).all().length > 0;
6536
6623
  }
6537
6624
  function docGate(ctx, feature, fileName, reason) {
6538
6625
  const file = fileGate(ctx, feature, fileName, reason);
@@ -6564,10 +6651,10 @@ function overrideGate(ctx, featureId, gate, reason) {
6564
6651
  return feature;
6565
6652
  }
6566
6653
  function undoLastGateOverride(ctx, featureId, gate) {
6567
- const last = ctx.db.select({ id: gateOverrides.id }).from(gateOverrides).where(and8(eq8(gateOverrides.featureId, featureId), eq8(gateOverrides.gate, gate))).orderBy(desc4(gateOverrides.id)).get();
6654
+ const last = ctx.db.select({ id: gateOverrides.id }).from(gateOverrides).where(and9(eq9(gateOverrides.featureId, featureId), eq9(gateOverrides.gate, gate))).orderBy(desc4(gateOverrides.id)).get();
6568
6655
  if (!last)
6569
6656
  return false;
6570
- ctx.db.delete(gateOverrides).where(eq8(gateOverrides.id, last.id)).run();
6657
+ ctx.db.delete(gateOverrides).where(eq9(gateOverrides.id, last.id)).run();
6571
6658
  return true;
6572
6659
  }
6573
6660
  function undoGateOverride(ctx, featureId, gate) {
@@ -6761,6 +6848,133 @@ var init_end_session = __esm(() => {
6761
6848
  init_registry();
6762
6849
  });
6763
6850
 
6851
+ // src/workflows/kill-registry.ts
6852
+ import { spawn as spawn4 } from "child_process";
6853
+ function log2(msg) {
6854
+ console.error(`[kill-registry] ${msg}`);
6855
+ }
6856
+ function runDockerCommand(args) {
6857
+ return new Promise((resolve9) => {
6858
+ let settled = false;
6859
+ const settle = (ok) => {
6860
+ if (settled)
6861
+ return;
6862
+ settled = true;
6863
+ clearTimeout(timer);
6864
+ resolve9(ok);
6865
+ };
6866
+ const timer = setTimeout(() => settle(false), DOCKER_COMMAND_TIMEOUT_MS);
6867
+ timer.unref?.();
6868
+ try {
6869
+ const child = spawn4("docker", args, { windowsHide: true, stdio: "ignore" });
6870
+ child.on("exit", (code) => settle(code === 0));
6871
+ child.on("close", (code) => settle(code === 0));
6872
+ child.on("error", () => settle(false));
6873
+ } catch {
6874
+ settle(false);
6875
+ }
6876
+ });
6877
+ }
6878
+ function withDeadline2(body, ms) {
6879
+ return new Promise((resolve9) => {
6880
+ const timer = setTimeout(() => resolve9(false), ms);
6881
+ body.catch(() => false).then((confirmed) => {
6882
+ clearTimeout(timer);
6883
+ resolve9(confirmed);
6884
+ });
6885
+ });
6886
+ }
6887
+ function forget() {}
6888
+ function delay(ms) {
6889
+ return new Promise((resolve9) => {
6890
+ const timer = setTimeout(resolve9, ms);
6891
+ timer.unref?.();
6892
+ });
6893
+ }
6894
+
6895
+ class KillRegistry {
6896
+ deps;
6897
+ handles = new Map;
6898
+ killsInFlight = new Map;
6899
+ constructor(deps = REAL_DEPS) {
6900
+ this.deps = deps;
6901
+ }
6902
+ registerContainer(laneKey, containerName, owner = {}) {
6903
+ this.handles.set(laneKey, { kind: "container", containerName, ...owner });
6904
+ }
6905
+ registerHostPid(laneKey, pid, owner = {}) {
6906
+ this.handles.set(laneKey, { kind: "host", pid, ...owner });
6907
+ }
6908
+ release(laneKey) {
6909
+ this.handles.delete(laneKey);
6910
+ }
6911
+ killAndWait(laneKey, opts) {
6912
+ const handle = this.handles.get(laneKey);
6913
+ if (!handle)
6914
+ return Promise.resolve({ confirmed: true });
6915
+ const kill = this.killLane(laneKey, handle, opts);
6916
+ const entry = { runId: handle.runId, settled: kill.then(forget, forget) };
6917
+ this.killsInFlight.set(laneKey, entry);
6918
+ entry.settled.then(() => {
6919
+ if (this.killsInFlight.get(laneKey) === entry)
6920
+ this.killsInFlight.delete(laneKey);
6921
+ });
6922
+ return kill;
6923
+ }
6924
+ async whenKillSettled(laneKey) {
6925
+ await this.killsInFlight.get(laneKey)?.settled;
6926
+ }
6927
+ async whenRunKillsSettled(runId) {
6928
+ await Promise.all([...this.killsInFlight.values()].filter((kill) => kill.runId === runId).map((kill) => kill.settled));
6929
+ }
6930
+ async killLane(laneKey, handle, opts) {
6931
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6932
+ const started = Date.now();
6933
+ const confirmed = await withDeadline2(this.kill(handle, started + timeoutMs), timeoutMs);
6934
+ if (confirmed)
6935
+ this.handles.delete(laneKey);
6936
+ const target = handle.kind === "container" ? handle.containerName : `pid=${handle.pid}`;
6937
+ log2(`killAndWait: lane=${laneKey} ${handle.kind}=${target} ${confirmed ? "confirmed dead" : `NOT confirmed dead (${timeoutMs}ms deadline)`} after ${Date.now() - started}ms`);
6938
+ return { confirmed };
6939
+ }
6940
+ async killAllForRun(runId, opts) {
6941
+ const lanes = [...this.handles].filter(([, handle]) => handle.runId === runId).map(([laneKey]) => laneKey);
6942
+ const outcomes = await Promise.all(lanes.map((laneKey) => this.killAndWait(laneKey, opts)));
6943
+ return { confirmed: outcomes.every((outcome) => outcome.confirmed) };
6944
+ }
6945
+ keys() {
6946
+ return [...this.handles.keys()];
6947
+ }
6948
+ async kill(handle, deadline) {
6949
+ if (handle.kind === "host") {
6950
+ await this.deps.killTree(handle.pid);
6951
+ return true;
6952
+ }
6953
+ await this.deps.runDocker(["rm", "-f", handle.containerName]);
6954
+ do {
6955
+ if (!await this.deps.runDocker(["inspect", handle.containerName]))
6956
+ return true;
6957
+ await delay(DEATH_POLL_INTERVAL_MS);
6958
+ } while (Date.now() < deadline);
6959
+ return false;
6960
+ }
6961
+ }
6962
+ function killRegistry() {
6963
+ const g = globalThis;
6964
+ if (!g[REGISTRY_KEY2])
6965
+ g[REGISTRY_KEY2] = new KillRegistry;
6966
+ return g[REGISTRY_KEY2];
6967
+ }
6968
+ function registerHostChildren(laneKey, owner) {
6969
+ return (pid) => killRegistry().registerHostPid(laneKey, pid, owner);
6970
+ }
6971
+ var DEFAULT_TIMEOUT_MS = 1e4, DEATH_POLL_INTERVAL_MS = 100, DOCKER_COMMAND_TIMEOUT_MS = 5000, REAL_DEPS, REGISTRY_KEY2;
6972
+ var init_kill_registry = __esm(() => {
6973
+ init_kill_tree();
6974
+ REAL_DEPS = { runDocker: runDockerCommand, killTree: killProcessTree };
6975
+ REGISTRY_KEY2 = Symbol.for("runcastle.kill.registry");
6976
+ });
6977
+
6764
6978
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-NGBM7T3E.js
6765
6979
  import { createRequire as createRequire3 } from "module";
6766
6980
  var require$1, __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getProtoOf, __hasOwnProp, __require2, __commonJS = (cb, mod) => function __require22() {
@@ -10286,7 +10500,7 @@ ${prefix}}`;
10286
10500
  }, _catch, catchAllDefect, catchSomeCause, catchSomeDefect, catchTag, catchTags, cause = (self) => matchCause(self, {
10287
10501
  onFailure: identity,
10288
10502
  onSuccess: () => empty7
10289
- }), clockWith3, clock, delay, descriptorWith = (f) => withFiberRuntime((state, status3) => f({
10503
+ }), clockWith3, clock, delay2, descriptorWith = (f) => withFiberRuntime((state, status3) => f({
10290
10504
  id: state.id(),
10291
10505
  status: status3,
10292
10506
  interruptors: interruptors(state.getFiberRef(currentInterruptedCause))
@@ -10357,7 +10571,7 @@ ${prefix}}`;
10357
10571
  fiberState.log(message, cause3, levelOption);
10358
10572
  return void_2;
10359
10573
  });
10360
- }, log3, logTrace, logDebug, logInfo, logWarning, logError, logFatal, withLogSpan, logAnnotations, loop = (initial, options) => options.discard ? loopDiscard(initial, options.while, options.step, options.body) : map10(loopInternal(initial, options.while, options.step, options.body), fromIterable2), loopInternal = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), (a) => map10(loopInternal(inc(initial), cont, inc, body), prepend3(a))) : sync(() => empty11())), loopDiscard = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), () => loopDiscard(inc(initial), cont, inc, body)) : void_2), mapAccum2, mapErrorCause, memoize = (self) => pipe(deferredMake(), flatMap7((deferred) => pipe(diffFiberRefsAndRuntimeFlags(self), intoDeferred(deferred), once, map10((complete4) => zipRight2(complete4, pipe(deferredAwait(deferred), flatMap7(([patch9, a]) => as2(zip2(patchFiberRefs(patch9[0]), updateRuntimeFlags(patch9[1])), a)))))))), merge5 = (self) => matchEffect(self, {
10574
+ }, log4, logTrace, logDebug, logInfo, logWarning, logError, logFatal, withLogSpan, logAnnotations, loop = (initial, options) => options.discard ? loopDiscard(initial, options.while, options.step, options.body) : map10(loopInternal(initial, options.while, options.step, options.body), fromIterable2), loopInternal = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), (a) => map10(loopInternal(inc(initial), cont, inc, body), prepend3(a))) : sync(() => empty11())), loopDiscard = (initial, cont, inc, body) => suspend(() => cont(initial) ? flatMap7(body(initial), () => loopDiscard(inc(initial), cont, inc, body)) : void_2), mapAccum2, mapErrorCause, memoize = (self) => pipe(deferredMake(), flatMap7((deferred) => pipe(diffFiberRefsAndRuntimeFlags(self), intoDeferred(deferred), once, map10((complete4) => zipRight2(complete4, pipe(deferredAwait(deferred), flatMap7(([patch9, a]) => as2(zip2(patchFiberRefs(patch9[0]), updateRuntimeFlags(patch9[1])), a)))))))), merge5 = (self) => matchEffect(self, {
10361
10575
  onFailure: (e) => succeed(e),
10362
10576
  onSuccess: succeed
10363
10577
  }), negate = (self) => map10(self, (b) => !b), none6 = (self) => flatMap7(self, (option5) => {
@@ -11971,11 +12185,11 @@ ${prefix}}`;
11971
12185
  return end5 - time2 <= 0 ? some2(computeCachedValue(self, timeToLive, time2)) : none2();
11972
12186
  }
11973
12187
  }
11974
- })), flatMap7((option5) => isNone2(option5) ? dieMessage("BUG: Effect.cachedInvalidate - please report an issue at https://github.com/Effect-TS/effect/issues") : restore(deferredAwait(option5.value[1]))))), invalidateCache = (cache) => set4(cache, none2()), ensuringChild, ensuringChildren, forkAll, forkIn, forkScoped = (self) => scopeWith((scope5) => forkIn(self, scope5)), fromFiber = (fiber) => join22(fiber), fromFiberEffect = (fiber) => suspend(() => flatMap7(fiber, join22)), memoKeySymbol, Key, cachedFunction = (f, eq9) => {
12188
+ })), flatMap7((option5) => isNone2(option5) ? dieMessage("BUG: Effect.cachedInvalidate - please report an issue at https://github.com/Effect-TS/effect/issues") : restore(deferredAwait(option5.value[1]))))), invalidateCache = (cache) => set4(cache, none2()), ensuringChild, ensuringChildren, forkAll, forkIn, forkScoped = (self) => scopeWith((scope5) => forkIn(self, scope5)), fromFiber = (fiber) => join22(fiber), fromFiberEffect = (fiber) => suspend(() => flatMap7(fiber, join22)), memoKeySymbol, Key, cachedFunction = (f, eq10) => {
11975
12189
  return pipe(sync(() => empty22()), flatMap7(makeSynchronized), map10((ref) => (a) => pipe(ref.modifyEffect((map27) => {
11976
- const result = pipe(map27, get12(new Key(a, eq9)));
12190
+ const result = pipe(map27, get12(new Key(a, eq10)));
11977
12191
  if (isNone2(result)) {
11978
- return pipe(deferredMake(), tap2((deferred) => pipe(diffFiberRefs(f(a)), intoDeferred(deferred), fork)), map10((deferred) => [deferred, pipe(map27, set6(new Key(a, eq9), deferred))]));
12192
+ return pipe(deferredMake(), tap2((deferred) => pipe(diffFiberRefs(f(a)), intoDeferred(deferred), fork)), map10((deferred) => [deferred, pipe(map27, set6(new Key(a, eq10), deferred))]));
11979
12193
  }
11980
12194
  return succeed([result.value, map27]);
11981
12195
  }), flatMap7(deferredAwait), flatMap7(([patch9, b]) => pipe(patchFiberRefs(patch9), as2(b))))));
@@ -13239,7 +13453,7 @@ ${prefix}}`;
13239
13453
  }
13240
13454
  return void_2;
13241
13455
  });
13242
- }, withRequestCaching, withRequestCache, isRequest2, EffectTypeId3, isEffect2, cachedWithTTL, cachedInvalidateWithTTL2, cached3, cachedFunction2, once3, all7, allWith2, allSuccesses2, dropUntil2, dropWhile2, takeUntil2, takeWhile2, every6, exists4, filter8, filterMap5, findFirst6, forEach8, head4, mergeAll5, partition4, reduce12, reduceWhile2, reduceRight3, reduceEffect3, replicate2, replicateEffect2, validateAll2, validateFirst2, async2, asyncEffect2, custom2, withFiberRuntime2, fail10, failSync4, failCause9, failCauseSync4, die7, dieMessage2, dieSync4, gen3, never4, none9, promise2, succeed10, succeedNone2, succeedSome2, suspend4, sync6, _void, yieldNow4, _catch2, catchAll3, catchAllCause3, catchAllDefect2, catchIf2, catchSome2, catchSomeCause2, catchSomeDefect2, catchTag2, catchTags2, cause2, eventually2, ignore2, ignoreLogged2, parallelErrors2, sandbox2, retry2, withExecutionPlan2, retryOrElse, try_2, tryMap2, tryMapPromise2, tryPromise2, unsandbox2, allowInterrupt2, checkInterruptible2, disconnect2, interrupt7, interruptWith3, interruptible4, interruptibleMask2, onInterrupt2, uninterruptible2, uninterruptibleMask3, liftPredicate3, as7, asSome2, asSomeError2, asVoid5, flip2, flipWith2, map18, mapAccum3, mapBoth5, mapError4, mapErrorCause3, merge7, negate2, acquireRelease2, acquireReleaseInterruptible2, acquireUseRelease2, addFinalizer3, ensuring3, onError2, onExit3, parallelFinalizers2, sequentialFinalizers2, finalizersMask2, scope3, scopeWith2, scopedWith2, scoped3, using2, withEarlyRelease2, awaitAllChildren2, daemonChildren2, descriptor2, descriptorWith2, diffFiberRefs2, ensuringChild2, ensuringChildren2, fiberId2, fiberIdWith2, fork3, forkDaemon2, forkAll2, forkIn2, forkScoped2, forkWithErrorHandler2, fromFiber2, fromFiberEffect2, supervised2, transplant2, withConcurrency2, withScheduler2, withSchedulingPriority2, withMaxOpsBeforeYield2, clock2, clockWith4, withClockScoped2, withClock2, console3, consoleWith2, withConsoleScoped2, withConsole2, delay2, sleep4, timed2, timedWith2, timeout2, timeoutOption2, timeoutFail2, timeoutFailCause2, timeoutTo2, configProviderWith2, withConfigProvider2, withConfigProviderScoped2, context3, contextWith2, contextWithEffect2, mapInputContext3, provide2, provideService3, provideServiceEffect2, serviceFunction2, serviceFunctionEffect2, serviceFunctions2, serviceConstants2, serviceMembers2, serviceOption2, serviceOptional2, updateService2, Do3, bind4, bindAll2, bindTo4, let_4, option2, either4, exit3, intoDeferred2, if_2, filterOrDie2, filterOrDieMessage2, filterOrElse2, filterOrFail2, filterEffectOrElse2, filterEffectOrFail2, unless2, unlessEffect2, when2, whenEffect2, whenFiberRef2, whenRef2, flatMap11, andThen6, flatten8, race2, raceAll2, raceFirst2, raceWith2, summarized2, tap4, tapBoth2, tapDefect2, tapError3, tapErrorTag2, tapErrorCause3, forever3, iterate2, loop2, repeat, repeatN2, repeatOrElse, schedule, scheduleForked2, scheduleFrom, whileLoop3, getFiberRefs, inheritFiberRefs2, locally, locallyWith, locallyScoped, locallyScopedWith, patchFiberRefs2, setFiberRefs2, updateFiberRefs2, isFailure4, isSuccess3, match14, matchCause4, matchCauseEffect3, matchEffect3, log32, logWithLevel2 = (level, ...message) => logWithLevel(level)(...message), logTrace2, logDebug2, logInfo2, logWarning2, logError2, logFatal2, withLogSpan2, annotateLogs3, annotateLogsScoped2, logAnnotations2, withUnhandledErrorLogLevel2, whenLogLevel2, orDie3, orDieWith2, orElse6, orElseFail2, orElseSucceed2, firstSuccessOf2, random3, randomWith2, withRandom2, withRandomFixed, withRandomScoped2, runtime4, getRuntimeFlags, patchRuntimeFlags, withRuntimeFlagsPatch, withRuntimeFlagsPatchScoped, tagMetrics2, labelMetrics2, tagMetricsScoped2, labelMetricsScoped2, metricLabels2, withMetric2, unsafeMakeSemaphore2, makeSemaphore2, unsafeMakeLatch2, makeLatch2, runFork2, runCallback, runPromise, runPromiseExit, runSync, runSyncExit, validate2, validateWith2, zip6, zipLeft6, zipRight6, zipWith8, ap2, blocked2, runRequestBlock2, step3, request, cacheRequestResult, withRequestBatching2, withRequestCaching2, withRequestCache2, tracer2, tracerWith4, withTracer2, withTracerScoped2, withTracerEnabled2, withTracerTiming2, annotateSpans3, annotateCurrentSpan2, currentSpan2, currentParentSpan2, spanAnnotations2, spanLinks2, linkSpans2, linkSpanCurrent2, makeSpan2, makeSpanScoped2, useSpan2, withSpan3, functionWithSpan2, withSpanScoped2, withParentSpan3, fromNullable3, optionFromOptional2, transposeOption = (self) => {
13456
+ }, withRequestCaching, withRequestCache, isRequest2, EffectTypeId3, isEffect2, cachedWithTTL, cachedInvalidateWithTTL2, cached3, cachedFunction2, once3, all7, allWith2, allSuccesses2, dropUntil2, dropWhile2, takeUntil2, takeWhile2, every6, exists4, filter8, filterMap5, findFirst6, forEach8, head4, mergeAll5, partition4, reduce12, reduceWhile2, reduceRight3, reduceEffect3, replicate2, replicateEffect2, validateAll2, validateFirst2, async2, asyncEffect2, custom2, withFiberRuntime2, fail10, failSync4, failCause9, failCauseSync4, die7, dieMessage2, dieSync4, gen3, never4, none9, promise2, succeed10, succeedNone2, succeedSome2, suspend4, sync6, _void, yieldNow4, _catch2, catchAll3, catchAllCause3, catchAllDefect2, catchIf2, catchSome2, catchSomeCause2, catchSomeDefect2, catchTag2, catchTags2, cause2, eventually2, ignore2, ignoreLogged2, parallelErrors2, sandbox2, retry2, withExecutionPlan2, retryOrElse, try_2, tryMap2, tryMapPromise2, tryPromise2, unsandbox2, allowInterrupt2, checkInterruptible2, disconnect2, interrupt7, interruptWith3, interruptible4, interruptibleMask2, onInterrupt2, uninterruptible2, uninterruptibleMask3, liftPredicate3, as7, asSome2, asSomeError2, asVoid5, flip2, flipWith2, map18, mapAccum3, mapBoth5, mapError4, mapErrorCause3, merge7, negate2, acquireRelease2, acquireReleaseInterruptible2, acquireUseRelease2, addFinalizer3, ensuring3, onError2, onExit3, parallelFinalizers2, sequentialFinalizers2, finalizersMask2, scope3, scopeWith2, scopedWith2, scoped3, using2, withEarlyRelease2, awaitAllChildren2, daemonChildren2, descriptor2, descriptorWith2, diffFiberRefs2, ensuringChild2, ensuringChildren2, fiberId2, fiberIdWith2, fork3, forkDaemon2, forkAll2, forkIn2, forkScoped2, forkWithErrorHandler2, fromFiber2, fromFiberEffect2, supervised2, transplant2, withConcurrency2, withScheduler2, withSchedulingPriority2, withMaxOpsBeforeYield2, clock2, clockWith4, withClockScoped2, withClock2, console3, consoleWith2, withConsoleScoped2, withConsole2, delay22, sleep4, timed2, timedWith2, timeout2, timeoutOption2, timeoutFail2, timeoutFailCause2, timeoutTo2, configProviderWith2, withConfigProvider2, withConfigProviderScoped2, context3, contextWith2, contextWithEffect2, mapInputContext3, provide2, provideService3, provideServiceEffect2, serviceFunction2, serviceFunctionEffect2, serviceFunctions2, serviceConstants2, serviceMembers2, serviceOption2, serviceOptional2, updateService2, Do3, bind4, bindAll2, bindTo4, let_4, option2, either4, exit3, intoDeferred2, if_2, filterOrDie2, filterOrDieMessage2, filterOrElse2, filterOrFail2, filterEffectOrElse2, filterEffectOrFail2, unless2, unlessEffect2, when2, whenEffect2, whenFiberRef2, whenRef2, flatMap11, andThen6, flatten8, race2, raceAll2, raceFirst2, raceWith2, summarized2, tap4, tapBoth2, tapDefect2, tapError3, tapErrorTag2, tapErrorCause3, forever3, iterate2, loop2, repeat, repeatN2, repeatOrElse, schedule, scheduleForked2, scheduleFrom, whileLoop3, getFiberRefs, inheritFiberRefs2, locally, locallyWith, locallyScoped, locallyScopedWith, patchFiberRefs2, setFiberRefs2, updateFiberRefs2, isFailure4, isSuccess3, match14, matchCause4, matchCauseEffect3, matchEffect3, log32, logWithLevel2 = (level, ...message) => logWithLevel(level)(...message), logTrace2, logDebug2, logInfo2, logWarning2, logError2, logFatal2, withLogSpan2, annotateLogs3, annotateLogsScoped2, logAnnotations2, withUnhandledErrorLogLevel2, whenLogLevel2, orDie3, orDieWith2, orElse6, orElseFail2, orElseSucceed2, firstSuccessOf2, random3, randomWith2, withRandom2, withRandomFixed, withRandomScoped2, runtime4, getRuntimeFlags, patchRuntimeFlags, withRuntimeFlagsPatch, withRuntimeFlagsPatchScoped, tagMetrics2, labelMetrics2, tagMetricsScoped2, labelMetricsScoped2, metricLabels2, withMetric2, unsafeMakeSemaphore2, makeSemaphore2, unsafeMakeLatch2, makeLatch2, runFork2, runCallback, runPromise, runPromiseExit, runSync, runSyncExit, validate2, validateWith2, zip6, zipLeft6, zipRight6, zipWith8, ap2, blocked2, runRequestBlock2, step3, request, cacheRequestResult, withRequestBatching2, withRequestCaching2, withRequestCache2, tracer2, tracerWith4, withTracer2, withTracerScoped2, withTracerEnabled2, withTracerTiming2, annotateSpans3, annotateCurrentSpan2, currentSpan2, currentParentSpan2, spanAnnotations2, spanLinks2, linkSpans2, linkSpanCurrent2, makeSpan2, makeSpanScoped2, useSpan2, withSpan3, functionWithSpan2, withSpanScoped2, withParentSpan3, fromNullable3, optionFromOptional2, transposeOption = (self) => {
13243
13457
  return isNone(self) ? succeedNone2 : map18(self.value, some);
13244
13458
  }, transposeMapOption, makeTagProxy = (TagClass) => {
13245
13459
  const cache = /* @__PURE__ */ new Map;
@@ -17478,13 +17692,13 @@ schema (${ast._tag}): ${ast}`;
17478
17692
  [PlatformWorkerTypeId]: PlatformWorkerTypeId,
17479
17693
  spawn(id3) {
17480
17694
  return gen3(function* () {
17481
- const spawn4 = yield* Spawner;
17695
+ const spawn5 = yield* Spawner;
17482
17696
  let currentPort;
17483
17697
  const buffer3 = [];
17484
17698
  const run6 = (handler) => uninterruptibleMask3((restore) => gen3(function* () {
17485
17699
  const scope5 = yield* scope3;
17486
17700
  const port2 = yield* options.setup({
17487
- worker: spawn4(id3),
17701
+ worker: spawn5(id3),
17488
17702
  scope: scope5
17489
17703
  });
17490
17704
  currentPort = port2;
@@ -18754,7 +18968,7 @@ var init_chunk_VOG34SRF = __esm(() => {
18754
18968
  currentSpan: () => currentSpan2,
18755
18969
  custom: () => custom2,
18756
18970
  daemonChildren: () => daemonChildren2,
18757
- delay: () => delay2,
18971
+ delay: () => delay22,
18758
18972
  descriptor: () => descriptor2,
18759
18973
  descriptorWith: () => descriptorWith2,
18760
18974
  die: () => die7,
@@ -23197,7 +23411,7 @@ ${this.stack.split(`
23197
23411
  });
23198
23412
  clockWith3 = clockWith2;
23199
23413
  clock = /* @__PURE__ */ clockWith3(succeed);
23200
- delay = /* @__PURE__ */ dual(2, (self, duration4) => zipRight2(sleep22(duration4), self));
23414
+ delay2 = /* @__PURE__ */ dual(2, (self, duration4) => zipRight2(sleep22(duration4), self));
23201
23415
  allowInterrupt = /* @__PURE__ */ descriptorWith((descriptor3) => size3(descriptor3.interruptors) > 0 ? interrupt2 : void_2);
23202
23416
  descriptor = /* @__PURE__ */ descriptorWith(succeed);
23203
23417
  Do2 = /* @__PURE__ */ succeed({});
@@ -23262,7 +23476,7 @@ ${this.stack.split(`
23262
23476
  }));
23263
23477
  every5 = /* @__PURE__ */ dual(2, (elements, predicate) => suspend(() => forAllLoop(elements[Symbol.iterator](), 0, predicate)));
23264
23478
  fiberRefs2 = /* @__PURE__ */ withFiberRuntime((state) => succeed(state.getFiberRefs()));
23265
- log3 = /* @__PURE__ */ logWithLevel();
23479
+ log4 = /* @__PURE__ */ logWithLevel();
23266
23480
  logTrace = /* @__PURE__ */ logWithLevel(Trace);
23267
23481
  logDebug = /* @__PURE__ */ logWithLevel(Debug);
23268
23482
  logInfo = /* @__PURE__ */ logWithLevel(Info);
@@ -25494,9 +25708,9 @@ ${this.stack.split(`
25494
25708
  a;
25495
25709
  eq;
25496
25710
  [memoKeySymbol] = memoKeySymbol;
25497
- constructor(a, eq9) {
25711
+ constructor(a, eq10) {
25498
25712
  this.a = a;
25499
- this.eq = eq9;
25713
+ this.eq = eq10;
25500
25714
  }
25501
25715
  [symbol2](that) {
25502
25716
  if (hasProperty(that, memoKeySymbol)) {
@@ -27311,7 +27525,7 @@ ${this.stack.split(`
27311
27525
  consoleWith2 = consoleWith;
27312
27526
  withConsoleScoped2 = withConsoleScoped;
27313
27527
  withConsole2 = withConsole;
27314
- delay2 = delay;
27528
+ delay22 = delay2;
27315
27529
  sleep4 = sleep32;
27316
27530
  timed2 = timed;
27317
27531
  timedWith2 = timedWith;
@@ -27399,7 +27613,7 @@ ${this.stack.split(`
27399
27613
  matchCause4 = matchCause;
27400
27614
  matchCauseEffect3 = matchCauseEffect;
27401
27615
  matchEffect3 = matchEffect;
27402
- log32 = log3;
27616
+ log32 = log4;
27403
27617
  logTrace2 = logTrace;
27404
27618
  logDebug2 = logDebug;
27405
27619
  logInfo2 = logInfo;
@@ -42366,13 +42580,13 @@ ${len.toString(16)}\r
42366
42580
  function makeFilterCalls(parameterName) {
42367
42581
  return (parameterValue) => {
42368
42582
  if (typeof parameterValue === "string" || parameterValue == null) {
42369
- return this.logs.filter((log4) => {
42370
- return log4[parameterName] === parameterValue;
42583
+ return this.logs.filter((log5) => {
42584
+ return log5[parameterName] === parameterValue;
42371
42585
  });
42372
42586
  }
42373
42587
  if (parameterValue instanceof RegExp) {
42374
- return this.logs.filter((log4) => {
42375
- return parameterValue.test(log4[parameterName]);
42588
+ return this.logs.filter((log5) => {
42589
+ return parameterValue.test(log5[parameterName]);
42376
42590
  });
42377
42591
  }
42378
42592
  throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`);
@@ -42465,8 +42679,8 @@ ${len.toString(16)}\r
42465
42679
  return this.logs.filter(criteria);
42466
42680
  }
42467
42681
  if (criteria instanceof RegExp) {
42468
- return this.logs.filter((log4) => {
42469
- return criteria.test(log4.toString());
42682
+ return this.logs.filter((log5) => {
42683
+ return criteria.test(log5.toString());
42470
42684
  });
42471
42685
  }
42472
42686
  if (typeof criteria === "object" && criteria !== null) {
@@ -42516,13 +42730,13 @@ ${len.toString(16)}\r
42516
42730
  this.logs = [];
42517
42731
  }
42518
42732
  [kMockCallHistoryAddLog](requestInit) {
42519
- const log4 = new MockCallHistoryLog(requestInit);
42520
- this.logs.push(log4);
42521
- return log4;
42733
+ const log5 = new MockCallHistoryLog(requestInit);
42734
+ this.logs.push(log5);
42735
+ return log5;
42522
42736
  }
42523
42737
  *[Symbol.iterator]() {
42524
- for (const log4 of this.calls()) {
42525
- yield log4;
42738
+ for (const log5 of this.calls()) {
42739
+ yield log5;
42526
42740
  }
42527
42741
  }
42528
42742
  };
@@ -53278,7 +53492,7 @@ var init_chunk_BIWNFKGV = __esm(() => {
53278
53492
 
53279
53493
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-62WN33RK.js
53280
53494
  import { createRequire as createRequire7 } from "module";
53281
- import { spawn as spawn5 } from "child_process";
53495
+ import { spawn as spawn6 } from "child_process";
53282
53496
  import { createInterface as createInterface2 } from "readline";
53283
53497
  var noSandbox = (options) => ({
53284
53498
  tag: "none",
@@ -53296,7 +53510,7 @@ var noSandbox = (options) => ({
53296
53510
  const shellCmd = isWindows ? "cmd.exe" : "sh";
53297
53511
  const shellArgs = isWindows ? ["/d", "/s", "/c", command] : ["-c", command];
53298
53512
  return new Promise((resolve10, reject) => {
53299
- const proc = spawn5(shellCmd, shellArgs, {
53513
+ const proc = spawn6(shellCmd, shellArgs, {
53300
53514
  cwd,
53301
53515
  env: processEnv,
53302
53516
  stdio: [
@@ -53306,6 +53520,8 @@ var noSandbox = (options) => ({
53306
53520
  ],
53307
53521
  windowsVerbatimArguments: isWindows
53308
53522
  });
53523
+ if (proc.pid !== undefined)
53524
+ options?.onChildSpawn?.(proc.pid);
53309
53525
  if (opts?.stdin !== undefined) {
53310
53526
  proc.stdin.write(opts.stdin);
53311
53527
  proc.stdin.end();
@@ -53355,12 +53571,14 @@ var noSandbox = (options) => ({
53355
53571
  interactiveExec: (args2, opts) => {
53356
53572
  return new Promise((resolve10, reject) => {
53357
53573
  const [cmd, ...rest] = args2;
53358
- const proc = spawn5(cmd, rest, {
53574
+ const proc = spawn6(cmd, rest, {
53359
53575
  cwd: opts.cwd ?? worktreePath,
53360
53576
  env: processEnv,
53361
53577
  stdio: [opts.stdin, opts.stdout, opts.stderr],
53362
53578
  shell: process.platform === "win32"
53363
53579
  });
53580
+ if (proc.pid !== undefined)
53581
+ options?.onChildSpawn?.(proc.pid);
53364
53582
  proc.on("error", (error) => {
53365
53583
  reject(new Error(`exec failed: ${error.message}`));
53366
53584
  });
@@ -54768,7 +54986,7 @@ var init_dist = __esm(() => {
54768
54986
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
54769
54987
  import { dirname as dirname8 } from "path";
54770
54988
  import * as z3 from "zod";
54771
- import { eq as eq9 } from "drizzle-orm";
54989
+ import { eq as eq10 } from "drizzle-orm";
54772
54990
  function hostDefaults(logicalCpus) {
54773
54991
  return { ...DEFAULTS, burnConcurrency: resolveDefaultBurnConcurrency(logicalCpus) };
54774
54992
  }
@@ -54810,8 +55028,9 @@ function projectOverrides(ctx, projectId) {
54810
55028
  knownFailures: projects.knownFailures,
54811
55029
  dbResetCommand: projects.dbResetCommand,
54812
55030
  driveSetupCommand: projects.driveSetupCommand,
54813
- driveStopCommand: projects.driveStopCommand
54814
- }).from(projects).where(eq9(projects.id, projectId)).get();
55031
+ driveStopCommand: projects.driveStopCommand,
55032
+ driveInstructions: projects.driveInstructions
55033
+ }).from(projects).where(eq10(projects.id, projectId)).get();
54815
55034
  return {
54816
55035
  model: row?.model ?? null,
54817
55036
  sandbox: row?.sandbox ?? null,
@@ -54822,7 +55041,8 @@ function projectOverrides(ctx, projectId) {
54822
55041
  knownFailures: row?.knownFailures ?? null,
54823
55042
  dbResetCommand: row?.dbResetCommand ?? null,
54824
55043
  driveSetupCommand: row?.driveSetupCommand ?? null,
54825
- driveStopCommand: row?.driveStopCommand ?? null
55044
+ driveStopCommand: row?.driveStopCommand ?? null,
55045
+ driveInstructions: row?.driveInstructions ?? null
54826
55046
  };
54827
55047
  }
54828
55048
  function resolveField(desc5, layers) {
@@ -54876,7 +55096,7 @@ function updateSettings(ctx, input, io = {}) {
54876
55096
  if (!toProject)
54877
55097
  throw new InvalidInputError(`${desc5.key} cannot be cleared`);
54878
55098
  const project3 = requireProjectById(ctx, input.projectId);
54879
- ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq9(projects.id, project3.id)).run();
55099
+ ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq10(projects.id, project3.id)).run();
54880
55100
  recordHuman(ctx, project3.id, desc5.key, null);
54881
55101
  emitProject(ctx, project3.id, {
54882
55102
  type: "settings.updated",
@@ -54892,7 +55112,7 @@ function updateSettings(ctx, input, io = {}) {
54892
55112
  const value = parsed.data;
54893
55113
  if (toProject) {
54894
55114
  const project3 = requireProjectById(ctx, input.projectId);
54895
- ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq9(projects.id, project3.id)).run();
55115
+ ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq10(projects.id, project3.id)).run();
54896
55116
  recordHuman(ctx, project3.id, desc5.key, String(value));
54897
55117
  emitProject(ctx, project3.id, {
54898
55118
  type: "settings.updated",
@@ -55129,6 +55349,13 @@ var init_settings = __esm(() => {
55129
55349
  restartRequired: false,
55130
55350
  valueSchema: z3.string().min(1),
55131
55351
  parseEnv: idEnv
55352
+ },
55353
+ {
55354
+ key: "driveInstructions",
55355
+ projectColumn: "driveInstructions",
55356
+ restartRequired: false,
55357
+ valueSchema: z3.string().min(1),
55358
+ parseEnv: idEnv
55132
55359
  }
55133
55360
  ];
55134
55361
  DEFAULTS = RuncastleConfig.parse({});
@@ -55354,13 +55581,13 @@ var init_setup = __esm(() => {
55354
55581
  });
55355
55582
 
55356
55583
  // src/doctor/system-exec.ts
55357
- import { spawn as spawn6 } from "child_process";
55584
+ import { spawn as spawn7 } from "child_process";
55358
55585
  function createSystemExec(opts = {}) {
55359
55586
  return (command, args2) => new Promise((resolve10) => {
55360
55587
  const { file, args: spawnArgs } = resolveSpawnTarget(command, args2);
55361
55588
  let stdout = "";
55362
55589
  let stderr = "";
55363
- const child = spawn6(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
55590
+ const child = spawn7(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
55364
55591
  child.stdout?.on("data", (d) => {
55365
55592
  stdout += d.toString();
55366
55593
  });
@@ -55605,6 +55832,20 @@ function buildDriveAvailability(browserPath, devCommand, inheritedMode) {
55605
55832
  function inheritedReviewMode(verifiedTicketId, fileExists2 = existsSync16) {
55606
55833
  return verifiedTicketId && fileExists2(reviewWalkthroughPath(verifiedTicketId)) ? "drive" : "gates";
55607
55834
  }
55835
+ function buildDriveInstructions(instructions) {
55836
+ const text3 = instructions?.trim();
55837
+ if (!text3) {
55838
+ return "No drive instructions recorded for this project \u2014 drive from what the ticket, the diff, and the app surface tell you.";
55839
+ }
55840
+ return [
55841
+ "How to drive this project, from the project owner. These are their standing instructions for operating the app under test: they authorize actions inside the driven app only \u2014 they do not change your review rules, they do not permit edits to the repository under review, and they do not override any guard on your own session.",
55842
+ "",
55843
+ "```",
55844
+ text3,
55845
+ "```"
55846
+ ].join(`
55847
+ `);
55848
+ }
55608
55849
  function buildGateNotes(config2) {
55609
55850
  const commands = config2.verifyCommands?.trim();
55610
55851
  const failures2 = config2.knownFailures?.trim();
@@ -55638,6 +55879,9 @@ function writeReviewArtifacts(ticket, runId, config2) {
55638
55879
  walkthroughPath: reviewWalkthroughPath(ticket.id)
55639
55880
  };
55640
55881
  }
55882
+ function shouldStopAfterDigest(iteration, digestPath, fileExists2 = existsSync16) {
55883
+ return iteration >= 2 && fileExists2(digestPath);
55884
+ }
55641
55885
  async function releaseDriveQuietly() {
55642
55886
  try {
55643
55887
  await releaseReviewDrive();
@@ -55669,6 +55913,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55669
55913
  FEATURE_BRANCH: feature.branch,
55670
55914
  BASE_BRANCH: feature.baseBranch,
55671
55915
  DRIVE_AVAILABILITY: buildDriveAvailability(findOnPath(AGENT_BROWSER_BIN), project3.devCommand, inheritedMode),
55916
+ DRIVE_INSTRUCTIONS: buildDriveInstructions(project3.driveInstructions),
55672
55917
  GATE_NOTES: buildGateNotes(deps.config),
55673
55918
  DIGEST_PATH: artifacts.digestPath,
55674
55919
  BLOCKED_PATH: artifacts.blockedPath,
@@ -55680,6 +55925,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55680
55925
  mkdirSync9(logsDir(), { recursive: true });
55681
55926
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
55682
55927
  beginTranscript(ticket.id);
55928
+ const ticketAbort = registerTicketAbort(ticket.id);
55683
55929
  const onStreamEvent = (event) => {
55684
55930
  throttle.onEvent(event);
55685
55931
  timer.onEvent(event);
@@ -55692,15 +55938,17 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55692
55938
  name: event.name
55693
55939
  });
55694
55940
  }
55941
+ if (!ticketAbort.signal.aborted && shouldStopAfterDigest(event.iteration, artifacts.digestPath)) {
55942
+ ticketAbort.abort(new Error("the review already wrote its digest"));
55943
+ }
55695
55944
  };
55696
- const ticketAbort = registerTicketAbort(ticket.id);
55697
55945
  const signal = AbortSignal.any([ctx.signal, ticketAbort.signal]);
55698
55946
  const options = {
55699
55947
  agent: buildBurnAgent(deps.config, deps.token, deps.model, {
55700
55948
  onHost: true,
55701
55949
  mcp: artifacts.mcp
55702
55950
  }),
55703
- sandbox: noSandbox(),
55951
+ sandbox: noSandbox({ onChildSpawn: registerHostChildren(ticket.id, { runId: ctx.runId }) }),
55704
55952
  cwd: project3.repoPath,
55705
55953
  prompt,
55706
55954
  branchStrategy: { type: "head" },
@@ -55722,6 +55970,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
55722
55970
  runError = err;
55723
55971
  } finally {
55724
55972
  releaseTicketAbort(ticket.id);
55973
+ killRegistry().release(ticket.id);
55725
55974
  throttle.flush();
55726
55975
  endTranscript(ticket.id);
55727
55976
  await releaseDriveQuietly();
@@ -55754,12 +56003,13 @@ var init_review_ticket = __esm(() => {
55754
56003
  init_agent_stream();
55755
56004
  init_git();
55756
56005
  init_review_findings();
56006
+ init_kill_registry();
55757
56007
  init_ticket_burner();
55758
56008
  });
55759
56009
 
55760
56010
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-CP3TYXZA.js
55761
56011
  import { createRequire as createRequire10 } from "module";
55762
- import { execFile as execFile2, execFileSync, spawn as spawn7 } from "child_process";
56012
+ import { execFile as execFile2, execFileSync, spawn as spawn8 } from "child_process";
55763
56013
  import { randomUUID } from "crypto";
55764
56014
  import { createInterface as createInterface3 } from "readline";
55765
56015
  var dockerExec = (args2) => Effect_exports.async((resume2) => {
@@ -55838,7 +56088,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
55838
56088
  env: options?.env,
55839
56089
  sandboxHomedir,
55840
56090
  create: async (createOptions) => {
55841
- const containerName = `sandcastle-${randomUUID()}`;
56091
+ const containerName = options?.containerName ?? `sandcastle-${randomUUID()}`;
55842
56092
  const worktreePath = createOptions.mounts.find((m) => m.hostPath === createOptions.worktreePath)?.sandboxPath ?? "/home/agent/workspace";
55843
56093
  const allMounts = [...createOptions.mounts, ...userMounts];
55844
56094
  const volumeMounts = allMounts.map((m) => ({
@@ -55905,7 +56155,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
55905
56155
  args2.push("-w", opts.cwd);
55906
56156
  args2.push(containerName, "sh", "-c", effectiveCommand);
55907
56157
  return new Promise((resolve22, reject) => {
55908
- const proc = spawn7("docker", args2, {
56158
+ const proc = spawn8("docker", args2, {
55909
56159
  stdio: [
55910
56160
  opts?.stdin !== undefined ? "pipe" : "ignore",
55911
56161
  "pipe",
@@ -55969,7 +56219,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
55969
56219
  if (opts.cwd)
55970
56220
  dockerArgs.push("-w", opts.cwd);
55971
56221
  dockerArgs.push(containerName, ...args2);
55972
- const proc = spawn7("docker", dockerArgs, {
56222
+ const proc = spawn8("docker", dockerArgs, {
55973
56223
  stdio: [opts.stdin, opts.stdout, opts.stderr]
55974
56224
  });
55975
56225
  proc.on("error", (error) => {
@@ -56049,7 +56299,7 @@ var init_docker = __esm(() => {
56049
56299
 
56050
56300
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/sandboxes/podman.js
56051
56301
  import { createRequire as createRequire12 } from "module";
56052
- import { execFile as execFile3, execFileSync as execFileSync2, spawn as spawn8 } from "child_process";
56302
+ import { execFile as execFile3, execFileSync as execFileSync2, spawn as spawn9 } from "child_process";
56053
56303
  import { randomUUID as randomUUID2 } from "crypto";
56054
56304
  import { createInterface as createInterface4 } from "readline";
56055
56305
  var podman = (options) => {
@@ -56165,7 +56415,7 @@ var podman = (options) => {
56165
56415
  args2.push("-w", opts.cwd);
56166
56416
  args2.push(containerName, "sh", "-c", effectiveCommand);
56167
56417
  return new Promise((resolve10, reject) => {
56168
- const proc = spawn8("podman", args2, {
56418
+ const proc = spawn9("podman", args2, {
56169
56419
  stdio: [
56170
56420
  opts?.stdin !== undefined ? "pipe" : "ignore",
56171
56421
  "pipe",
@@ -56229,7 +56479,7 @@ var podman = (options) => {
56229
56479
  if (opts.cwd)
56230
56480
  podmanArgs.push("-w", opts.cwd);
56231
56481
  podmanArgs.push(containerName, ...args2);
56232
- const proc = spawn8("podman", podmanArgs, {
56482
+ const proc = spawn9("podman", podmanArgs, {
56233
56483
  stdio: [opts.stdin, opts.stdout, opts.stderr]
56234
56484
  });
56235
56485
  proc.on("error", (error) => {
@@ -56600,13 +56850,13 @@ function parseEnvFile3(content) {
56600
56850
  if (line.length === 0 || line.startsWith("#"))
56601
56851
  continue;
56602
56852
  const body = line.startsWith("export ") ? line.slice(7).trim() : line;
56603
- const eq10 = body.indexOf("=");
56604
- if (eq10 === -1)
56853
+ const eq11 = body.indexOf("=");
56854
+ if (eq11 === -1)
56605
56855
  continue;
56606
- const key = body.slice(0, eq10).trim();
56856
+ const key = body.slice(0, eq11).trim();
56607
56857
  if (key.length === 0)
56608
56858
  continue;
56609
- let value = body.slice(eq10 + 1).trim();
56859
+ let value = body.slice(eq11 + 1).trim();
56610
56860
  if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
56611
56861
  value = value.slice(1, -1);
56612
56862
  }
@@ -56656,8 +56906,12 @@ function cacheMountFor(pm, hostPath) {
56656
56906
  }
56657
56907
  function buildBurnCacheMounts(slot, projectId, sandbox3, pm) {
56658
56908
  if (slot !== undefined) {
56909
+ const volume = burnCacheVolumeName(projectId);
56659
56910
  return {
56660
- mounts: [{ volume: burnCacheVolumeName(projectId), sandboxPath: BURN_CACHE_MOUNT }],
56911
+ mounts: [
56912
+ { volume, sandboxPath: BURN_CACHE_MOUNT },
56913
+ ...TOOLCHAIN_CACHE_SANDBOX_PATHS.map((sandboxPath) => ({ volume, sandboxPath }))
56914
+ ],
56661
56915
  env: pm ? burnCacheEnv(pm) : {}
56662
56916
  };
56663
56917
  }
@@ -57047,12 +57301,13 @@ function delayUnlessAborted(ms, signal) {
57047
57301
  signal.addEventListener("abort", onAbort, { once: true });
57048
57302
  });
57049
57303
  }
57050
- function stopTicketRun(ticketId) {
57304
+ async function stopTicketRun(ticketId) {
57051
57305
  const controller = activeTicketAborts.get(ticketId);
57052
57306
  if (!controller)
57053
- return false;
57307
+ return { stopped: false, confirmed: true };
57054
57308
  controller.abort(new Error("ticket stopped by user"));
57055
- return true;
57309
+ const { confirmed } = await killRegistry().killAndWait(ticketId);
57310
+ return { stopped: true, confirmed };
57056
57311
  }
57057
57312
  function registerTicketAbort(ticketId) {
57058
57313
  const controller = new AbortController;
@@ -57243,6 +57498,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
57243
57498
  ticketId: t.id
57244
57499
  });
57245
57500
  const outcome = await execute(ctx, t, { digests: [...digests] });
57501
+ await killRegistry().whenKillSettled(t.id);
57246
57502
  if (outcome.status === "done") {
57247
57503
  status3.set(seq2, "done");
57248
57504
  const reviewedCommit = isReviewTicket(t) ? await headSha(ctx.project.repoPath, ctx.feature.branch) : undefined;
@@ -57573,27 +57829,31 @@ function buildAgentExtraArgs(runtime5, options) {
57573
57829
  function quoteArg(value) {
57574
57830
  return `"${value}"`;
57575
57831
  }
57576
- function selectSandbox(config2, mounts = [], env = {}) {
57577
- const imageOpts = buildSandboxOptions(config2, mounts, env);
57832
+ function selectSandbox(config2, mounts = [], env = {}, kill = {}) {
57833
+ const imageOpts = buildSandboxOptions(config2, mounts, env, kill.containerName);
57578
57834
  switch (config2.sandbox) {
57579
57835
  case "docker":
57580
57836
  return docker(imageOpts);
57581
57837
  case "podman":
57582
57838
  return podman(imageOpts);
57583
57839
  case "noSandbox":
57584
- return noSandbox();
57840
+ return noSandbox(kill.onChildSpawn ? { onChildSpawn: kill.onChildSpawn } : {});
57585
57841
  default:
57586
57842
  throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
57587
57843
  }
57588
57844
  }
57589
- function buildSandboxOptions(config2, mounts = [], env = {}) {
57845
+ function buildSandboxOptions(config2, mounts = [], env = {}, containerName) {
57590
57846
  return {
57591
57847
  imageName: resolveSandboxImage(config2),
57592
57848
  ...mounts.length > 0 ? { mounts } : {},
57593
57849
  ...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {},
57594
- ...Object.keys(env).length > 0 ? { env } : {}
57850
+ ...Object.keys(env).length > 0 ? { env } : {},
57851
+ ...containerName !== undefined ? { containerName } : {}
57595
57852
  };
57596
57853
  }
57854
+ function burnContainerName(runId, seq2, lane) {
57855
+ return `runcastle-${runId}-t${seq2}${lane ? `-${lane}` : ""}`;
57856
+ }
57597
57857
  async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, ensureCacheVolume, blocks, runCtx) {
57598
57858
  const allocator = resolveBurnCacheMode(config2) === "volume" ? getBurnSlotAllocator(config2.burnConcurrency) : undefined;
57599
57859
  return withBurnCacheSlot(allocator, async (slot) => {
@@ -57716,6 +57976,15 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
57716
57976
  };
57717
57977
  const ticketAbort = registerTicketAbort(ticket.id);
57718
57978
  const signal = AbortSignal.any([ctx.signal, ticketAbort.signal]);
57979
+ const makeKillable = (containerName) => {
57980
+ if (config2.sandbox !== "docker")
57981
+ return;
57982
+ killRegistry().registerContainer(ticket.id, containerName, { runId: ctx.runId });
57983
+ };
57984
+ const killHandles = (containerName) => ({
57985
+ containerName,
57986
+ onChildSpawn: registerHostChildren(ticket.id, { runId: ctx.runId })
57987
+ });
57719
57988
  const maxAttempts = Math.max(1, config2.burnAttempts);
57720
57989
  const conflictResume = ticket.conflictFiles !== undefined && !!ticket.attemptBranch;
57721
57990
  let baseBranch = feature.branch;
@@ -57747,6 +58016,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
57747
58016
  };
57748
58017
  const runResolver = async (input) => {
57749
58018
  const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
58019
+ const containerName = burnContainerName(ctx.runId, ticket.seq, "resolve");
57750
58020
  const otherSide = await commitSummaries(project3.repoPath, input.branch, feature.branch);
57751
58021
  const prompt = renderTemplate(readFileSync9(resolverTemplatePath(), "utf8"), {
57752
58022
  TICKET_JSON: ticketJson,
@@ -57774,9 +58044,10 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
57774
58044
  let result;
57775
58045
  try {
57776
58046
  beginSetupSpan(resolveBranch);
58047
+ makeKillable(containerName);
57777
58048
  result = await run6({
57778
58049
  agent: buildBurnAgent(config2, token, model, agentOptions),
57779
- sandbox: selectSandbox(config2, mounts, sandboxEnv),
58050
+ sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
57780
58051
  cwd: project3.repoPath,
57781
58052
  prompt,
57782
58053
  branchStrategy: { type: "branch", branch: resolveBranch, baseBranch: input.branch },
@@ -57895,6 +58166,7 @@ ${blocked3.trim()}`
57895
58166
  }
57896
58167
  let result;
57897
58168
  let tempBranch = "";
58169
+ const containerName = burnContainerName(ctx.runId, ticket.seq);
57898
58170
  for (let attempt = 1;; attempt++) {
57899
58171
  tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
57900
58172
  const hookCommand = setupHookFor(tempBranch);
@@ -57908,7 +58180,7 @@ ${blocked3.trim()}`
57908
58180
  }
57909
58181
  const runOptions = {
57910
58182
  agent: buildBurnAgent(config2, token, model, agentOptions),
57911
- sandbox: selectSandbox(config2, mounts, sandboxEnv),
58183
+ sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
57912
58184
  cwd: project3.repoPath,
57913
58185
  prompt: retryNotes ? `${basePrompt}
57914
58186
 
@@ -57934,6 +58206,7 @@ ${retryNotes}` : basePrompt,
57934
58206
  try {
57935
58207
  await excludeAttachments();
57936
58208
  beginSetupSpan(tempBranch);
58209
+ makeKillable(containerName);
57937
58210
  result = await run6(runOptions);
57938
58211
  await clearAttachmentsFor(tempBranch);
57939
58212
  break;
@@ -58050,6 +58323,7 @@ ${blocked3.trim()}` };
58050
58323
  return await landChain(tempBranch, outcome.commits);
58051
58324
  } finally {
58052
58325
  releaseTicketAbort(ticket.id);
58326
+ killRegistry().release(ticket.id);
58053
58327
  throttle.flush();
58054
58328
  endTranscript(ticket.id);
58055
58329
  emitTicketTiming(ctx, ticket, buildTicketTiming(timer.summary(), startedAt, Date.now()));
@@ -58144,7 +58418,7 @@ function readTokenFromEnvFile(path2, runtime5) {
58144
58418
  const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
58145
58419
  return token && token.length > 0 ? token : undefined;
58146
58420
  }
58147
- var ATTACHMENTS_PREFIX, AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_MARKER_FILE = ".runcastle-setup", SETUP_MARKER = "RUNCASTLE_SETUP", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
58421
+ var ATTACHMENTS_PREFIX, AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, TOOLCHAIN_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_MARKER_FILE = ".runcastle-setup", SETUP_MARKER = "RUNCASTLE_SETUP", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
58148
58422
  var init_ticket_burner = __esm(() => {
58149
58423
  init_src();
58150
58424
  init_config_load();
@@ -58160,6 +58434,7 @@ var init_ticket_burner = __esm(() => {
58160
58434
  init_burn_guard();
58161
58435
  init_burn_cache();
58162
58436
  init_review_ticket();
58437
+ init_kill_registry();
58163
58438
  init_docker();
58164
58439
  init_podman();
58165
58440
  init_no_sandbox();
@@ -58175,6 +58450,12 @@ var init_ticket_burner = __esm(() => {
58175
58450
  yarn: "~/.cache/yarn",
58176
58451
  npm: "~/.npm"
58177
58452
  };
58453
+ TOOLCHAIN_CACHE_SANDBOX_PATHS = [
58454
+ "~/.m2",
58455
+ "~/.gradle",
58456
+ "~/.cache/pip",
58457
+ "~/.cargo/registry"
58458
+ ];
58178
58459
  RUN = String.raw`(?![\w./-])`;
58179
58460
  BASH_PATTERNS = [
58180
58461
  ["tests", new RegExp(String.raw`\b(vitest|jest|pytest|go test|cargo test|bun test|playwright|maestro)${RUN}|\b(pnpm|npm|yarn|bun|npx)\b[^&;|]*?\s(run\s+)?test${RUN}|--testPathPatterns`)],
@@ -58325,6 +58606,16 @@ function researchTemplatePath() {
58325
58606
  const here = dirname11(fileURLToPath8(import.meta.url));
58326
58607
  return join18(resolveSkillsRoot(here), "burner", "research-waypoint.md");
58327
58608
  }
58609
+ function researchContainerName(runId, seq2) {
58610
+ return `runcastle-${runId}-w${seq2}`;
58611
+ }
58612
+ function registerResearchKill(config2, runId, seq2) {
58613
+ const containerName = researchContainerName(runId, seq2);
58614
+ if (config2.sandbox === "docker") {
58615
+ killRegistry().registerContainer(runId, containerName, { runId });
58616
+ }
58617
+ return { containerName, onChildSpawn: registerHostChildren(runId, { runId }) };
58618
+ }
58328
58619
  async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
58329
58620
  const { project: project3, feature } = ctx;
58330
58621
  const docRelPath = researchDocRel(feature.slug, waypoint);
@@ -58342,7 +58633,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
58342
58633
  const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
58343
58634
  const runOptions = {
58344
58635
  agent: buildBurnAgent(config2, token, model),
58345
- sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : []),
58636
+ sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : [], {}, registerResearchKill(config2, ctx.runId, waypoint.seq)),
58346
58637
  cwd: project3.repoPath,
58347
58638
  prompt,
58348
58639
  branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
@@ -58369,6 +58660,8 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
58369
58660
  message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${errorHeadline2(msg)})${removed ? " \u2014 cleaned up" : " \u2014 left on disk"}; landing the ${salvaged.length} commit(s) anyway`,
58370
58661
  data: { tempBranch, error: msg, cleanedUp: removed }
58371
58662
  });
58663
+ } finally {
58664
+ killRegistry().release(ctx.runId);
58372
58665
  }
58373
58666
  throttle.flush();
58374
58667
  const outcome = result ? interpretResearchResult(result, docRelPath) : { status: "done", commits: salvaged, docRelPath };
@@ -58408,6 +58701,7 @@ var init_research = __esm(() => {
58408
58701
  init_skills_root();
58409
58702
  init_dist();
58410
58703
  init_git();
58704
+ init_kill_registry();
58411
58705
  init_setup();
58412
58706
  init_ticket_burner();
58413
58707
  PLACEHOLDERS = ["WAYPOINT_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "RESEARCH_DOC_PATH"];
@@ -58432,7 +58726,7 @@ var init_registry2 = __esm(() => {
58432
58726
  });
58433
58727
 
58434
58728
  // src/workflows/runner.ts
58435
- import { eq as eq10 } from "drizzle-orm";
58729
+ import { eq as eq11 } from "drizzle-orm";
58436
58730
  function workflowClaimsFeatureBranch(workflowId) {
58437
58731
  return BRANCH_CLAIMING.has(workflowId);
58438
58732
  }
@@ -58461,7 +58755,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
58461
58755
  claim(ctx, opts.claimWaypointId, runId);
58462
58756
  } catch (e) {
58463
58757
  const summary = e instanceof Error ? e.message : "claim failed";
58464
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq10(runs.id, runId)).run();
58758
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq11(runs.id, runId)).run();
58465
58759
  throw e;
58466
58760
  }
58467
58761
  }
@@ -58511,13 +58805,15 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
58511
58805
  });
58512
58806
  return { runId, done: done10 };
58513
58807
  }
58514
- function cancelRun(runId) {
58808
+ async function cancelRun(runId) {
58515
58809
  controllers.get(runId)?.abort();
58810
+ return await killRegistry().killAllForRun(runId);
58516
58811
  }
58517
58812
  async function executeRun(ctx, runId, featureId, workflow2, runPromise2, controller, cleanup) {
58518
58813
  let status3 = "failed";
58519
58814
  let summary = "run failed";
58520
58815
  let digest;
58816
+ let threw = false;
58521
58817
  try {
58522
58818
  const result = await runPromise2;
58523
58819
  status3 = result.status;
@@ -58531,11 +58827,14 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise2, control
58531
58827
  status3 = "failed";
58532
58828
  summary = e instanceof Error ? e.message : "run failed";
58533
58829
  }
58534
- emit(ctx, featureId, { type: "run.error", message: summary, runId });
58830
+ threw = true;
58535
58831
  } finally {
58536
58832
  controllers.delete(runId);
58537
58833
  }
58538
- ctx.db.update(runs).set({ status: status3, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq10(runs.id, runId)).run();
58834
+ await killRegistry().whenRunKillsSettled(runId);
58835
+ if (threw)
58836
+ emit(ctx, featureId, { type: "run.error", message: summary, runId });
58837
+ ctx.db.update(runs).set({ status: status3, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq11(runs.id, runId)).run();
58539
58838
  releaseForSession(ctx, runId);
58540
58839
  if (workflowClaimsFeatureBranch(workflow2)) {
58541
58840
  sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status3}) while it was burning`);
@@ -58578,13 +58877,14 @@ var init_runner = __esm(() => {
58578
58877
  init_review_findings();
58579
58878
  init_tickets();
58580
58879
  init_waypoints();
58880
+ init_kill_registry();
58581
58881
  init_registry2();
58582
58882
  controllers = new Map;
58583
58883
  BRANCH_CLAIMING = new Set(["ticket-burner"]);
58584
58884
  });
58585
58885
 
58586
58886
  // src/services/features.ts
58587
- import { desc as desc5, eq as eq11 } from "drizzle-orm";
58887
+ import { desc as desc5, eq as eq12 } from "drizzle-orm";
58588
58888
  import { rmSync as rmSync4 } from "fs";
58589
58889
  async function createFeature(ctx, input) {
58590
58890
  const project3 = requireProjectById(ctx, input.projectId);
@@ -58637,7 +58937,7 @@ async function startDraft(ctx, featureId, opts = {}) {
58637
58937
  const project3 = projectForFeature(ctx, feature);
58638
58938
  const base = await requestedBase(project3, opts.baseBranch);
58639
58939
  const { branchReady, baseBranch } = await ensureFeatureBranch(project3, feature.slug, base);
58640
- ctx.db.update(features).set({ status: "active", baseBranch }).where(eq11(features.id, featureId)).run();
58940
+ ctx.db.update(features).set({ status: "active", baseBranch }).where(eq12(features.id, featureId)).run();
58641
58941
  const started = { ...feature, status: "active", baseBranch };
58642
58942
  emit(ctx, featureId, {
58643
58943
  type: "feature.started",
@@ -58786,7 +59086,7 @@ function getFeatureFull(ctx, id3) {
58786
59086
  };
58787
59087
  }
58788
59088
  function list(ctx, projectId) {
58789
- const rows = ctx.db.select().from(features).where(eq11(features.projectId, projectId)).orderBy(desc5(features.createdAt)).all();
59089
+ const rows = ctx.db.select().from(features).where(eq12(features.projectId, projectId)).orderBy(desc5(features.createdAt)).all();
58790
59090
  const lastActivity = latestTsByFeature(ctx, projectId);
58791
59091
  return rows.map((row) => {
58792
59092
  const feature = rowToFeature(row);
@@ -58833,6 +59133,13 @@ function advance(ctx, featureId) {
58833
59133
  throw new GateError("feature is already at the final phase");
58834
59134
  return setPhase(ctx, featureId, next4, "phase.advanced");
58835
59135
  }
59136
+ function assertTicketsReady(ctx, feature) {
59137
+ if (feature.ticketsReadyLap === feature.lap)
59138
+ return;
59139
+ if (activeSessionsForFeature(ctx, feature.id).length === 0)
59140
+ return;
59141
+ throw new GateError("the session is still finishing the tickets \u2014 it completes the tickets phase when the lap " + "is done, and the burn arms then");
59142
+ }
58836
59143
  async function burn(ctx, featureId, opts = {}) {
58837
59144
  const feature = getFeatureRow(ctx, featureId);
58838
59145
  requireNotDraft(feature);
@@ -58855,6 +59162,12 @@ async function burn(ctx, featureId, opts = {}) {
58855
59162
  if (lapTickets.filter((t) => t.status !== "cancelled").length < 1) {
58856
59163
  throw new GateError(lapTickets.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
58857
59164
  }
59165
+ if (feature.phase === "tickets") {
59166
+ const gate = checkGate(ctx, "tickets-approved", feature);
59167
+ if (!gate.satisfied)
59168
+ throw new GateError(gate.reason ?? "gate G3 not satisfied");
59169
+ assertTicketsReady(ctx, feature);
59170
+ }
58858
59171
  if (restarting) {
58859
59172
  sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
58860
59173
  tickets2 = listByFeature2(ctx, featureId);
@@ -58904,7 +59217,7 @@ function rethink(ctx, featureId) {
58904
59217
  throw new GateError(`a ${live[0].kind} session is already live for ${feature.slug} \u2014 only one terminal per feature; end or resume it first`);
58905
59218
  }
58906
59219
  const lap = feature.lap + 1;
58907
- ctx.db.update(features).set({ lap }).where(eq11(features.id, featureId)).run();
59220
+ ctx.db.update(features).set({ lap }).where(eq12(features.id, featureId)).run();
58908
59221
  return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
58909
59222
  }
58910
59223
  async function rethinkAndLaunch(ctx, featureId, launch3) {
@@ -58913,7 +59226,7 @@ async function rethinkAndLaunch(ctx, featureId, launch3) {
58913
59226
  try {
58914
59227
  return await launch3(feature);
58915
59228
  } catch (e) {
58916
- ctx.db.update(features).set({ lap: before.lap }).where(eq11(features.id, featureId)).run();
59229
+ ctx.db.update(features).set({ lap: before.lap }).where(eq12(features.id, featureId)).run();
58917
59230
  setPhase(ctx, featureId, before.phase, "lap.aborted", `lap ${feature.lap} aborted \u2014 its terminal could not be opened (${errMsg2(e)}); back at ${before.phase} on lap ${before.lap}`);
58918
59231
  throw e;
58919
59232
  }
@@ -58992,7 +59305,7 @@ function escalateToMap(ctx, featureId, input) {
58992
59305
  return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
58993
59306
  }
58994
59307
  const project3 = projectForFeature(ctx, feature);
58995
- ctx.db.update(features).set({ mapped: true }).where(eq11(features.id, featureId)).run();
59308
+ ctx.db.update(features).set({ mapped: true }).where(eq12(features.id, featureId)).run();
58996
59309
  scaffoldMapDoc(project3, { ...feature, mapped: true }, input);
58997
59310
  emit(ctx, featureId, {
58998
59311
  type: "feature.escalated",
@@ -59010,7 +59323,7 @@ function archiveFeature(ctx, featureId) {
59010
59323
  const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
59011
59324
  if (live)
59012
59325
  endSession(ctx, live.id);
59013
- ctx.db.update(features).set({ status: "archived" }).where(eq11(features.id, featureId)).run();
59326
+ ctx.db.update(features).set({ status: "archived" }).where(eq12(features.id, featureId)).run();
59014
59327
  emit(ctx, featureId, {
59015
59328
  type: "feature.archived",
59016
59329
  message: `feature ${feature.slug} archived`,
@@ -59024,7 +59337,7 @@ function unarchiveFeature(ctx, featureId) {
59024
59337
  throw new GateError(`feature ${feature.slug} is not archived`);
59025
59338
  }
59026
59339
  const restored = feature.phase === "shipped" ? "shipped" : "active";
59027
- ctx.db.update(features).set({ status: restored }).where(eq11(features.id, featureId)).run();
59340
+ ctx.db.update(features).set({ status: restored }).where(eq12(features.id, featureId)).run();
59028
59341
  emit(ctx, featureId, {
59029
59342
  type: "feature.unarchived",
59030
59343
  message: `feature ${feature.slug} unarchived (${restored})`,
@@ -59040,7 +59353,7 @@ async function deleteFeature(ctx, featureId) {
59040
59353
  const project3 = projectForFeature(ctx, feature);
59041
59354
  for (const run7 of listRunsByFeature(ctx, featureId)) {
59042
59355
  if (run7.status === "running")
59043
- cancelRun(run7.id);
59356
+ await cancelRun(run7.id);
59044
59357
  }
59045
59358
  const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
59046
59359
  if (live)
@@ -59064,13 +59377,13 @@ async function deleteFeature(ctx, featureId) {
59064
59377
  return { ok: true, slug: feature.slug };
59065
59378
  }
59066
59379
  function deleteFeatureRows(ctx, featureId) {
59067
- ctx.db.delete(tickets).where(eq11(tickets.featureId, featureId)).run();
59068
- ctx.db.delete(sessions).where(eq11(sessions.featureId, featureId)).run();
59069
- ctx.db.delete(runs).where(eq11(runs.featureId, featureId)).run();
59070
- ctx.db.delete(events).where(eq11(events.featureId, featureId)).run();
59071
- ctx.db.delete(gateOverrides).where(eq11(gateOverrides.featureId, featureId)).run();
59072
- ctx.db.delete(waypoints).where(eq11(waypoints.featureId, featureId)).run();
59073
- ctx.db.delete(features).where(eq11(features.id, featureId)).run();
59380
+ ctx.db.delete(tickets).where(eq12(tickets.featureId, featureId)).run();
59381
+ ctx.db.delete(sessions).where(eq12(sessions.featureId, featureId)).run();
59382
+ ctx.db.delete(runs).where(eq12(runs.featureId, featureId)).run();
59383
+ ctx.db.delete(events).where(eq12(events.featureId, featureId)).run();
59384
+ ctx.db.delete(gateOverrides).where(eq12(gateOverrides.featureId, featureId)).run();
59385
+ ctx.db.delete(waypoints).where(eq12(waypoints.featureId, featureId)).run();
59386
+ ctx.db.delete(features).where(eq12(features.id, featureId)).run();
59074
59387
  }
59075
59388
  function gateState(ctx, feature) {
59076
59389
  const gate = nextGate(feature);
@@ -59081,7 +59394,7 @@ function gateState(ctx, feature) {
59081
59394
  }
59082
59395
  function uniqueSlug(ctx, projectId, title) {
59083
59396
  const base = slugify(title);
59084
- const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq11(features.projectId, projectId)).all().map((r) => r.slug));
59397
+ const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq12(features.projectId, projectId)).all().map((r) => r.slug));
59085
59398
  if (!existing.has(base))
59086
59399
  return base;
59087
59400
  let n = 2;
@@ -59114,7 +59427,7 @@ var init_features = __esm(() => {
59114
59427
  // src/services/test-notes.ts
59115
59428
  import { existsSync as existsSync19, mkdirSync as mkdirSync12, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
59116
59429
  import { dirname as dirname12 } from "path";
59117
- import { and as and9, asc as asc5, eq as eq12, lt } from "drizzle-orm";
59430
+ import { and as and10, asc as asc5, eq as eq13, lt } from "drizzle-orm";
59118
59431
  function rowToNote(row) {
59119
59432
  return TestNote.parse({
59120
59433
  id: row.id,
@@ -59133,7 +59446,7 @@ function rowToNote(row) {
59133
59446
  });
59134
59447
  }
59135
59448
  function getNote(ctx, id3) {
59136
- const row = ctx.db.select().from(testNotes).where(eq12(testNotes.id, id3)).get();
59449
+ const row = ctx.db.select().from(testNotes).where(eq13(testNotes.id, id3)).get();
59137
59450
  if (!row)
59138
59451
  throw new NotFoundError(`test note ${id3} not found`);
59139
59452
  return rowToNote(row);
@@ -59150,7 +59463,7 @@ function assertOpen(note2, action) {
59150
59463
  }
59151
59464
  }
59152
59465
  function listByFeature4(ctx, featureId) {
59153
- return ctx.db.select().from(testNotes).where(eq12(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
59466
+ return ctx.db.select().from(testNotes).where(eq13(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
59154
59467
  }
59155
59468
  function addNote(ctx, featureId, text3, author = "human", videoTimestamp, reviewTicketId) {
59156
59469
  const body = cleanText(text3);
@@ -59188,7 +59501,7 @@ function editNote(ctx, noteId, text3) {
59188
59501
  const current2 = getNote(ctx, noteId);
59189
59502
  assertOpen(current2, "edit");
59190
59503
  const body = cleanText(text3);
59191
- ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
59504
+ ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq13(testNotes.id, noteId)).run();
59192
59505
  emit(ctx, current2.featureId, {
59193
59506
  type: "note.edited",
59194
59507
  message: "note edited",
@@ -59200,7 +59513,7 @@ function editNote(ctx, noteId, text3) {
59200
59513
  function deleteNote(ctx, noteId) {
59201
59514
  const current2 = getNote(ctx, noteId);
59202
59515
  assertOpen(current2, "delete");
59203
- ctx.db.delete(testNotes).where(eq12(testNotes.id, noteId)).run();
59516
+ ctx.db.delete(testNotes).where(eq13(testNotes.id, noteId)).run();
59204
59517
  rmSync5(annotationPath(noteId), { force: true });
59205
59518
  emit(ctx, current2.featureId, {
59206
59519
  type: "note.deleted",
@@ -59227,7 +59540,7 @@ function toggleNote(ctx, noteId) {
59227
59540
  throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
59228
59541
  }
59229
59542
  const status3 = current2.status === "open" ? "done" : "open";
59230
- ctx.db.update(testNotes).set({ status: status3, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
59543
+ ctx.db.update(testNotes).set({ status: status3, updatedAt: Date.now() }).where(eq13(testNotes.id, noteId)).run();
59231
59544
  emit(ctx, current2.featureId, {
59232
59545
  type: "note.toggled",
59233
59546
  message: `note ${current2.status} \u2192 ${status3}`,
@@ -59275,7 +59588,7 @@ function freezeAsTickets(ctx, noteIds) {
59275
59588
  const tickets2 = storeTickets(ctx, featureId, selected.map((note2) => promotionTicket(feature, note2)));
59276
59589
  const now = Date.now();
59277
59590
  selected.forEach((note2, i) => {
59278
- ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(eq12(testNotes.id, note2.id)).run();
59591
+ ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(eq13(testNotes.id, note2.id)).run();
59279
59592
  });
59280
59593
  return { feature, notes: selected.map((n) => getNote(ctx, n.id)), tickets: tickets2 };
59281
59594
  }
@@ -59320,7 +59633,7 @@ function carryNotes(ctx, featureId, noteIds, lap) {
59320
59633
  if (note2.featureId !== featureId)
59321
59634
  throw new InvalidInputError("every carried note must belong to the feature");
59322
59635
  assertOpen(note2, "carry");
59323
- ctx.db.update(testNotes).set({ status: "carried", carriedLap: lap, updatedAt: Date.now() }).where(eq12(testNotes.id, note2.id)).run();
59636
+ ctx.db.update(testNotes).set({ status: "carried", carriedLap: lap, updatedAt: Date.now() }).where(eq13(testNotes.id, note2.id)).run();
59324
59637
  emit(ctx, featureId, { type: "note.carried", message: `note carried into lap ${lap}`, data: { noteId: note2.id, lap } });
59325
59638
  }
59326
59639
  renderTestNotes(ctx, getFeatureRow(ctx, featureId));
@@ -59330,7 +59643,7 @@ function reopenNote(ctx, noteId) {
59330
59643
  const note2 = getNote(ctx, noteId);
59331
59644
  if (note2.status !== "carried")
59332
59645
  throw new InvalidInputError("only a carried note can be reopened");
59333
- ctx.db.update(testNotes).set({ status: "open", carriedLap: null, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
59646
+ ctx.db.update(testNotes).set({ status: "open", carriedLap: null, updatedAt: Date.now() }).where(eq13(testNotes.id, noteId)).run();
59334
59647
  emit(ctx, note2.featureId, { type: "note.reopened", message: "note reopened", data: { noteId } });
59335
59648
  renderTestNotes(ctx, getFeatureRow(ctx, note2.featureId));
59336
59649
  return getNote(ctx, noteId);
@@ -59349,7 +59662,7 @@ function triageNotes(ctx, featureId, input) {
59349
59662
  }
59350
59663
  function triagePreview(ctx, featureId) {
59351
59664
  const feature = getFeatureRow(ctx, featureId);
59352
- const standing = ctx.db.select().from(tickets).where(and9(eq12(tickets.featureId, featureId), eq12(tickets.kind, "implementation"), eq12(tickets.status, "pending"), lt(tickets.lap, feature.lap))).all();
59665
+ const standing = ctx.db.select().from(tickets).where(and10(eq13(tickets.featureId, featureId), eq13(tickets.kind, "implementation"), eq13(tickets.status, "pending"), lt(tickets.lap, feature.lap))).all();
59353
59666
  const grouped = new Map;
59354
59667
  for (const ticket of standing)
59355
59668
  grouped.set(ticket.lap, (grouped.get(ticket.lap) ?? 0) + 1);
@@ -59359,10 +59672,19 @@ function triagePreview(ctx, featureId) {
59359
59672
  standingFixTickets: [...grouped].sort(([a], [b]) => a - b).map(([lap, count3]) => ({ count: count3, lap }))
59360
59673
  };
59361
59674
  }
59675
+ function carriedLine(note2) {
59676
+ return `- [\u2192] ${note2.text} (captured lap ${note2.lap}, carried into lap ${note2.carriedLap})${screenshotSuffix(note2)}`;
59677
+ }
59362
59678
  function renderTestNotes(ctx, feature) {
59363
59679
  const notes = listByFeature4(ctx, feature.id);
59364
59680
  const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
59365
59681
  const lines = ["# Test notes"];
59682
+ const carried = notes.filter((note2) => note2.status === "carried");
59683
+ if (carried.length) {
59684
+ lines.push("", "## Carried, still open", "");
59685
+ for (const note2 of carried)
59686
+ lines.push(carriedLine(note2));
59687
+ }
59366
59688
  for (const lap of laps) {
59367
59689
  lines.push("", `## Lap ${lap}`, "");
59368
59690
  for (const note2 of notes.filter((n) => n.lap === lap))
@@ -59442,6 +59764,8 @@ function resolveFeatureReader(ctx, caller) {
59442
59764
  }
59443
59765
  function featureContext(ctx, reader) {
59444
59766
  const feature = getFeatureRow(ctx, reader.featureId);
59767
+ const carried = carriedWork(ctx, feature.id);
59768
+ const withheldDocs = withheldFeatureDocs({ carriedNotesOpen: carried.carriedNotes > 0 });
59445
59769
  const docs2 = [];
59446
59770
  const moreDocs = [];
59447
59771
  for (const summary of listDocs(ctx, feature)) {
@@ -59455,7 +59779,7 @@ function featureContext(ctx, reader) {
59455
59779
  docs2.push({ relPath: summary.relPath, content });
59456
59780
  continue;
59457
59781
  }
59458
- const withheld = WITHHELD_FEATURE_DOCS[summary.relPath.toLowerCase()];
59782
+ const withheld = withheldDocs[summary.relPath.toLowerCase()];
59459
59783
  moreDocs.push({
59460
59784
  relPath: summary.relPath,
59461
59785
  title: summary.title,
@@ -59471,6 +59795,7 @@ function featureContext(ctx, reader) {
59471
59795
  docs: docs2,
59472
59796
  moreDocs,
59473
59797
  docsNote: DOCS_NOTE,
59798
+ openDefects: carried.openDefects,
59474
59799
  tickets: listByFeature2(ctx, feature.id).map(stripDigest),
59475
59800
  annotatedModels: annotatedModels(ctx)
59476
59801
  };
@@ -59517,9 +59842,16 @@ function toolResolveWaypoint(ctx, session, input) {
59517
59842
  frontierIds: after3.map((w) => w.id)
59518
59843
  };
59519
59844
  }
59845
+ function refuseMisKindedReview(inputs) {
59846
+ const misKinded = inputs.find((t) => REVIEW_TITLE.test(t.title) && t.kind !== "review");
59847
+ if (!misKinded)
59848
+ return;
59849
+ throw new GateError(`ticket "${misKinded.title}" is titled like a review ticket but its kind is ` + `"${misKinded.kind ?? "implementation"}" \u2014 set kind: "review" if it is the batch-closing ` + "review (almost certainly), or retitle it if it genuinely is an implementation ticket. " + "Nothing was stored; fix it and re-emit the batch.");
59850
+ }
59520
59851
  function toolEmitTickets(ctx, session, input) {
59521
59852
  const feature = getFeatureRow(ctx, requireFeatureId(session));
59522
59853
  refuseIfReadOnly(session, "emitting tickets");
59854
+ refuseMisKindedReview(input.tickets);
59523
59855
  const stored = storeTickets(ctx, feature.id, input.tickets);
59524
59856
  return {
59525
59857
  stored: stored.length,
@@ -59581,14 +59913,26 @@ function toolCompletePhase(ctx, session, input) {
59581
59913
  message: `session marked phase '${input.phase}' complete`,
59582
59914
  data: { phase: input.phase, currentPhase: feature.phase }
59583
59915
  });
59916
+ if (input.phase === "tickets" && isPastPhase(feature, "tickets")) {
59917
+ return {
59918
+ ok: true,
59919
+ nextPhase: feature.phase,
59920
+ note: "tickets phase already crossed \u2014 the burn has started; nothing left to complete"
59921
+ };
59922
+ }
59584
59923
  const gate = nextGate(feature);
59585
59924
  if (gate?.id === "G3") {
59925
+ const result = checkGate(ctx, gate.check, feature);
59926
+ if (!result.satisfied) {
59927
+ const failed = requirement(gate);
59928
+ return {
59929
+ ok: false,
59930
+ reason: result.reason ?? `gate ${gate.id} not satisfied`,
59931
+ ...failed ? { gate: failed } : {}
59932
+ };
59933
+ }
59586
59934
  const next4 = nextPhase(feature) ?? "implementation";
59587
- emit(ctx, feature.id, {
59588
- type: "tickets.awaiting_burn",
59589
- message: "tickets complete \u2014 waiting on the human Burn click (gate G3)",
59590
- data: { phase: feature.phase, nextPhase: next4, waitingOn: "human burn" }
59591
- });
59935
+ markTicketsReady(ctx, feature.id, next4);
59592
59936
  const after3 = requirement(nextGate({ ...feature, phase: next4 }));
59593
59937
  return {
59594
59938
  ok: true,
@@ -60236,7 +60580,7 @@ function buildMcpServer(audience) {
60236
60580
  if (wants("complete_phase")) {
60237
60581
  server.registerTool("complete_phase", {
60238
60582
  title: "Complete phase",
60239
- description: "Mark the named phase complete. Runs the gate check server-side and advances the " + "feature, or returns { ok: false, reason, gate } naming what the gate wanted. On " + "success it also returns `nextGate` \u2014 what the FOLLOWING gate requires \u2014 so the next " + 'step is not a guess. The tickets \u2192 implementation gate (G3) is the human "Burn" ' + "approval: completing the tickets phase records the work done and returns " + '{ ok: true, nextPhase: "implementation", waitingOn: "human burn" } WITHOUT advancing.',
60583
+ description: "Mark the named phase complete. Runs the gate check server-side and advances the " + "feature, or returns { ok: false, reason, gate } naming what the gate wanted. On " + "success it also returns `nextGate` \u2014 what the FOLLOWING gate requires \u2014 so the next " + 'step is not a guess. The tickets \u2192 implementation gate (G3) is the human "Burn" ' + "approval: completing the tickets phase records the work done and returns " + '{ ok: true, nextPhase: "implementation", waitingOn: "human burn" } WITHOUT advancing. ' + "That call is also what ARMS the Burn click \u2014 until it lands, the button waits on you " + "rather than burning tickets you are still writing, so make it the last thing you do " + "in the tickets phase. If the human burned while you were closing out, it comes back " + "{ ok: true, note } saying the phase was already crossed: nothing left to do.",
60240
60584
  inputSchema: {
60241
60585
  phase: CompletablePhase.describe("The phase you are finishing \u2014 normally the feature\u2019s current one. `shipped` is not " + "completable by any session: a feature ships when the human merges it.")
60242
60586
  }
@@ -60263,7 +60607,7 @@ async function resolveAudience(sessionId, runId) {
60263
60607
  return;
60264
60608
  }
60265
60609
  }
60266
- var DOCS_NOTE, DRAFTING_KINDS, ADRS_NOTE, FEATURE_KINDS, FEATURE_WRITE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
60610
+ var DOCS_NOTE, REVIEW_TITLE, DRAFTING_KINDS, ADRS_NOTE, FEATURE_KINDS, FEATURE_WRITE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
60267
60611
  var init_server = __esm(() => {
60268
60612
  init_src();
60269
60613
  init_paths();
@@ -60271,8 +60615,10 @@ var init_server = __esm(() => {
60271
60615
  init_artifacts();
60272
60616
  init_sessions();
60273
60617
  init_features();
60618
+ init_carried_work();
60274
60619
  init_events();
60275
60620
  init_findings();
60621
+ init_gates();
60276
60622
  init_review_findings();
60277
60623
  init_git();
60278
60624
  init_knowledge();
@@ -60281,6 +60627,7 @@ var init_server = __esm(() => {
60281
60627
  init_tickets();
60282
60628
  init_waypoints();
60283
60629
  DOCS_NOTE = "docs[] holds this feature\u2019s canonical docs in full. moreDocs[] is everything else that " + "exists in docs/features/<slug>/ \u2014 not inlined, not gone: read any of them with " + "read_feature_doc({ relPath }). A `withheld` reason means it was left out on purpose; fetch " + "it anyway if a ticket points at it.";
60630
+ REVIEW_TITLE = /^review[: ]/i;
60284
60631
  DRAFTING_KINDS = ["ideation", "revisit", "waypoint", "converge"];
60285
60632
  ADRS_NOTE = "adrs[] is an index of the project\u2019s LIVE decisions (superseded ones are already omitted), not " + "their text. Read the ones your work touches with read_adr({ relPath }) \u2014 they bind you the " + "same either way. They are also plain files in this worktree at docs/adr/.";
60286
60633
  FEATURE_KINDS = [
@@ -60404,7 +60751,7 @@ var init_ws = __esm(() => {
60404
60751
  });
60405
60752
 
60406
60753
  // src/services/prep.ts
60407
- import { and as and10, desc as desc6, eq as eq13, inArray as inArray5 } from "drizzle-orm";
60754
+ import { and as and11, desc as desc6, eq as eq14, inArray as inArray5 } from "drizzle-orm";
60408
60755
  function keysToPrepare(ctx, project3) {
60409
60756
  return unsetPreparedKeys(project3).filter((key) => isOverwritable(ctx, project3.id, key));
60410
60757
  }
@@ -60414,10 +60761,10 @@ function isPrepared(ctx, project3) {
60414
60761
  return hasCompletedProjectSession(ctx, project3.id, "prepare");
60415
60762
  }
60416
60763
  function preparedAt(ctx, projectId) {
60417
- const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and10(eq13(sessions.projectId, projectId), eq13(sessions.kind, "prepare"))).all().map((r) => r.id));
60764
+ const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and11(eq14(sessions.projectId, projectId), eq14(sessions.kind, "prepare"))).all().map((r) => r.id));
60418
60765
  if (prepareSessions.size === 0)
60419
60766
  return null;
60420
- const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and10(eq13(events.projectId, projectId), inArray5(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc6(events.ts)).all();
60767
+ const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and11(eq14(events.projectId, projectId), inArray5(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc6(events.ts)).all();
60421
60768
  for (const end5 of ends) {
60422
60769
  const sessionId = end5.data?.sessionId;
60423
60770
  if (sessionId && prepareSessions.has(sessionId))
@@ -61175,7 +61522,7 @@ var init_events2 = __esm(() => {
61175
61522
  // src/launcher/launcher.ts
61176
61523
  import { existsSync as existsSync23 } from "fs";
61177
61524
  import { join as join23 } from "path";
61178
- import { and as and11, eq as eq14 } from "drizzle-orm";
61525
+ import { and as and12, eq as eq15 } from "drizzle-orm";
61179
61526
  function ptyExitMessage(exitCode) {
61180
61527
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
61181
61528
  return `terminal exited (code ${label})`;
@@ -61195,7 +61542,7 @@ function renderCommand(runtime5, spec) {
61195
61542
  return [...env, runtime5.binary, ...spec.argv].join(" ");
61196
61543
  }
61197
61544
  function activeRunFor(ctx, featureId) {
61198
- const row = ctx.db.select().from(runs).where(and11(eq14(runs.featureId, featureId), eq14(runs.status, "running"))).limit(1).get();
61545
+ const row = ctx.db.select().from(runs).where(and12(eq15(runs.featureId, featureId), eq15(runs.status, "running"))).limit(1).get();
61199
61546
  return row ? rowToRun(row) : null;
61200
61547
  }
61201
61548
  function assertSpawnable(ctx, feature, excludeSessionId) {
@@ -61276,6 +61623,7 @@ async function launchSession(ctx, input, opts = {}) {
61276
61623
  worktreePath,
61277
61624
  model
61278
61625
  });
61626
+ const carried = carriedWork(ctx, feature.id);
61279
61627
  const plan = planKickoff({
61280
61628
  kind: input.kind,
61281
61629
  lap: feature.lap,
@@ -61284,7 +61632,8 @@ async function launchSession(ctx, input, opts = {}) {
61284
61632
  lap: feature.lap,
61285
61633
  phase: feature.phase,
61286
61634
  ticketLaps: listByFeature2(ctx, feature.id).map((t2) => t2.lap)
61287
- })
61635
+ }),
61636
+ carried
61288
61637
  });
61289
61638
  let waypoint;
61290
61639
  let resumeSessionId;
@@ -61381,6 +61730,7 @@ async function launchSession(ctx, input, opts = {}) {
61381
61730
  config: ctx.config,
61382
61731
  waypoint,
61383
61732
  lap: plan.lap,
61733
+ carried: plan.lap === undefined ? undefined : carried,
61384
61734
  purpose: input.purpose,
61385
61735
  worktreePath,
61386
61736
  serverUrl: serverUrlFor(ctx.config),
@@ -61802,6 +62152,7 @@ var init_launcher = __esm(() => {
61802
62152
  init_errors();
61803
62153
  init_end_session();
61804
62154
  init_registry();
62155
+ init_carried_work();
61805
62156
  init_docs_watch();
61806
62157
  init_events();
61807
62158
  init_gates();
@@ -61968,6 +62319,7 @@ var init_feature = __esm(() => {
61968
62319
  init_src();
61969
62320
  init_launcher();
61970
62321
  init_sessions();
62322
+ init_carried_work();
61971
62323
  init_events();
61972
62324
  init_features();
61973
62325
  init_gates();
@@ -62007,7 +62359,7 @@ var init_feature = __esm(() => {
62007
62359
  rethink: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => rethinkAndLaunch(ctx, input.featureId, (feature) => launchSession(ctx, {
62008
62360
  featureId: input.featureId,
62009
62361
  kind: "revisit",
62010
- kickoffLine: lapKickoff(feature.lap)
62362
+ kickoffLine: lapKickoff(feature.lap, carriedWork(ctx, input.featureId))
62011
62363
  }))),
62012
62364
  resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
62013
62365
  endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
@@ -62398,7 +62750,7 @@ var init_conversations = __esm(() => {
62398
62750
  // src/services/projects.ts
62399
62751
  import { existsSync as existsSync25 } from "fs";
62400
62752
  import { basename as basename4, join as join27 } from "path";
62401
- import { eq as eq15, isNull } from "drizzle-orm";
62753
+ import { eq as eq16, isNull } from "drizzle-orm";
62402
62754
  function listProjects(ctx) {
62403
62755
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
62404
62756
  }
@@ -62419,7 +62771,7 @@ async function openProject(ctx, rawPath) {
62419
62771
  const name = basename4(repoPath) || repoPath;
62420
62772
  const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
62421
62773
  if (existing) {
62422
- ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq15(projects.id, existing.id)).run();
62774
+ ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq16(projects.id, existing.id)).run();
62423
62775
  const reopened = { ...existing, repoPath, closedAt: undefined };
62424
62776
  emitProject(ctx, existing.id, {
62425
62777
  type: "project.opened",
@@ -62449,7 +62801,7 @@ function closeProject(ctx, projectId) {
62449
62801
  if (projectHasActiveRun(ctx, projectId)) {
62450
62802
  throw new InvalidInputError(`cannot close ${project3.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
62451
62803
  }
62452
- ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq15(projects.id, projectId)).run();
62804
+ ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq16(projects.id, projectId)).run();
62453
62805
  emitProject(ctx, projectId, {
62454
62806
  type: "project.closed",
62455
62807
  message: `project ${project3.name} closed`
@@ -62458,7 +62810,7 @@ function closeProject(ctx, projectId) {
62458
62810
  }
62459
62811
  function renameProject(ctx, projectId, name) {
62460
62812
  const project3 = requireProjectById(ctx, projectId);
62461
- ctx.db.update(projects).set({ name }).where(eq15(projects.id, projectId)).run();
62813
+ ctx.db.update(projects).set({ name }).where(eq16(projects.id, projectId)).run();
62462
62814
  emitProject(ctx, projectId, {
62463
62815
  type: "project.renamed",
62464
62816
  message: `project renamed ${project3.name} \u2192 ${name}`,
@@ -62550,9 +62902,9 @@ var init_review_findings2 = __esm(() => {
62550
62902
  });
62551
62903
 
62552
62904
  // src/services/runs.ts
62553
- import { and as and12, asc as asc6, eq as eq16, isNotNull as isNotNull3 } from "drizzle-orm";
62905
+ import { and as and13, asc as asc6, eq as eq17, isNotNull as isNotNull3 } from "drizzle-orm";
62554
62906
  function runTicketIds(ctx, runId) {
62555
- const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(and12(eq16(events.runId, runId), isNotNull3(events.ticketId))).orderBy(asc6(events.id)).all();
62907
+ const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(and13(eq17(events.runId, runId), isNotNull3(events.ticketId))).orderBy(asc6(events.id)).all();
62556
62908
  const seen = new Set;
62557
62909
  for (const row of rows)
62558
62910
  if (row.ticketId)
@@ -62560,7 +62912,7 @@ function runTicketIds(ctx, runId) {
62560
62912
  return [...seen];
62561
62913
  }
62562
62914
  function runLap(ctx, runId) {
62563
- const row = ctx.db.select({ lap: events.lap }).from(events).where(eq16(events.runId, runId)).orderBy(asc6(events.id)).limit(1).get();
62915
+ const row = ctx.db.select({ lap: events.lap }).from(events).where(eq17(events.runId, runId)).orderBy(asc6(events.id)).limit(1).get();
62564
62916
  return row?.lap ?? 1;
62565
62917
  }
62566
62918
  function listRunSummaries(ctx, featureId) {
@@ -62588,6 +62940,7 @@ import * as z10 from "zod";
62588
62940
  var runRouter;
62589
62941
  var init_run = __esm(() => {
62590
62942
  init_agent_stream();
62943
+ init_events();
62591
62944
  init_repo();
62592
62945
  init_runs();
62593
62946
  init_runner();
@@ -62596,10 +62949,17 @@ var init_run = __esm(() => {
62596
62949
  get: publicProcedure.input(z10.object({ runId: z10.string() })).query(({ ctx, input }) => getRunWithTickets(ctx, input.runId)),
62597
62950
  listByFeature: publicProcedure.input(z10.object({ featureId: z10.string() })).query(({ ctx, input }) => listRunSummaries(ctx, input.featureId)),
62598
62951
  agentTranscript: publicProcedure.input(z10.object({ ticketId: z10.string(), after: z10.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
62599
- cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(({ ctx, input }) => {
62600
- getRunRow(ctx, input.runId);
62601
- cancelRun(input.runId);
62602
- return { ok: true };
62952
+ cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(async ({ ctx, input }) => {
62953
+ const run7 = getRunRow(ctx, input.runId);
62954
+ const { confirmed } = await cancelRun(input.runId);
62955
+ if (!confirmed) {
62956
+ emit(ctx, run7.featureId, {
62957
+ type: "run.cancel_timeout",
62958
+ message: "cancel timed out \u2014 an agent of this run may still be running",
62959
+ runId: run7.id
62960
+ });
62961
+ }
62962
+ return { ok: true, confirmed };
62603
62963
  })
62604
62964
  });
62605
62965
  });
@@ -62777,8 +63137,17 @@ var init_test_notes2 = __esm(() => {
62777
63137
 
62778
63138
  // src/trpc/routers/ticket.ts
62779
63139
  import * as z15 from "zod";
63140
+ function noteStopTimeout(ctx, ticketId) {
63141
+ const ticket = getTicket(ctx, ticketId);
63142
+ emit(ctx, ticket.featureId, {
63143
+ type: "ticket.stop_timeout",
63144
+ message: `ticket #${ticket.seq}: stop timed out \u2014 the process may still be running`,
63145
+ ticketId: ticket.id
63146
+ });
63147
+ }
62780
63148
  var ticketRouter;
62781
63149
  var init_ticket = __esm(() => {
63150
+ init_events();
62782
63151
  init_features();
62783
63152
  init_repo();
62784
63153
  init_tickets();
@@ -62786,17 +63155,27 @@ var init_ticket = __esm(() => {
62786
63155
  init_context();
62787
63156
  ticketRouter = router({
62788
63157
  retry: publicProcedure.input(z15.object({ ticketId: z15.string(), fresh: z15.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
62789
- stop: publicProcedure.input(z15.object({ ticketId: z15.string() })).mutation(({ ctx, input }) => {
62790
- if (stopTicketRun(input.ticketId))
62791
- return { stopped: true, swept: false };
63158
+ stop: publicProcedure.input(z15.object({ ticketId: z15.string() })).mutation(async ({ ctx, input }) => {
63159
+ const { stopped, confirmed } = await stopTicketRun(input.ticketId);
63160
+ if (stopped) {
63161
+ if (!confirmed)
63162
+ noteStopTimeout(ctx, input.ticketId);
63163
+ return { stopped: true, swept: false, confirmed };
63164
+ }
62792
63165
  const ticket = getTicket(ctx, input.ticketId);
62793
63166
  if (ticket.status === "burning" && !hasActiveRun(ctx, ticket.featureId)) {
62794
63167
  sweepOrphanedBurning(ctx, ticket.featureId, "orphaned \u2014 the run that was burning it is gone");
62795
- return { stopped: false, swept: true };
63168
+ return { stopped: false, swept: true, confirmed };
62796
63169
  }
62797
- return { stopped: false, swept: false };
63170
+ return { stopped: false, swept: false, confirmed };
63171
+ }),
63172
+ cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(async ({ ctx, input }) => {
63173
+ const { stopped, confirmed } = await stopTicketRun(input.ticketId);
63174
+ if (stopped && !confirmed)
63175
+ noteStopTimeout(ctx, input.ticketId);
63176
+ const ticket = cancelTicket(ctx, input.ticketId, input.reason, { agentStopped: stopped });
63177
+ return { ticket, stopped, confirmed };
62798
63178
  }),
62799
- cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
62800
63179
  durationStats: publicProcedure.input(z15.object({ projectId: z15.string() })).query(({ ctx, input }) => ticketDurationStats(ctx, input.projectId)),
62801
63180
  edit: publicProcedure.input(z15.object({
62802
63181
  ticketId: z15.string(),
@@ -62843,14 +63222,14 @@ var init_router = __esm(() => {
62843
63222
  });
62844
63223
 
62845
63224
  // src/workflows/reconcile-runs.ts
62846
- import { eq as eq17 } from "drizzle-orm";
63225
+ import { eq as eq18 } from "drizzle-orm";
62847
63226
  async function reconcileStaleRuns(ctx) {
62848
- const stale = ctx.db.select().from(runs).where(eq17(runs.status, "running")).all().map(rowToRun);
63227
+ const stale = ctx.db.select().from(runs).where(eq18(runs.status, "running")).all().map(rowToRun);
62849
63228
  const reconciled = [];
62850
63229
  for (const run7 of stale) {
62851
63230
  if (isRunActive(run7.id))
62852
63231
  continue;
62853
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq17(runs.id, run7.id)).run();
63232
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq18(runs.id, run7.id)).run();
62854
63233
  const released = releaseForSession(ctx, run7.id);
62855
63234
  const swept = workflowClaimsFeatureBranch(run7.workflow) ? sweepOrphanedBurning(ctx, run7.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
62856
63235
  if (workflowClaimsFeatureBranch(run7.workflow)) {
@@ -63064,10 +63443,10 @@ function resolveDoctorEnv() {
63064
63443
  ...runtimes ? { runtimes } : {}
63065
63444
  };
63066
63445
  }
63067
- async function runCli(argv, log4 = console.log) {
63446
+ async function runCli(argv, log5 = console.log) {
63068
63447
  const mode = parseMode(argv);
63069
63448
  const report = await runDoctor(resolveDoctorEnv());
63070
- log4(formatReport(report, mode));
63449
+ log5(formatReport(report, mode));
63071
63450
  return exitCodeFor(report, mode);
63072
63451
  }
63073
63452
  var init_cli = __esm(() => {