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