runcastle 1.2.0 → 1.2.2

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.
@@ -127,6 +127,13 @@
127
127
  "when": 1785745839500,
128
128
  "tag": "0017_certain_ezekiel_stane",
129
129
  "breakpoints": true
130
+ },
131
+ {
132
+ "idx": 18,
133
+ "version": "6",
134
+ "when": 1786021663891,
135
+ "tag": "0018_nice_lorna_dane",
136
+ "breakpoints": true
130
137
  }
131
138
  ]
132
139
  }
package/index.js CHANGED
@@ -245,7 +245,7 @@ var init_ids = () => {};
245
245
 
246
246
  // ../core/src/schemas.ts
247
247
  import * as z2 from "zod";
248
- var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
248
+ var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
249
249
  var init_schemas = __esm(() => {
250
250
  Phase = z2.enum([
251
251
  "ideation",
@@ -307,6 +307,17 @@ var init_schemas = __esm(() => {
307
307
  lastSessionId: z2.string().optional(),
308
308
  summary: z2.string().optional()
309
309
  });
310
+ TestNoteStatus = z2.enum(["open", "done", "promoted"]);
311
+ TestNote = z2.object({
312
+ id: z2.string(),
313
+ featureId: z2.string(),
314
+ lap: z2.number(),
315
+ text: z2.string(),
316
+ status: TestNoteStatus,
317
+ ticketId: z2.string().optional(),
318
+ createdAt: z2.number(),
319
+ updatedAt: z2.number()
320
+ });
310
321
  PREPARED_KEYS = [
311
322
  "setupCommand",
312
323
  "verifyCommands",
@@ -559,7 +570,7 @@ var init_pipeline = __esm(() => {
559
570
 
560
571
  // ../core/src/db-schema.ts
561
572
  import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
562
- var projects, projectFindings, features, sessions, tickets, waypoints, runs, events, gateOverrides;
573
+ var projects, projectFindings, features, sessions, tickets, testNotes, waypoints, runs, events, gateOverrides;
563
574
  var init_db_schema = __esm(() => {
564
575
  projects = sqliteTable("projects", {
565
576
  id: text("id").primaryKey(),
@@ -631,6 +642,16 @@ var init_db_schema = __esm(() => {
631
642
  attemptBranch: text("attempt_branch"),
632
643
  conflictFiles: text("conflict_files", { mode: "json" }).$type()
633
644
  });
645
+ testNotes = sqliteTable("test_notes", {
646
+ id: text("id").primaryKey(),
647
+ featureId: text("feature_id").notNull(),
648
+ lap: integer("lap").notNull(),
649
+ text: text("text").notNull(),
650
+ status: text("status").notNull().$type(),
651
+ ticketId: text("ticket_id"),
652
+ createdAt: integer("created_at").notNull(),
653
+ updatedAt: integer("updated_at").notNull()
654
+ });
634
655
  waypoints = sqliteTable("waypoints", {
635
656
  id: text("id").primaryKey(),
636
657
  featureId: text("feature_id").notNull(),
@@ -693,6 +714,7 @@ var init_schema = __esm(() => {
693
714
  features,
694
715
  sessions,
695
716
  tickets,
717
+ testNotes,
696
718
  waypoints,
697
719
  runs,
698
720
  events,
@@ -1773,6 +1795,7 @@ var init_waypoints = __esm(() => {
1773
1795
  });
1774
1796
 
1775
1797
  // src/pty/dev-pane.ts
1798
+ import { execFileSync } from "child_process";
1776
1799
  function drivePaneId(ownerId) {
1777
1800
  return `${DRIVE_PREFIX}${ownerId}`;
1778
1801
  }
@@ -1832,14 +1855,20 @@ function devPaneLive(paneId) {
1832
1855
  const entry = ptyRegistry().get(paneId);
1833
1856
  return !!entry && !entry.exited;
1834
1857
  }
1858
+ function killProcessTree(pid) {
1859
+ try {
1860
+ if (process.platform === "win32") {
1861
+ execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1862
+ } else {
1863
+ process.kill(-pid, "SIGTERM");
1864
+ }
1865
+ } catch {}
1866
+ }
1835
1867
  function stopDevPane(paneId) {
1836
1868
  const reg = ptyRegistry();
1837
1869
  const entry = reg.get(paneId);
1838
- if (entry && !entry.exited && process.platform !== "win32") {
1839
- try {
1840
- process.kill(-entry.pty.pid, "SIGTERM");
1841
- } catch {}
1842
- }
1870
+ if (entry && !entry.exited)
1871
+ killProcessTree(entry.pty.pid);
1843
1872
  reg.kill(paneId);
1844
1873
  reg.remove(paneId);
1845
1874
  }
@@ -9345,8 +9374,188 @@ var init_system = __esm(() => {
9345
9374
  });
9346
9375
  });
9347
9376
 
9348
- // src/trpc/routers/ticket.ts
9377
+ // src/services/test-notes.ts
9378
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9379
+ import { dirname as dirname10 } from "path";
9380
+ import { asc as asc4, eq as eq14 } from "drizzle-orm";
9381
+ function rowToNote(row) {
9382
+ return {
9383
+ id: row.id,
9384
+ featureId: row.featureId,
9385
+ lap: row.lap,
9386
+ text: row.text,
9387
+ status: row.status,
9388
+ ticketId: row.ticketId ?? undefined,
9389
+ createdAt: row.createdAt,
9390
+ updatedAt: row.updatedAt
9391
+ };
9392
+ }
9393
+ function getNote(ctx, id) {
9394
+ const row = ctx.db.select().from(testNotes).where(eq14(testNotes.id, id)).get();
9395
+ if (!row)
9396
+ throw new NotFoundError(`test note ${id} not found`);
9397
+ return rowToNote(row);
9398
+ }
9399
+ function cleanText(text2) {
9400
+ const trimmed = text2.trim();
9401
+ if (!trimmed)
9402
+ throw new InvalidInputError("note text cannot be empty");
9403
+ return trimmed;
9404
+ }
9405
+ function assertOpen(note, action) {
9406
+ if (note.status !== "open") {
9407
+ throw new InvalidInputError(`cannot ${action} note ${note.id} \u2014 it is ${note.status}; only open notes can be changed`);
9408
+ }
9409
+ }
9410
+ function listByFeature3(ctx, featureId) {
9411
+ return ctx.db.select().from(testNotes).where(eq14(testNotes.featureId, featureId)).orderBy(asc4(testNotes.createdAt), asc4(testNotes.id)).all().map(rowToNote);
9412
+ }
9413
+ function addNote(ctx, featureId, text2) {
9414
+ const body = cleanText(text2);
9415
+ const feature = getFeatureRow(ctx, featureId);
9416
+ const now = Date.now();
9417
+ const row = ctx.db.insert(testNotes).values({
9418
+ id: newId("note"),
9419
+ featureId,
9420
+ lap: feature.lap,
9421
+ text: body,
9422
+ status: "open",
9423
+ ticketId: null,
9424
+ createdAt: now,
9425
+ updatedAt: now
9426
+ }).returning().get();
9427
+ const note = rowToNote(row);
9428
+ emit(ctx, featureId, {
9429
+ type: "note.added",
9430
+ message: `note captured on lap ${note.lap}`,
9431
+ data: { noteId: note.id, lap: note.lap }
9432
+ });
9433
+ renderTestNotes(ctx, feature);
9434
+ return note;
9435
+ }
9436
+ function editNote(ctx, noteId, text2) {
9437
+ const current2 = getNote(ctx, noteId);
9438
+ assertOpen(current2, "edit");
9439
+ const body = cleanText(text2);
9440
+ ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9441
+ emit(ctx, current2.featureId, {
9442
+ type: "note.edited",
9443
+ message: "note edited",
9444
+ data: { noteId }
9445
+ });
9446
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9447
+ return getNote(ctx, noteId);
9448
+ }
9449
+ function deleteNote(ctx, noteId) {
9450
+ const current2 = getNote(ctx, noteId);
9451
+ assertOpen(current2, "delete");
9452
+ ctx.db.delete(testNotes).where(eq14(testNotes.id, noteId)).run();
9453
+ emit(ctx, current2.featureId, {
9454
+ type: "note.deleted",
9455
+ message: "note deleted",
9456
+ data: { noteId }
9457
+ });
9458
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9459
+ }
9460
+ function toggleNote(ctx, noteId) {
9461
+ const current2 = getNote(ctx, noteId);
9462
+ if (current2.status === "promoted") {
9463
+ throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
9464
+ }
9465
+ const status = current2.status === "open" ? "done" : "open";
9466
+ ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9467
+ emit(ctx, current2.featureId, {
9468
+ type: "note.toggled",
9469
+ message: `note ${current2.status} \u2192 ${status}`,
9470
+ data: { noteId, from: current2.status, to: status }
9471
+ });
9472
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9473
+ return getNote(ctx, noteId);
9474
+ }
9475
+ function promotionTicket(feature, note) {
9476
+ const firstLine = note.text.split(`
9477
+ `)[0].trim();
9478
+ const docs = featureDocsRel(feature.slug);
9479
+ return {
9480
+ title: firstLine.length <= TITLE_MAX ? firstLine : `${firstLine.slice(0, TITLE_MAX - 1).trimEnd()}\u2026`,
9481
+ goal: note.text,
9482
+ context: [
9483
+ `Found during lap ${note.lap} test drive of ${feature.slug}.`,
9484
+ `Read ${docs}/spec.md and ${docs}/decisions.md for what this feature is meant to do.`
9485
+ ].join(`
9486
+
9487
+ `),
9488
+ acceptanceCriteria: [`The noted behavior no longer reproduces: ${note.text}`],
9489
+ seams: [],
9490
+ blockedBy: []
9491
+ };
9492
+ }
9493
+ function promoteNote(ctx, noteId) {
9494
+ const current2 = getNote(ctx, noteId);
9495
+ assertOpen(current2, "promote");
9496
+ const feature = getFeatureRow(ctx, current2.featureId);
9497
+ const [ticket] = storeTickets(ctx, current2.featureId, [promotionTicket(feature, current2)]);
9498
+ ctx.db.update(testNotes).set({ status: "promoted", ticketId: ticket.id, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9499
+ emit(ctx, current2.featureId, {
9500
+ type: "note.promoted",
9501
+ message: `note promoted to ticket ${ticket.seq}`,
9502
+ ticketId: ticket.id,
9503
+ data: { noteId, seq: ticket.seq }
9504
+ });
9505
+ renderTestNotes(ctx, feature);
9506
+ return { note: getNote(ctx, noteId), ticket };
9507
+ }
9508
+ function noteLine(ctx, note) {
9509
+ if (note.status === "open")
9510
+ return `- [ ] ${note.text}`;
9511
+ const ticket = note.ticketId ? ` (\u2192 ticket ${getTicket(ctx, note.ticketId).seq})` : "";
9512
+ return `- [x] ${note.text}${ticket}`;
9513
+ }
9514
+ function renderTestNotes(ctx, feature) {
9515
+ const notes = listByFeature3(ctx, feature.id);
9516
+ const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
9517
+ const lines = ["# Test notes"];
9518
+ for (const lap of laps) {
9519
+ lines.push("", `## Lap ${lap}`, "");
9520
+ for (const note of notes.filter((n) => n.lap === lap))
9521
+ lines.push(noteLine(ctx, note));
9522
+ }
9523
+ const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9524
+ mkdirSync9(dirname10(path), { recursive: true });
9525
+ writeFileSync5(path, `${lines.join(`
9526
+ `)}
9527
+ `, "utf8");
9528
+ }
9529
+ var TITLE_MAX = 60;
9530
+ var init_test_notes = __esm(() => {
9531
+ init_src();
9532
+ init_paths();
9533
+ init_schema();
9534
+ init_errors();
9535
+ init_events();
9536
+ init_feature_docs();
9537
+ init_repo();
9538
+ init_tickets();
9539
+ });
9540
+
9541
+ // src/trpc/routers/test-notes.ts
9349
9542
  import * as z12 from "zod";
9543
+ var testNotesRouter;
9544
+ var init_test_notes2 = __esm(() => {
9545
+ init_test_notes();
9546
+ init_context();
9547
+ testNotesRouter = router({
9548
+ list: publicProcedure.input(z12.object({ featureId: z12.string() })).query(({ ctx, input }) => listByFeature3(ctx, input.featureId)),
9549
+ add: publicProcedure.input(z12.object({ featureId: z12.string(), text: z12.string().min(1) })).mutation(({ ctx, input }) => addNote(ctx, input.featureId, input.text)),
9550
+ edit: publicProcedure.input(z12.object({ noteId: z12.string(), text: z12.string().min(1) })).mutation(({ ctx, input }) => editNote(ctx, input.noteId, input.text)),
9551
+ remove: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => deleteNote(ctx, input.noteId)),
9552
+ toggle: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => toggleNote(ctx, input.noteId)),
9553
+ promote: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => promoteNote(ctx, input.noteId))
9554
+ });
9555
+ });
9556
+
9557
+ // src/trpc/routers/ticket.ts
9558
+ import * as z13 from "zod";
9350
9559
  var ticketRouter;
9351
9560
  var init_ticket = __esm(() => {
9352
9561
  init_features();
@@ -9355,8 +9564,8 @@ var init_ticket = __esm(() => {
9355
9564
  init_ticket_burner();
9356
9565
  init_context();
9357
9566
  ticketRouter = router({
9358
- retry: publicProcedure.input(z12.object({ ticketId: z12.string(), fresh: z12.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
9359
- stop: publicProcedure.input(z12.object({ ticketId: z12.string() })).mutation(({ ctx, input }) => {
9567
+ retry: publicProcedure.input(z13.object({ ticketId: z13.string(), fresh: z13.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
9568
+ stop: publicProcedure.input(z13.object({ ticketId: z13.string() })).mutation(({ ctx, input }) => {
9360
9569
  if (stopTicketRun(input.ticketId))
9361
9570
  return { stopped: true, swept: false };
9362
9571
  const ticket = getTicket(ctx, input.ticketId);
@@ -9366,13 +9575,13 @@ var init_ticket = __esm(() => {
9366
9575
  }
9367
9576
  return { stopped: false, swept: false };
9368
9577
  }),
9369
- cancel: publicProcedure.input(z12.object({ ticketId: z12.string(), reason: z12.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
9370
- edit: publicProcedure.input(z12.object({
9371
- ticketId: z12.string(),
9372
- title: z12.string().min(1).optional(),
9373
- goal: z12.string().min(1).optional(),
9374
- context: z12.string().optional(),
9375
- acceptanceCriteria: z12.array(z12.string().min(1)).optional()
9578
+ cancel: publicProcedure.input(z13.object({ ticketId: z13.string(), reason: z13.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
9579
+ edit: publicProcedure.input(z13.object({
9580
+ ticketId: z13.string(),
9581
+ title: z13.string().min(1).optional(),
9582
+ goal: z13.string().min(1).optional(),
9583
+ context: z13.string().optional(),
9584
+ acceptanceCriteria: z13.array(z13.string().min(1)).optional()
9376
9585
  })).mutation(({ ctx, input }) => {
9377
9586
  const { ticketId, ...patch } = input;
9378
9587
  return editTicket(ctx, ticketId, patch);
@@ -9392,12 +9601,14 @@ var init_router = __esm(() => {
9392
9601
  init_settings2();
9393
9602
  init_setup2();
9394
9603
  init_system();
9604
+ init_test_notes2();
9395
9605
  init_ticket();
9396
9606
  appRouter = router({
9397
9607
  project: projectRouter,
9398
9608
  feature: featureRouter,
9399
9609
  run: runRouter,
9400
9610
  ticket: ticketRouter,
9611
+ notes: testNotesRouter,
9401
9612
  events: eventsRouter,
9402
9613
  docs: docsRouter,
9403
9614
  settings: settingsRouter,
@@ -9407,14 +9618,14 @@ var init_router = __esm(() => {
9407
9618
  });
9408
9619
 
9409
9620
  // src/workflows/reconcile-runs.ts
9410
- import { eq as eq14 } from "drizzle-orm";
9621
+ import { eq as eq15 } from "drizzle-orm";
9411
9622
  async function reconcileStaleRuns(ctx) {
9412
- const stale = ctx.db.select().from(runs).where(eq14(runs.status, "running")).all().map(rowToRun);
9623
+ const stale = ctx.db.select().from(runs).where(eq15(runs.status, "running")).all().map(rowToRun);
9413
9624
  const reconciled = [];
9414
9625
  for (const run3 of stale) {
9415
9626
  if (isRunActive(run3.id))
9416
9627
  continue;
9417
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq14(runs.id, run3.id)).run();
9628
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq15(runs.id, run3.id)).run();
9418
9629
  const released = releaseForSession(ctx, run3.id);
9419
9630
  const swept = workflowClaimsFeatureBranch(run3.workflow) ? sweepOrphanedBurning(ctx, run3.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
9420
9631
  if (workflowClaimsFeatureBranch(run3.workflow)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",