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/index.js
CHANGED
|
@@ -271,6 +271,7 @@ var init_schemas = __esm(() => {
|
|
|
271
271
|
featureId: z2.string(),
|
|
272
272
|
seq: z2.number(),
|
|
273
273
|
status: TicketStatus,
|
|
274
|
+
lap: z2.number(),
|
|
274
275
|
commits: z2.array(z2.string()),
|
|
275
276
|
error: z2.string().optional(),
|
|
276
277
|
attemptBranch: z2.string().optional(),
|
|
@@ -348,6 +349,7 @@ var init_schemas = __esm(() => {
|
|
|
348
349
|
title: z2.string(),
|
|
349
350
|
oneLiner: z2.string(),
|
|
350
351
|
mapped: z2.boolean(),
|
|
352
|
+
lap: z2.number(),
|
|
351
353
|
phase: Phase,
|
|
352
354
|
branch: z2.string(),
|
|
353
355
|
baseBranch: z2.string().optional(),
|
|
@@ -484,7 +486,7 @@ function nextGate(feature) {
|
|
|
484
486
|
return MAPPED_G1;
|
|
485
487
|
return gate;
|
|
486
488
|
}
|
|
487
|
-
var PIPELINE, ORDER, REVIEW_LOOP_BACK, MAPPED_G1;
|
|
489
|
+
var PIPELINE, ORDER, REVIEW_LOOP_BACK, RETHINK_LOOP_BACK, MAPPED_G1;
|
|
488
490
|
var init_pipeline = __esm(() => {
|
|
489
491
|
PIPELINE = [
|
|
490
492
|
{ phase: "ideation" },
|
|
@@ -531,6 +533,7 @@ var init_pipeline = __esm(() => {
|
|
|
531
533
|
];
|
|
532
534
|
ORDER = PIPELINE.map((p) => p.phase);
|
|
533
535
|
REVIEW_LOOP_BACK = { from: "review", to: "implementation" };
|
|
536
|
+
RETHINK_LOOP_BACK = { from: "review", to: "ideation" };
|
|
534
537
|
MAPPED_G1 = {
|
|
535
538
|
id: "G1",
|
|
536
539
|
description: "Every waypoint resolved or dropped before converging",
|
|
@@ -583,6 +586,7 @@ var init_db_schema = __esm(() => {
|
|
|
583
586
|
title: text("title").notNull(),
|
|
584
587
|
oneLiner: text("one_liner").notNull(),
|
|
585
588
|
mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
|
|
589
|
+
lap: integer("lap").notNull().default(1),
|
|
586
590
|
phase: text("phase").notNull().$type(),
|
|
587
591
|
branch: text("branch").notNull(),
|
|
588
592
|
baseBranch: text("base_branch"),
|
|
@@ -593,6 +597,7 @@ var init_db_schema = __esm(() => {
|
|
|
593
597
|
id: text("id").primaryKey(),
|
|
594
598
|
featureId: text("feature_id"),
|
|
595
599
|
projectId: text("project_id"),
|
|
600
|
+
lap: integer("lap").notNull().default(1),
|
|
596
601
|
kind: text("kind").notNull().$type(),
|
|
597
602
|
ccSessionId: text("cc_session_id"),
|
|
598
603
|
transcriptPath: text("transcript_path"),
|
|
@@ -609,6 +614,7 @@ var init_db_schema = __esm(() => {
|
|
|
609
614
|
acceptanceCriteria: text("acceptance_criteria", { mode: "json" }).notNull().$type(),
|
|
610
615
|
seams: text("seams", { mode: "json" }).notNull().$type(),
|
|
611
616
|
blockedBy: text("blocked_by", { mode: "json" }).notNull().$type(),
|
|
617
|
+
lap: integer("lap").notNull().default(1),
|
|
612
618
|
status: text("status").notNull().$type(),
|
|
613
619
|
commits: text("commits", { mode: "json" }).notNull().$type(),
|
|
614
620
|
error: text("error"),
|
|
@@ -644,6 +650,7 @@ var init_db_schema = __esm(() => {
|
|
|
644
650
|
featureId: text("feature_id"),
|
|
645
651
|
runId: text("run_id"),
|
|
646
652
|
ticketId: text("ticket_id"),
|
|
653
|
+
lap: integer("lap").notNull().default(1),
|
|
647
654
|
ts: integer("ts").notNull(),
|
|
648
655
|
type: text("type").notNull(),
|
|
649
656
|
message: text("message").notNull(),
|
|
@@ -1382,6 +1389,10 @@ function projectIdForFeature(ctx, featureId) {
|
|
|
1382
1389
|
throw new NotFoundError(`feature ${featureId} not found`);
|
|
1383
1390
|
return row.projectId;
|
|
1384
1391
|
}
|
|
1392
|
+
function lapForFeature(ctx, featureId) {
|
|
1393
|
+
const row = ctx.db.select({ lap: features.lap }).from(features).where(eq(features.id, featureId)).get();
|
|
1394
|
+
return row?.lap ?? 1;
|
|
1395
|
+
}
|
|
1385
1396
|
function emit(ctx, featureId, e) {
|
|
1386
1397
|
return insertEvent(ctx, projectIdForFeature(ctx, featureId), featureId, e);
|
|
1387
1398
|
}
|
|
@@ -1399,6 +1410,7 @@ function insertEvent(ctx, projectId, featureId, e) {
|
|
|
1399
1410
|
const row = ctx.db.insert(events).values({
|
|
1400
1411
|
projectId,
|
|
1401
1412
|
featureId,
|
|
1413
|
+
lap: featureId ? lapForFeature(ctx, featureId) : 1,
|
|
1402
1414
|
runId: e.runId ?? null,
|
|
1403
1415
|
ticketId: e.ticketId ?? null,
|
|
1404
1416
|
ts: Date.now(),
|
|
@@ -1439,6 +1451,7 @@ function rowToFeature(row) {
|
|
|
1439
1451
|
title: row.title,
|
|
1440
1452
|
oneLiner: row.oneLiner,
|
|
1441
1453
|
mapped: row.mapped,
|
|
1454
|
+
lap: row.lap,
|
|
1442
1455
|
phase: row.phase,
|
|
1443
1456
|
branch: row.branch,
|
|
1444
1457
|
baseBranch: row.baseBranch ?? undefined,
|
|
@@ -1740,6 +1753,7 @@ function createSessionRow(ctx, input) {
|
|
|
1740
1753
|
id: newId("sess"),
|
|
1741
1754
|
featureId: input.featureId ?? null,
|
|
1742
1755
|
projectId: input.projectId ?? null,
|
|
1756
|
+
lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
|
|
1743
1757
|
kind: input.kind,
|
|
1744
1758
|
ccSessionId: null,
|
|
1745
1759
|
transcriptPath: null,
|
|
@@ -1771,6 +1785,9 @@ function markSessionLive(ctx, id, input = {}) {
|
|
|
1771
1785
|
}
|
|
1772
1786
|
return getSessionRow(ctx, id);
|
|
1773
1787
|
}
|
|
1788
|
+
function lapKickoff(lap) {
|
|
1789
|
+
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.";
|
|
1790
|
+
}
|
|
1774
1791
|
function kickoffLineFor(kind, override) {
|
|
1775
1792
|
return override ?? KICKOFF_LINES[kind];
|
|
1776
1793
|
}
|
|
@@ -4904,6 +4921,7 @@ function rowToTicket(row) {
|
|
|
4904
4921
|
acceptanceCriteria: row.acceptanceCriteria,
|
|
4905
4922
|
seams: row.seams,
|
|
4906
4923
|
blockedBy: row.blockedBy,
|
|
4924
|
+
lap: row.lap,
|
|
4907
4925
|
status: row.status,
|
|
4908
4926
|
commits: row.commits,
|
|
4909
4927
|
error: row.error ?? undefined,
|
|
@@ -4926,6 +4944,7 @@ function listByFeature2(ctx, featureId) {
|
|
|
4926
4944
|
function storeTickets(ctx, featureId, inputs) {
|
|
4927
4945
|
if (inputs.length === 0)
|
|
4928
4946
|
return [];
|
|
4947
|
+
const { lap } = getFeatureRow(ctx, featureId);
|
|
4929
4948
|
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
4930
4949
|
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
4931
4950
|
const resolved = resolveBlocking(inputs, startSeq);
|
|
@@ -4939,6 +4958,7 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
4939
4958
|
acceptanceCriteria: t.acceptanceCriteria,
|
|
4940
4959
|
seams: t.seams,
|
|
4941
4960
|
blockedBy: resolved[i].blockedBy,
|
|
4961
|
+
lap,
|
|
4942
4962
|
status: "pending",
|
|
4943
4963
|
commits: [],
|
|
4944
4964
|
error: null,
|
|
@@ -5046,6 +5066,7 @@ var init_tickets = __esm(() => {
|
|
|
5046
5066
|
init_schema();
|
|
5047
5067
|
init_errors();
|
|
5048
5068
|
init_events();
|
|
5069
|
+
init_repo();
|
|
5049
5070
|
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
5050
5071
|
});
|
|
5051
5072
|
|
|
@@ -5069,7 +5090,7 @@ function checkGate(ctx, check, feature) {
|
|
|
5069
5090
|
case "spec-file-exists":
|
|
5070
5091
|
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
5071
5092
|
case "tickets-approved": {
|
|
5072
|
-
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
5093
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled").length;
|
|
5073
5094
|
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
5074
5095
|
}
|
|
5075
5096
|
case "all-tickets-terminal": {
|
|
@@ -5604,6 +5625,7 @@ async function createFeature(ctx, input) {
|
|
|
5604
5625
|
title: input.title,
|
|
5605
5626
|
oneLiner: input.oneLiner,
|
|
5606
5627
|
mapped: false,
|
|
5628
|
+
lap: 1,
|
|
5607
5629
|
phase: "ideation",
|
|
5608
5630
|
branch,
|
|
5609
5631
|
baseBranch,
|
|
@@ -5688,7 +5710,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5688
5710
|
const feature = getFeatureRow(ctx, featureId);
|
|
5689
5711
|
const running = hasActiveRun(ctx, featureId);
|
|
5690
5712
|
let tickets2 = listByFeature2(ctx, featureId);
|
|
5691
|
-
const
|
|
5713
|
+
const lapTickets = tickets2.filter((t) => t.lap === feature.lap);
|
|
5714
|
+
const pending = lapTickets.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
5692
5715
|
const restarting = feature.phase === "implementation" && !running;
|
|
5693
5716
|
const iterating = feature.phase === "review" && !running && pending.length >= 1;
|
|
5694
5717
|
if (feature.phase !== "tickets" && !restarting && !iterating) {
|
|
@@ -5701,8 +5724,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5701
5724
|
why = `feature must be in the tickets phase to burn (currently ${feature.phase})`;
|
|
5702
5725
|
throw new GateError(why);
|
|
5703
5726
|
}
|
|
5704
|
-
if (
|
|
5705
|
-
throw new GateError(
|
|
5727
|
+
if (lapTickets.filter((t) => t.status !== "cancelled").length < 1) {
|
|
5728
|
+
throw new GateError(lapTickets.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
|
|
5706
5729
|
}
|
|
5707
5730
|
if (restarting) {
|
|
5708
5731
|
sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
|
|
@@ -5726,6 +5749,22 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5726
5749
|
});
|
|
5727
5750
|
return { runId };
|
|
5728
5751
|
}
|
|
5752
|
+
function rethink(ctx, featureId) {
|
|
5753
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
5754
|
+
if (feature.phase !== RETHINK_LOOP_BACK.from) {
|
|
5755
|
+
throw new GateError(`feature must be in the review phase to rethink (currently ${feature.phase})`);
|
|
5756
|
+
}
|
|
5757
|
+
if (hasActiveRun(ctx, featureId)) {
|
|
5758
|
+
throw new GateError("a run is burning this feature \u2014 cancel or wait for it before rethinking");
|
|
5759
|
+
}
|
|
5760
|
+
const live = activeSessionsForFeature(ctx, featureId);
|
|
5761
|
+
if (live.length > 0) {
|
|
5762
|
+
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`);
|
|
5763
|
+
}
|
|
5764
|
+
const lap = feature.lap + 1;
|
|
5765
|
+
ctx.db.update(features).set({ lap }).where(eq9(features.id, featureId)).run();
|
|
5766
|
+
return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
|
|
5767
|
+
}
|
|
5729
5768
|
async function retryTicket(ctx, ticketId, opts = {}) {
|
|
5730
5769
|
const ticket = getTicket(ctx, ticketId);
|
|
5731
5770
|
const feature = getFeatureRow(ctx, ticket.featureId);
|
|
@@ -5907,6 +5946,7 @@ var init_features = __esm(() => {
|
|
|
5907
5946
|
init_git();
|
|
5908
5947
|
init_knowledge();
|
|
5909
5948
|
init_repo();
|
|
5949
|
+
init_sessions();
|
|
5910
5950
|
init_end_session();
|
|
5911
5951
|
init_tickets();
|
|
5912
5952
|
init_waypoints();
|
|
@@ -5952,6 +5992,7 @@ function toolGetFeatureContext(ctx, session) {
|
|
|
5952
5992
|
const context = {
|
|
5953
5993
|
feature,
|
|
5954
5994
|
phase: feature.phase,
|
|
5995
|
+
lap: feature.lap,
|
|
5955
5996
|
docs,
|
|
5956
5997
|
tickets: listByFeature2(ctx, feature.id)
|
|
5957
5998
|
};
|
|
@@ -7460,6 +7501,7 @@ var gateId, featureRouter;
|
|
|
7460
7501
|
var init_feature = __esm(() => {
|
|
7461
7502
|
init_src();
|
|
7462
7503
|
init_launcher();
|
|
7504
|
+
init_sessions();
|
|
7463
7505
|
init_events();
|
|
7464
7506
|
init_features();
|
|
7465
7507
|
init_gates();
|
|
@@ -7483,6 +7525,14 @@ var init_feature = __esm(() => {
|
|
|
7483
7525
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
7484
7526
|
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string(), endLive: z7.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
7485
7527
|
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 })),
|
|
7528
|
+
rethink: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => {
|
|
7529
|
+
const feature = rethink(ctx, input.featureId);
|
|
7530
|
+
return launchSession(ctx, {
|
|
7531
|
+
featureId: input.featureId,
|
|
7532
|
+
kind: "revisit",
|
|
7533
|
+
kickoffLine: lapKickoff(feature.lap)
|
|
7534
|
+
});
|
|
7535
|
+
}),
|
|
7486
7536
|
resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
7487
7537
|
endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
7488
7538
|
advance: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
package/package.json
CHANGED
|
@@ -28,6 +28,8 @@ Interview the human about every aspect of the feature until you reach a genuinel
|
|
|
28
28
|
- **Look up facts; ask only for decisions.** If a fact is discoverable in the codebase — how something currently works, what a type looks like, whether a pattern already exists — read/grep the repo and find it. Never make the human be your grep. *Decisions* are theirs: put each one to them and wait.
|
|
29
29
|
- **Model the domain as you go** (forked from domain-modeling): challenge terms that conflict with existing usage, sharpen fuzzy words to one canonical meaning ("you said 'account' — Customer or User?"), invent concrete edge-case scenarios that force precision, and cross-check claims against the actual code ("your code cancels whole Orders, but you just said partial — which is right?").
|
|
30
30
|
- **Probe the size early.** Within your first few questions, ask enough about scope to judge whether this is one-session-sized or map-sized — how many independent areas it touches, what has to be researched or prototyped before decisions can lock, how wide the design tree looks. Make that call up front, not thirty questions deep into one corner. If it smells map-sized, go to §3 *now* rather than rabbit-holing first.
|
|
31
|
+
- **Ask the slicing question: how sure are you this is what you want?** Put it to them early, plainly, with your recommendation attached. **Sure, and small** → spec the whole thing; it is one lap and will likely merge on lap 1 — the old linear flow, verbatim. **Unsure, or large** → recommend a thin **lap 1**: a walking skeleton of the uncertain part, or one sub-feature slice they can actually test-drive, with the consciously deferred scope parked in the spec's `## Later laps` section (from there it seeds the next lap's session alongside the test notes). **Say the recommendation out loud and let them answer** — this is a judgment you offer, not a rule you enforce, and the human decides. There is no mode and no flag: how many laps a feature takes is a description of how it went.
|
|
32
|
+
- **Size and certainty are orthogonal.** Mapping (§3) is for ideation too big to *think* through in one session; laps are for a feature too uncertain to *spec whole*. A feature can need both, one, or neither — a small but fuzzy feature gets a thin lap 1 and no map; a big but settled one gets a map and still merges on lap 1.
|
|
31
33
|
|
|
32
34
|
Do not move on until the human confirms you have reached shared understanding.
|
|
33
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: revisit
|
|
3
|
-
description: Fold late-arriving information into a feature whose sessions are finished — amend the docs, then reconcile tickets (update/cancel/emit)
|
|
3
|
+
description: Fold late-arriving information into a feature whose sessions are finished — amend the docs, then reconcile tickets (update/cancel/emit); an ordinary revisit never advances phases. Also the lap session: on a Rethink from review, digest the test drive, amend decisions + spec, emit the lap's tickets and complete ideation → spec → tickets in the one session. Entry skill for kind=revisit sessions.
|
|
4
4
|
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@ disable-model-invocation: false
|
|
|
8
8
|
|
|
9
9
|
The human came back: they remembered a constraint, changed their mind, or learned something that the grilling/spec didn't capture. This session's job is to make the **record** and the **ticket queue** match the new reality — nothing more. The pipeline does not move; whatever phase the feature is in, it stays in.
|
|
10
10
|
|
|
11
|
+
That is the ordinary revisit, and it is what the moves below describe. The one exception is a **lap**: a Rethink from review opens this same session with a lap kickoff and a bigger job — see **Lap mode** below, and work that section instead.
|
|
12
|
+
|
|
11
13
|
## Order of operations
|
|
12
14
|
|
|
13
15
|
1. **Listen first.** The human opens with what changed. If this terminal resumed the previous conversation, use that context; do not re-grill what is already settled. Ask only the questions the NEW information raises.
|
|
@@ -23,18 +25,26 @@ The human came back: they remembered a constraint, changed their mind, or learne
|
|
|
23
25
|
- `done` work now wrong → emit a NEW ticket that corrects it. Never touch done/burning tickets.
|
|
24
26
|
5. **Close.** `mcp__runcastle__record_event({ type: "feature.revisited", message: "<one-line gist of what changed>" })`, then tell the human what state you left things in — especially whether a re-Burn is needed (pending tickets exist and the feature is at `implementation`).
|
|
25
27
|
|
|
26
|
-
##
|
|
28
|
+
## Lap mode (a Rethink)
|
|
29
|
+
|
|
30
|
+
Your kickoff line reads `LAP <n> REVIEW ITERATION`. The human burned the last lap, test-drove the branch, and clicked **Rethink**: the code was right, the *spec* wasn't. `feature.rethink` has already incremented the lap and looped the feature back, so it sits at **ideation**, not review. One trip round the pipeline is a lap, and this session is the whole front half of lap `<n>` — you carry it from what-the-drive-taught to ticket cards waiting on the human's **Burn** click, in this one conversation:
|
|
31
|
+
|
|
32
|
+
1. **Digest what the drive taught.** Your inputs are the previous lap's section of `docs/features/<slug>/test-notes.md` and the spec's `## Later laps` section, both injected into your context. **Both may be absent** — that is not an error and not something to go hunting for: interview the human from scratch instead. What did they hit, what surprised them, what do they want instead now they've used it?
|
|
33
|
+
2. **Context.** `mcp__runcastle__get_feature_context` — the feature (including its `lap`), the docs, and the full ticket history across every lap. Skim what the last lap actually landed and what failed before you interview.
|
|
34
|
+
3. **Never re-emit a promoted note.** Notes the human already promoted from the review checklist are tickets in *this* lap and arrive as ids in your context. However well such a note reads as a ticket, if its id is in front of you the work is already carded — emitting it again gives the burner the same job twice.
|
|
35
|
+
4. **Grill, briefly.** A small rethink is a short grilling: a few questions, one at a time, each with your recommended answer attached. Prune and promote `## Later laps` entries with the human as part of that conversation — what the drive taught is usually what decides which deferred scope this lap picks up and which stays parked.
|
|
36
|
+
5. **Amend the docs.** Append this lap's learning to `docs/features/<slug>/decisions.md` under a `## Lap <n>` heading (the convention) — never rewrite old decisions, supersede them. Then amend `spec.md` in place: the sections this lap changes, plus the pruned `## Later laps`.
|
|
37
|
+
6. **Emit this lap's tickets.** `mcp__runcastle__emit_tickets({ tickets })` — only the work this lap will burn; the rest stays in `## Later laps`. Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`; `done` work that is now wrong gets a NEW ticket, as always.
|
|
38
|
+
7. **Advance the pipeline.** `mcp__runcastle__complete_phase` through `ideation` → `spec` → `tickets`, here, without opening another session. It will refuse to cross into implementation — that gate is the human's Burn click, which is the point.
|
|
39
|
+
8. **Hand to Burn.** Tell the human in a line or two what this lap does, then:
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
> Lap `<n>` is specced and carded. Review the ticket cards and click **Burn** — I'll stop here.
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
2. **Interview about the test drive.** Ask what they hit — bugs, rough edges, missing polish, follow-on tweaks. One concrete change per fix ticket.
|
|
32
|
-
3. **Emit fix tickets.** New work → `emit_tickets`. If a just-burned ticket was wrong, emit a NEW fix ticket (never reopen `done` work). Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`.
|
|
33
|
-
4. **Hand back to Burn.** Do NOT advance the phase. When the cards are ready, tell the human to review them and click **Burn** — burning from review loops the feature back through implementation, and the run returns it to review when it finishes. The loop repeats until they click Merge & ship.
|
|
43
|
+
If the rethink turns out to be genuinely big — whole branches of design reopened, decisions hanging on material nobody has read — say so and escalate the way ideation would (`/runcastle:ideate` §3, the map), rather than grinding it out here.
|
|
34
44
|
|
|
35
45
|
## Do NOT
|
|
36
46
|
|
|
37
|
-
- **Never call `complete_phase
|
|
47
|
+
- **Never call `complete_phase` in an ordinary revisit.** It has no gates to cross; the human drives the pipeline from the UI. **Lap mode is the one exception** — advancing ideation → spec → tickets is its job (move 7), and nothing else here licenses it.
|
|
38
48
|
- **Never resolve or reopen waypoints.** If a resolved waypoint's answer is now wrong, record the superseding decision in `decisions.md` — the map's history stays intact.
|
|
39
49
|
- **Never implement changes.** Code changes ride tickets; the burner (or a human) implements them.
|
|
40
50
|
|