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 +55 -5
- package/drizzle/0014_ordinary_mystique.sql +4 -0
- package/drizzle/meta/0014_snapshot.json +817 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +55 -5
- package/package.json +1 -1
- package/skills/packs/runcastle/skills/ideate/SKILL.md +2 -0
- package/skills/packs/runcastle/skills/revisit/SKILL.md +18 -8
- package/web/assets/{index-BcwSVMee.js → index-CBycxXwk.js} +37 -37
- package/web/assets/{index-BLrA7l4N.css → index-yePFYqdd.css} +1 -1
- package/web/index.html +2 -2
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
|
|
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 (
|
|
5706
|
-
throw new GateError(
|
|
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;
|