runcastle 1.0.26 → 1.0.27

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
@@ -272,6 +272,7 @@ var init_schemas = __esm(() => {
272
272
  featureId: z2.string(),
273
273
  seq: z2.number(),
274
274
  status: TicketStatus,
275
+ lap: z2.number(),
275
276
  commits: z2.array(z2.string()),
276
277
  error: z2.string().optional(),
277
278
  attemptBranch: z2.string().optional(),
@@ -349,6 +350,7 @@ var init_schemas = __esm(() => {
349
350
  title: z2.string(),
350
351
  oneLiner: z2.string(),
351
352
  mapped: z2.boolean(),
353
+ lap: z2.number(),
352
354
  phase: Phase,
353
355
  branch: z2.string(),
354
356
  baseBranch: z2.string().optional(),
@@ -485,7 +487,7 @@ function nextGate(feature) {
485
487
  return MAPPED_G1;
486
488
  return gate;
487
489
  }
488
- var PIPELINE, ORDER, REVIEW_LOOP_BACK, MAPPED_G1;
490
+ var PIPELINE, ORDER, REVIEW_LOOP_BACK, RETHINK_LOOP_BACK, MAPPED_G1;
489
491
  var init_pipeline = __esm(() => {
490
492
  PIPELINE = [
491
493
  { phase: "ideation" },
@@ -532,6 +534,7 @@ var init_pipeline = __esm(() => {
532
534
  ];
533
535
  ORDER = PIPELINE.map((p) => p.phase);
534
536
  REVIEW_LOOP_BACK = { from: "review", to: "implementation" };
537
+ RETHINK_LOOP_BACK = { from: "review", to: "ideation" };
535
538
  MAPPED_G1 = {
536
539
  id: "G1",
537
540
  description: "Every waypoint resolved or dropped before converging",
@@ -584,6 +587,7 @@ var init_db_schema = __esm(() => {
584
587
  title: text("title").notNull(),
585
588
  oneLiner: text("one_liner").notNull(),
586
589
  mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
590
+ lap: integer("lap").notNull().default(1),
587
591
  phase: text("phase").notNull().$type(),
588
592
  branch: text("branch").notNull(),
589
593
  baseBranch: text("base_branch"),
@@ -594,6 +598,7 @@ var init_db_schema = __esm(() => {
594
598
  id: text("id").primaryKey(),
595
599
  featureId: text("feature_id"),
596
600
  projectId: text("project_id"),
601
+ lap: integer("lap").notNull().default(1),
597
602
  kind: text("kind").notNull().$type(),
598
603
  ccSessionId: text("cc_session_id"),
599
604
  transcriptPath: text("transcript_path"),
@@ -610,6 +615,7 @@ var init_db_schema = __esm(() => {
610
615
  acceptanceCriteria: text("acceptance_criteria", { mode: "json" }).notNull().$type(),
611
616
  seams: text("seams", { mode: "json" }).notNull().$type(),
612
617
  blockedBy: text("blocked_by", { mode: "json" }).notNull().$type(),
618
+ lap: integer("lap").notNull().default(1),
613
619
  status: text("status").notNull().$type(),
614
620
  commits: text("commits", { mode: "json" }).notNull().$type(),
615
621
  error: text("error"),
@@ -645,6 +651,7 @@ var init_db_schema = __esm(() => {
645
651
  featureId: text("feature_id"),
646
652
  runId: text("run_id"),
647
653
  ticketId: text("ticket_id"),
654
+ lap: integer("lap").notNull().default(1),
648
655
  ts: integer("ts").notNull(),
649
656
  type: text("type").notNull(),
650
657
  message: text("message").notNull(),
@@ -1383,6 +1390,10 @@ function projectIdForFeature(ctx, featureId) {
1383
1390
  throw new NotFoundError(`feature ${featureId} not found`);
1384
1391
  return row.projectId;
1385
1392
  }
1393
+ function lapForFeature(ctx, featureId) {
1394
+ const row = ctx.db.select({ lap: features.lap }).from(features).where(eq(features.id, featureId)).get();
1395
+ return row?.lap ?? 1;
1396
+ }
1386
1397
  function emit(ctx, featureId, e) {
1387
1398
  return insertEvent(ctx, projectIdForFeature(ctx, featureId), featureId, e);
1388
1399
  }
@@ -1400,6 +1411,7 @@ function insertEvent(ctx, projectId, featureId, e) {
1400
1411
  const row = ctx.db.insert(events).values({
1401
1412
  projectId,
1402
1413
  featureId,
1414
+ lap: featureId ? lapForFeature(ctx, featureId) : 1,
1403
1415
  runId: e.runId ?? null,
1404
1416
  ticketId: e.ticketId ?? null,
1405
1417
  ts: Date.now(),
@@ -1440,6 +1452,7 @@ function rowToFeature(row) {
1440
1452
  title: row.title,
1441
1453
  oneLiner: row.oneLiner,
1442
1454
  mapped: row.mapped,
1455
+ lap: row.lap,
1443
1456
  phase: row.phase,
1444
1457
  branch: row.branch,
1445
1458
  baseBranch: row.baseBranch ?? undefined,
@@ -1741,6 +1754,7 @@ function createSessionRow(ctx, input) {
1741
1754
  id: newId("sess"),
1742
1755
  featureId: input.featureId ?? null,
1743
1756
  projectId: input.projectId ?? null,
1757
+ lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
1744
1758
  kind: input.kind,
1745
1759
  ccSessionId: null,
1746
1760
  transcriptPath: null,
@@ -1772,6 +1786,9 @@ function markSessionLive(ctx, id, input = {}) {
1772
1786
  }
1773
1787
  return getSessionRow(ctx, id);
1774
1788
  }
1789
+ function lapKickoff(lap) {
1790
+ 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.";
1791
+ }
1775
1792
  function kickoffLineFor(kind, override) {
1776
1793
  return override ?? KICKOFF_LINES[kind];
1777
1794
  }
@@ -4905,6 +4922,7 @@ function rowToTicket(row) {
4905
4922
  acceptanceCriteria: row.acceptanceCriteria,
4906
4923
  seams: row.seams,
4907
4924
  blockedBy: row.blockedBy,
4925
+ lap: row.lap,
4908
4926
  status: row.status,
4909
4927
  commits: row.commits,
4910
4928
  error: row.error ?? undefined,
@@ -4927,6 +4945,7 @@ function listByFeature2(ctx, featureId) {
4927
4945
  function storeTickets(ctx, featureId, inputs) {
4928
4946
  if (inputs.length === 0)
4929
4947
  return [];
4948
+ const { lap } = getFeatureRow(ctx, featureId);
4930
4949
  const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
4931
4950
  const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
4932
4951
  const resolved = resolveBlocking(inputs, startSeq);
@@ -4940,6 +4959,7 @@ function storeTickets(ctx, featureId, inputs) {
4940
4959
  acceptanceCriteria: t.acceptanceCriteria,
4941
4960
  seams: t.seams,
4942
4961
  blockedBy: resolved[i].blockedBy,
4962
+ lap,
4943
4963
  status: "pending",
4944
4964
  commits: [],
4945
4965
  error: null,
@@ -5047,6 +5067,7 @@ var init_tickets = __esm(() => {
5047
5067
  init_schema();
5048
5068
  init_errors();
5049
5069
  init_events();
5070
+ init_repo();
5050
5071
  MUTABLE_STATUSES = new Set(["pending", "failed"]);
5051
5072
  });
5052
5073
 
@@ -5070,7 +5091,7 @@ function checkGate(ctx, check, feature) {
5070
5091
  case "spec-file-exists":
5071
5092
  return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
5072
5093
  case "tickets-approved": {
5073
- const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
5094
+ const count = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled").length;
5074
5095
  return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
5075
5096
  }
5076
5097
  case "all-tickets-terminal": {
@@ -5605,6 +5626,7 @@ async function createFeature(ctx, input) {
5605
5626
  title: input.title,
5606
5627
  oneLiner: input.oneLiner,
5607
5628
  mapped: false,
5629
+ lap: 1,
5608
5630
  phase: "ideation",
5609
5631
  branch,
5610
5632
  baseBranch,
@@ -5689,7 +5711,8 @@ async function burn(ctx, featureId, opts = {}) {
5689
5711
  const feature = getFeatureRow(ctx, featureId);
5690
5712
  const running = hasActiveRun(ctx, featureId);
5691
5713
  let tickets2 = listByFeature2(ctx, featureId);
5692
- const pending = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
5714
+ const lapTickets = tickets2.filter((t) => t.lap === feature.lap);
5715
+ const pending = lapTickets.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
5693
5716
  const restarting = feature.phase === "implementation" && !running;
5694
5717
  const iterating = feature.phase === "review" && !running && pending.length >= 1;
5695
5718
  if (feature.phase !== "tickets" && !restarting && !iterating) {
@@ -5702,8 +5725,8 @@ async function burn(ctx, featureId, opts = {}) {
5702
5725
  why = `feature must be in the tickets phase to burn (currently ${feature.phase})`;
5703
5726
  throw new GateError(why);
5704
5727
  }
5705
- if (tickets2.filter((t) => t.status !== "cancelled").length < 1) {
5706
- throw new GateError(tickets2.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
5728
+ if (lapTickets.filter((t) => t.status !== "cancelled").length < 1) {
5729
+ throw new GateError(lapTickets.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
5707
5730
  }
5708
5731
  if (restarting) {
5709
5732
  sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
@@ -5727,6 +5750,22 @@ async function burn(ctx, featureId, opts = {}) {
5727
5750
  });
5728
5751
  return { runId };
5729
5752
  }
5753
+ function rethink(ctx, featureId) {
5754
+ const feature = getFeatureRow(ctx, featureId);
5755
+ if (feature.phase !== RETHINK_LOOP_BACK.from) {
5756
+ throw new GateError(`feature must be in the review phase to rethink (currently ${feature.phase})`);
5757
+ }
5758
+ if (hasActiveRun(ctx, featureId)) {
5759
+ throw new GateError("a run is burning this feature \u2014 cancel or wait for it before rethinking");
5760
+ }
5761
+ const live = activeSessionsForFeature(ctx, featureId);
5762
+ if (live.length > 0) {
5763
+ 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`);
5764
+ }
5765
+ const lap = feature.lap + 1;
5766
+ ctx.db.update(features).set({ lap }).where(eq9(features.id, featureId)).run();
5767
+ return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
5768
+ }
5730
5769
  async function retryTicket(ctx, ticketId, opts = {}) {
5731
5770
  const ticket = getTicket(ctx, ticketId);
5732
5771
  const feature = getFeatureRow(ctx, ticket.featureId);
@@ -5908,6 +5947,7 @@ var init_features = __esm(() => {
5908
5947
  init_git();
5909
5948
  init_knowledge();
5910
5949
  init_repo();
5950
+ init_sessions();
5911
5951
  init_end_session();
5912
5952
  init_tickets();
5913
5953
  init_waypoints();
@@ -5953,6 +5993,7 @@ function toolGetFeatureContext(ctx, session) {
5953
5993
  const context = {
5954
5994
  feature,
5955
5995
  phase: feature.phase,
5996
+ lap: feature.lap,
5956
5997
  docs,
5957
5998
  tickets: listByFeature2(ctx, feature.id)
5958
5999
  };
@@ -7461,6 +7502,7 @@ var gateId, featureRouter;
7461
7502
  var init_feature = __esm(() => {
7462
7503
  init_src();
7463
7504
  init_launcher();
7505
+ init_sessions();
7464
7506
  init_events();
7465
7507
  init_features();
7466
7508
  init_gates();
@@ -7484,6 +7526,14 @@ var init_feature = __esm(() => {
7484
7526
  })).mutation(({ ctx, input }) => launchSession(ctx, input)),
7485
7527
  workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string(), endLive: z7.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
7486
7528
  converge: publicProcedure.input(z7.object({ featureId: z7.string(), overrideReason: z7.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
7529
+ rethink: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => {
7530
+ const feature = rethink(ctx, input.featureId);
7531
+ return launchSession(ctx, {
7532
+ featureId: input.featureId,
7533
+ kind: "revisit",
7534
+ kickoffLine: lapKickoff(feature.lap)
7535
+ });
7536
+ }),
7487
7537
  resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
7488
7538
  endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
7489
7539
  advance: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
@@ -0,0 +1,4 @@
1
+ ALTER TABLE `events` ADD `lap` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
2
+ ALTER TABLE `features` ADD `lap` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
3
+ ALTER TABLE `sessions` ADD `lap` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
4
+ ALTER TABLE `tickets` ADD `lap` integer DEFAULT 1 NOT NULL;