runcastle 1.2.0 → 1.2.1

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,
@@ -9345,8 +9367,188 @@ var init_system = __esm(() => {
9345
9367
  });
9346
9368
  });
9347
9369
 
9348
- // src/trpc/routers/ticket.ts
9370
+ // src/services/test-notes.ts
9371
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9372
+ import { dirname as dirname10 } from "path";
9373
+ import { asc as asc4, eq as eq14 } from "drizzle-orm";
9374
+ function rowToNote(row) {
9375
+ return {
9376
+ id: row.id,
9377
+ featureId: row.featureId,
9378
+ lap: row.lap,
9379
+ text: row.text,
9380
+ status: row.status,
9381
+ ticketId: row.ticketId ?? undefined,
9382
+ createdAt: row.createdAt,
9383
+ updatedAt: row.updatedAt
9384
+ };
9385
+ }
9386
+ function getNote(ctx, id) {
9387
+ const row = ctx.db.select().from(testNotes).where(eq14(testNotes.id, id)).get();
9388
+ if (!row)
9389
+ throw new NotFoundError(`test note ${id} not found`);
9390
+ return rowToNote(row);
9391
+ }
9392
+ function cleanText(text2) {
9393
+ const trimmed = text2.trim();
9394
+ if (!trimmed)
9395
+ throw new InvalidInputError("note text cannot be empty");
9396
+ return trimmed;
9397
+ }
9398
+ function assertOpen(note, action) {
9399
+ if (note.status !== "open") {
9400
+ throw new InvalidInputError(`cannot ${action} note ${note.id} \u2014 it is ${note.status}; only open notes can be changed`);
9401
+ }
9402
+ }
9403
+ function listByFeature3(ctx, featureId) {
9404
+ return ctx.db.select().from(testNotes).where(eq14(testNotes.featureId, featureId)).orderBy(asc4(testNotes.createdAt), asc4(testNotes.id)).all().map(rowToNote);
9405
+ }
9406
+ function addNote(ctx, featureId, text2) {
9407
+ const body = cleanText(text2);
9408
+ const feature = getFeatureRow(ctx, featureId);
9409
+ const now = Date.now();
9410
+ const row = ctx.db.insert(testNotes).values({
9411
+ id: newId("note"),
9412
+ featureId,
9413
+ lap: feature.lap,
9414
+ text: body,
9415
+ status: "open",
9416
+ ticketId: null,
9417
+ createdAt: now,
9418
+ updatedAt: now
9419
+ }).returning().get();
9420
+ const note = rowToNote(row);
9421
+ emit(ctx, featureId, {
9422
+ type: "note.added",
9423
+ message: `note captured on lap ${note.lap}`,
9424
+ data: { noteId: note.id, lap: note.lap }
9425
+ });
9426
+ renderTestNotes(ctx, feature);
9427
+ return note;
9428
+ }
9429
+ function editNote(ctx, noteId, text2) {
9430
+ const current2 = getNote(ctx, noteId);
9431
+ assertOpen(current2, "edit");
9432
+ const body = cleanText(text2);
9433
+ ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9434
+ emit(ctx, current2.featureId, {
9435
+ type: "note.edited",
9436
+ message: "note edited",
9437
+ data: { noteId }
9438
+ });
9439
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9440
+ return getNote(ctx, noteId);
9441
+ }
9442
+ function deleteNote(ctx, noteId) {
9443
+ const current2 = getNote(ctx, noteId);
9444
+ assertOpen(current2, "delete");
9445
+ ctx.db.delete(testNotes).where(eq14(testNotes.id, noteId)).run();
9446
+ emit(ctx, current2.featureId, {
9447
+ type: "note.deleted",
9448
+ message: "note deleted",
9449
+ data: { noteId }
9450
+ });
9451
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9452
+ }
9453
+ function toggleNote(ctx, noteId) {
9454
+ const current2 = getNote(ctx, noteId);
9455
+ if (current2.status === "promoted") {
9456
+ throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
9457
+ }
9458
+ const status = current2.status === "open" ? "done" : "open";
9459
+ ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9460
+ emit(ctx, current2.featureId, {
9461
+ type: "note.toggled",
9462
+ message: `note ${current2.status} \u2192 ${status}`,
9463
+ data: { noteId, from: current2.status, to: status }
9464
+ });
9465
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9466
+ return getNote(ctx, noteId);
9467
+ }
9468
+ function promotionTicket(feature, note) {
9469
+ const firstLine = note.text.split(`
9470
+ `)[0].trim();
9471
+ const docs = featureDocsRel(feature.slug);
9472
+ return {
9473
+ title: firstLine.length <= TITLE_MAX ? firstLine : `${firstLine.slice(0, TITLE_MAX - 1).trimEnd()}\u2026`,
9474
+ goal: note.text,
9475
+ context: [
9476
+ `Found during lap ${note.lap} test drive of ${feature.slug}.`,
9477
+ `Read ${docs}/spec.md and ${docs}/decisions.md for what this feature is meant to do.`
9478
+ ].join(`
9479
+
9480
+ `),
9481
+ acceptanceCriteria: [`The noted behavior no longer reproduces: ${note.text}`],
9482
+ seams: [],
9483
+ blockedBy: []
9484
+ };
9485
+ }
9486
+ function promoteNote(ctx, noteId) {
9487
+ const current2 = getNote(ctx, noteId);
9488
+ assertOpen(current2, "promote");
9489
+ const feature = getFeatureRow(ctx, current2.featureId);
9490
+ const [ticket] = storeTickets(ctx, current2.featureId, [promotionTicket(feature, current2)]);
9491
+ ctx.db.update(testNotes).set({ status: "promoted", ticketId: ticket.id, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9492
+ emit(ctx, current2.featureId, {
9493
+ type: "note.promoted",
9494
+ message: `note promoted to ticket ${ticket.seq}`,
9495
+ ticketId: ticket.id,
9496
+ data: { noteId, seq: ticket.seq }
9497
+ });
9498
+ renderTestNotes(ctx, feature);
9499
+ return { note: getNote(ctx, noteId), ticket };
9500
+ }
9501
+ function noteLine(ctx, note) {
9502
+ if (note.status === "open")
9503
+ return `- [ ] ${note.text}`;
9504
+ const ticket = note.ticketId ? ` (\u2192 ticket ${getTicket(ctx, note.ticketId).seq})` : "";
9505
+ return `- [x] ${note.text}${ticket}`;
9506
+ }
9507
+ function renderTestNotes(ctx, feature) {
9508
+ const notes = listByFeature3(ctx, feature.id);
9509
+ const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
9510
+ const lines = ["# Test notes"];
9511
+ for (const lap of laps) {
9512
+ lines.push("", `## Lap ${lap}`, "");
9513
+ for (const note of notes.filter((n) => n.lap === lap))
9514
+ lines.push(noteLine(ctx, note));
9515
+ }
9516
+ const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9517
+ mkdirSync9(dirname10(path), { recursive: true });
9518
+ writeFileSync5(path, `${lines.join(`
9519
+ `)}
9520
+ `, "utf8");
9521
+ }
9522
+ var TITLE_MAX = 60;
9523
+ var init_test_notes = __esm(() => {
9524
+ init_src();
9525
+ init_paths();
9526
+ init_schema();
9527
+ init_errors();
9528
+ init_events();
9529
+ init_feature_docs();
9530
+ init_repo();
9531
+ init_tickets();
9532
+ });
9533
+
9534
+ // src/trpc/routers/test-notes.ts
9349
9535
  import * as z12 from "zod";
9536
+ var testNotesRouter;
9537
+ var init_test_notes2 = __esm(() => {
9538
+ init_test_notes();
9539
+ init_context();
9540
+ testNotesRouter = router({
9541
+ list: publicProcedure.input(z12.object({ featureId: z12.string() })).query(({ ctx, input }) => listByFeature3(ctx, input.featureId)),
9542
+ add: publicProcedure.input(z12.object({ featureId: z12.string(), text: z12.string().min(1) })).mutation(({ ctx, input }) => addNote(ctx, input.featureId, input.text)),
9543
+ edit: publicProcedure.input(z12.object({ noteId: z12.string(), text: z12.string().min(1) })).mutation(({ ctx, input }) => editNote(ctx, input.noteId, input.text)),
9544
+ remove: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => deleteNote(ctx, input.noteId)),
9545
+ toggle: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => toggleNote(ctx, input.noteId)),
9546
+ promote: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => promoteNote(ctx, input.noteId))
9547
+ });
9548
+ });
9549
+
9550
+ // src/trpc/routers/ticket.ts
9551
+ import * as z13 from "zod";
9350
9552
  var ticketRouter;
9351
9553
  var init_ticket = __esm(() => {
9352
9554
  init_features();
@@ -9355,8 +9557,8 @@ var init_ticket = __esm(() => {
9355
9557
  init_ticket_burner();
9356
9558
  init_context();
9357
9559
  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 }) => {
9560
+ retry: publicProcedure.input(z13.object({ ticketId: z13.string(), fresh: z13.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
9561
+ stop: publicProcedure.input(z13.object({ ticketId: z13.string() })).mutation(({ ctx, input }) => {
9360
9562
  if (stopTicketRun(input.ticketId))
9361
9563
  return { stopped: true, swept: false };
9362
9564
  const ticket = getTicket(ctx, input.ticketId);
@@ -9366,13 +9568,13 @@ var init_ticket = __esm(() => {
9366
9568
  }
9367
9569
  return { stopped: false, swept: false };
9368
9570
  }),
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()
9571
+ cancel: publicProcedure.input(z13.object({ ticketId: z13.string(), reason: z13.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
9572
+ edit: publicProcedure.input(z13.object({
9573
+ ticketId: z13.string(),
9574
+ title: z13.string().min(1).optional(),
9575
+ goal: z13.string().min(1).optional(),
9576
+ context: z13.string().optional(),
9577
+ acceptanceCriteria: z13.array(z13.string().min(1)).optional()
9376
9578
  })).mutation(({ ctx, input }) => {
9377
9579
  const { ticketId, ...patch } = input;
9378
9580
  return editTicket(ctx, ticketId, patch);
@@ -9392,12 +9594,14 @@ var init_router = __esm(() => {
9392
9594
  init_settings2();
9393
9595
  init_setup2();
9394
9596
  init_system();
9597
+ init_test_notes2();
9395
9598
  init_ticket();
9396
9599
  appRouter = router({
9397
9600
  project: projectRouter,
9398
9601
  feature: featureRouter,
9399
9602
  run: runRouter,
9400
9603
  ticket: ticketRouter,
9604
+ notes: testNotesRouter,
9401
9605
  events: eventsRouter,
9402
9606
  docs: docsRouter,
9403
9607
  settings: settingsRouter,
@@ -9407,14 +9611,14 @@ var init_router = __esm(() => {
9407
9611
  });
9408
9612
 
9409
9613
  // src/workflows/reconcile-runs.ts
9410
- import { eq as eq14 } from "drizzle-orm";
9614
+ import { eq as eq15 } from "drizzle-orm";
9411
9615
  async function reconcileStaleRuns(ctx) {
9412
- const stale = ctx.db.select().from(runs).where(eq14(runs.status, "running")).all().map(rowToRun);
9616
+ const stale = ctx.db.select().from(runs).where(eq15(runs.status, "running")).all().map(rowToRun);
9413
9617
  const reconciled = [];
9414
9618
  for (const run3 of stale) {
9415
9619
  if (isRunActive(run3.id))
9416
9620
  continue;
9417
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq14(runs.id, run3.id)).run();
9621
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq15(runs.id, run3.id)).run();
9418
9622
  const released = releaseForSession(ctx, run3.id);
9419
9623
  const swept = workflowClaimsFeatureBranch(run3.workflow) ? sweepOrphanedBurning(ctx, run3.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
9420
9624
  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.1",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",