runcastle 1.4.4 → 1.4.7
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 +207 -95
- package/index.js +207 -95
- package/package.json +1 -1
- package/skills/packs/runcastle/skills/ideate/SKILL.md +61 -6
- package/skills/packs/runcastle/skills/ideate/references/preview-starter.html +274 -0
- package/skills/packs/runcastle/skills/ideate/references/preview.md +98 -0
- package/skills/packs/runcastle/skills/revisit/SKILL.md +3 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +6 -0
- package/web/assets/index-bNgQedte.css +1 -0
- package/web/assets/index-e056iqcB.js +188 -0
- package/web/index.html +2 -2
- package/web/assets/index-BAbR9IXb.css +0 -1
- package/web/assets/index-CW8ioVQ6.js +0 -188
|
@@ -22,12 +22,14 @@ Everything happens in this one window. **Never compact. Never `/clear`. Never su
|
|
|
22
22
|
|
|
23
23
|
- `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` / `mcp__runcastle__get_ticket` — the record.
|
|
24
24
|
- `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true, dependsOn? })` — park scope creep as a draft feature (§1), with `dependsOn: [<this feature's slug>]` when it builds on this one. `draft: true` is the only door open to you; a full create belongs to the project session.
|
|
25
|
-
- `mcp__runcastle__complete_phase` — the phase boundaries (§
|
|
25
|
+
- `mcp__runcastle__complete_phase` — the phase boundaries (§4), and the ones `/runcastle:spec` and `/runcastle:tickets` cross after you.
|
|
26
26
|
- `mcp__runcastle__emit_tickets` — used by `/runcastle:tickets`, not by you directly.
|
|
27
27
|
- `mcp__runcastle__record_event` — the feature timeline. Most state changes already emit their own event; only reach for this when the timeline would otherwise have no record at all.
|
|
28
28
|
|
|
29
29
|
## 1. Grill relentlessly
|
|
30
30
|
|
|
31
|
+
This is the **grill path**: the human is here and answers. When your briefing says no human is present, take the **self-planning path** in "Planning without a human" below instead; the rules of this section do not bind it.
|
|
32
|
+
|
|
31
33
|
Interview the human about every aspect of the feature until you reach a genuinely shared understanding. Walk each branch of the design tree, resolving dependencies between decisions one at a time.
|
|
32
34
|
|
|
33
35
|
- **One question at a time.** Wait for the answer before the next. Asking several at once is bewildering.
|
|
@@ -38,7 +40,20 @@ Interview the human about every aspect of the feature until you reach a genuinel
|
|
|
38
40
|
- **Park scope creep instead of swallowing it.** When something surfaces that is plainly its own feature, say so and offer to park it: `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true })` creates a parked draft — no branch, nothing written — with the `brief` carrying why you deferred it and what it must not swallow. When the parked scope builds on this feature, also pass `dependsOn: [<this feature's slug>]` so it cannot be started before this one merges. Then get back to this feature. Drafts are the only thing you may create here; a full create belongs to the project session.
|
|
39
41
|
- **A feature bigger than expected gets a thin lap 1.** The project session sizes features to fit one ideation window before they reach you, so this is rare. When a feature still turns out bigger than this window mid-grill, do not grind on and do not split it yourself: put the slicing question to the human again, recommend a thin lap 1, and park the rest under the spec's `## Later laps`.
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
On the grill path, do not move on until the human confirms you have reached shared understanding. That confirmation happens on the preview (§3), not in chat alone.
|
|
44
|
+
|
|
45
|
+
## 1a. Planning without a human
|
|
46
|
+
|
|
47
|
+
This is the **self-planning path**. It applies only when your briefing says no human is present; with a human here, §1 stands and this section does not. You still look up facts, model the domain and park scope creep as §1 says. What changes is who answers.
|
|
48
|
+
|
|
49
|
+
- **One turn.** Do the whole plan in this one turn. Never end the turn to ask a question or to report progress: nobody will reply, and ending the turn ends the run.
|
|
50
|
+
- **Frontier rounds.** Work in rounds. A round is every open question that does not depend on an unanswered one. Write each as you would put it to the human, with your recommended answer and the main alternative, then accept your own recommendations and record them. Then ask what those answers opened: that is the next round. Write each round to `decisions.md` as it is decided (§2), not at the end, so a run that dies keeps what it had.
|
|
51
|
+
- **Depth rule.** Record a decision only if it changes what the feature is or does for its users, or the human might reasonably overturn it. Anything an engineer would settle while building (wording, internal formats, test coverage) is not a question; it goes to the spec as detail.
|
|
52
|
+
- **Exactly one challenger.** When a round would be empty, run one separate agent: the Agent tool in Claude Code, `spawn_agent` in Codex. Give it only the brief and `decisions.md`. Ask it for (a) design branches never asked, within the depth rule, and (b) decisions it would have made differently, with its reason. Do not tell it what you expect to hear. Ask its branches as one more round and decide them, with whatever they open. A decision it disagrees with is marked challenged with its reason (§2), and your own answer stands. There is no second pass, and no requirement that the pass come back empty. If you have no tool that runs a separate agent, challenge your own plan as a separate pass and write in `decisions.md` that the challenge was not independent.
|
|
53
|
+
- **A refused tool is not a wait.** If a tool is refused, do not retry it and do not wait: decide on what you know, and treat that decision as a candidate to check first.
|
|
54
|
+
- **A timed-out tool is not a refused one.** A refusal is runcastle's answer; a timeout is no answer, and the call may have landed anyway. Retry it once, checking first that it did not land: `complete_phase` is safe to repeat (a step already reported comes back ok with a note), but before repeating `emit_tickets`, `update_ticket` or `cancel_ticket`, read `list_tickets` and send only what is missing. If the retry times out too, carry on and say so in the closing message. Never leave `complete_phase({ phase: "tickets" })` on a timeout: until it lands, Burn is not armed.
|
|
55
|
+
- **Check first.** Pick at most five decisions for the human to check first: guesses about the human's intent that the brief did not settle, decisions that force several others, and decisions the challenger disagreed with. They open the preview's decisions part (`./references/preview.md`).
|
|
56
|
+
- **Straight through.** Then draw the preview and run its self-check (§3, steps 1 and 2), but do not tell the human to look and do not wait: complete ideation (§4), run `/runcastle:spec`, then `/runcastle:tickets` (§5). End with one closing message (§6) that covers the plan only: the plan was made without the human, how many decisions it holds, and which are to check first, plus anything that stopped you doing the job (a refused or failed tool the plan needed). Leave out the state of tools and connectors the plan did not need.
|
|
42
57
|
|
|
43
58
|
## 2. Lock decisions incrementally
|
|
44
59
|
|
|
@@ -50,24 +65,64 @@ The moment a decision locks, append it to `docs/features/<slug>/decisions.md`
|
|
|
50
65
|
**Why:** <the reason / the trade-off chosen over the alternatives>
|
|
51
66
|
```
|
|
52
67
|
|
|
68
|
+
A decision you made without the human (the self-planning path, §1a) keeps that shape and adds one line naming the main alternative it was chosen over. A decision the challenger disagreed with adds a further line with its reason:
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
## <n>. <short decision title>
|
|
72
|
+
**Decision:** <the choice>
|
|
73
|
+
**Why:** <the reason>
|
|
74
|
+
**Assumed.** Alternative: <the main alternative>.
|
|
75
|
+
**Challenged:** <the challenger's reason>.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For example:
|
|
79
|
+
|
|
80
|
+
```markdown
|
|
81
|
+
## 4. Tags and dates are left out of the PDF
|
|
82
|
+
**Decision:** The PDF carries the title and body only.
|
|
83
|
+
**Why:** The brief asks for the note, not its filing.
|
|
84
|
+
**Assumed.** Alternative: a small footer with tags and the last-edited date.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When the human later confirms or changes such a decision, the session doing the edit removes its `**Assumed.**` and `**Challenged:**` lines; from then on it reads as any other decision.
|
|
88
|
+
|
|
53
89
|
Keep `decisions.md` free of implementation minutiae — it is the record of *what was decided and why*, the raw material spec and tickets are built from. `complete_phase` checkpoints the file into git for you; you just write it. The file is the record — do not narrate it onto the timeline as you go.
|
|
54
90
|
|
|
55
|
-
## 3.
|
|
91
|
+
## 3. Draw the preview, then wait for the human to look
|
|
92
|
+
|
|
93
|
+
**The preview is what the human confirms shared understanding on.** It is one self-contained page, `docs/features/<slug>/preview.html`, that shows the feature and every decision; the human reads the feature by looking at it.
|
|
94
|
+
|
|
95
|
+
1. **Draw it.** When the decisions are locked, read `./references/preview.md` (the rules) and write `preview.html` from `./references/preview-starter.html`.
|
|
96
|
+
2. **Check it against `decisions.md`.** Every decision appears by its number and title. Fix the preview before anyone sees it.
|
|
97
|
+
|
|
98
|
+
On the self-planning path (§1a) steps 1 and 2 are the whole of this section: go straight on to §4. Steps 3 to 5 are the grill path.
|
|
56
99
|
|
|
57
|
-
|
|
100
|
+
3. **Tell the human to look, then wait.** Say the preview is on the feature's **Overview**, under the Burn row, and that the "Preview ready" notice in Chat leads there. Then stop and wait for their answer.
|
|
101
|
+
4. **A correction is a decision edit plus a redraw.** Edit the decision in `decisions.md` and redraw the preview in the same move, then ask them to look again.
|
|
102
|
+
5. **Only after the human confirms**, complete ideation (§4).
|
|
103
|
+
|
|
104
|
+
**Never skip the preview on your own judgement** — not because the feature is small, not because the grill went quickly. A small feature gets a small preview. **If the human says to skip it**, write no file and go straight on to §4.
|
|
105
|
+
|
|
106
|
+
**The redraw rule.** While the feature is at planning, any later edit to `decisions.md` — in this session or after it — updates `preview.html` in the same move, before anything else.
|
|
107
|
+
|
|
108
|
+
## 4. Complete ideation
|
|
109
|
+
|
|
110
|
+
On the grill path, when the human confirms shared understanding on the preview and the open questions are answered or explicitly deferred. On the self-planning path (§1a), as soon as the preview passes its check, without waiting:
|
|
58
111
|
|
|
59
112
|
`mcp__runcastle__complete_phase({ phase: "ideation" })` → returns `{ ok: true, nextPhase, nextStep }`. It does not refuse and it does not move the feature: ideation, spec and tickets are three steps inside **Planning**, and this records the milestone on the timeline. `nextStep` is the next step still missing its artifact, read off disk rather than stored — so if `decisions.md` is not written yet it comes back as `"ideation"` again, which is your cue to write it. It logs its own timeline event; do not add one.
|
|
60
113
|
|
|
61
|
-
##
|
|
114
|
+
## 5. Spec → tickets — stay in this window
|
|
62
115
|
|
|
63
116
|
Invoke `/runcastle:spec` (it writes `spec.md` and completes the `spec` phase); when it returns, invoke `/runcastle:tickets`.
|
|
64
117
|
|
|
65
118
|
Do not open a new session for these. They run here, on top of everything you just learned.
|
|
66
119
|
|
|
67
|
-
##
|
|
120
|
+
## 6. Close — hand off to the human
|
|
68
121
|
|
|
69
122
|
`/runcastle:tickets` ends by emitting tickets to the runcastle store. When it returns, your job is **done**. Tell the human plainly:
|
|
70
123
|
|
|
71
124
|
> Tickets are in the runcastle UI. Review the ticket cards and click **Burn** to start the AFK agents. That is the next step — I will stop here.
|
|
72
125
|
|
|
126
|
+
On the self-planning path (§1a), the closing message also says that the plan was made without the human, how many decisions it holds, and which are to check first, by number and title. It covers the plan only. Report anything that stopped you doing the job: a tool the plan needed that was refused or failed, and what you decided without it. Do not report the state of tools and connectors the plan did not need, such as MCP servers that failed to connect or connectors waiting to be authorised: the session inherits the human's own, and they are not part of the plan. Then end the turn; the run is done.
|
|
127
|
+
|
|
73
128
|
Do **not** start burning. Burn and Merge are the only two clicks in the whole pipeline, and both belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>[Feature title] preview</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ---- runcastle theme: dark is the default, light is a full peer.
|
|
9
|
+
The app sets data-theme on <html>; opened as a file, the OS setting decides.
|
|
10
|
+
Token names and values are the app's own (apps/web/src/theme.css): keep them in step. ---- */
|
|
11
|
+
:root{
|
|
12
|
+
color-scheme:dark;
|
|
13
|
+
--color-canvas:#0a0c11;--color-surface:#10131a;--color-surface-raised:#161a24;--color-surface-hover:#1a1e29;--color-surface-selected:#212636;--color-surface-inset:#0c0e14;
|
|
14
|
+
--color-border:#222837;--color-border-subtle:rgb(200 215 255 / 0.06);--color-border-strong:#333a4d;
|
|
15
|
+
--color-text:#eceef4;--color-text-secondary:#a2a8b9;--color-text-tertiary:#8a91a5;--color-text-disabled:#4b5163;--color-icon:#8990a4;
|
|
16
|
+
--color-primary:#eceef4;--color-on-primary:#0a0c11;
|
|
17
|
+
--color-accent:#4f8ef7;--color-accent-text:#7aaafa;--color-accent-subtle:#16213a;
|
|
18
|
+
--color-success:#46b67a;--color-warning:#e2a53b;--color-danger:#f06363;--color-danger-subtle:#2a1515;
|
|
19
|
+
--color-phase-ideation:#ec7cb5;--color-phase-spec:#35c2b8;--color-phase-implementation:#f08a4b;
|
|
20
|
+
--color-phase-planning:var(--color-phase-spec);--color-phase-building:var(--color-phase-implementation);
|
|
21
|
+
--color-phase-review:var(--color-accent);--color-phase-shipped:var(--color-success);
|
|
22
|
+
--color-scrim:rgb(0 0 0 / 0.6);
|
|
23
|
+
--font-sans:"Geist Variable","Segoe UI Variable",system-ui,-apple-system,sans-serif;
|
|
24
|
+
--font-mono:"Geist Mono Variable","Cascadia Code",ui-monospace,Consolas,monospace;
|
|
25
|
+
--radius-sm:4px;--radius-md:6px;--radius-lg:10px;--ease-app:cubic-bezier(.22,.61,.24,1);
|
|
26
|
+
}
|
|
27
|
+
:root[data-theme="light"]{
|
|
28
|
+
color-scheme:light;
|
|
29
|
+
--color-canvas:#f4f6f9;--color-surface:#ffffff;--color-surface-raised:#ffffff;--color-surface-hover:#eef1f6;--color-surface-selected:#e4e8f1;--color-surface-inset:#f4f6f9;
|
|
30
|
+
--color-border:#dfe3ec;--color-border-subtle:rgb(22 30 50 / 0.07);--color-border-strong:#cdd2de;
|
|
31
|
+
--color-text:#161a24;--color-text-secondary:#4c5366;--color-text-tertiary:#5c6377;--color-text-disabled:#a4aab9;--color-icon:#6a7185;
|
|
32
|
+
--color-primary:#161a24;--color-on-primary:#ffffff;
|
|
33
|
+
--color-accent:#2f6fe4;--color-accent-text:#2463d6;--color-accent-subtle:#ecf2fe;
|
|
34
|
+
--color-success:#1e8e53;--color-warning:#a86a06;--color-danger:#c42828;--color-danger-subtle:#fdeeee;
|
|
35
|
+
--color-phase-ideation:#c13d7e;--color-phase-spec:#0b7f78;--color-phase-implementation:#b44f0c;
|
|
36
|
+
--color-scrim:rgb(22 26 36 / 0.28);
|
|
37
|
+
}
|
|
38
|
+
@media (prefers-color-scheme:light){
|
|
39
|
+
:root:not([data-theme]){
|
|
40
|
+
color-scheme:light;
|
|
41
|
+
--color-canvas:#f4f6f9;--color-surface:#ffffff;--color-surface-raised:#ffffff;--color-surface-hover:#eef1f6;--color-surface-selected:#e4e8f1;--color-surface-inset:#f4f6f9;
|
|
42
|
+
--color-border:#dfe3ec;--color-border-subtle:rgb(22 30 50 / 0.07);--color-border-strong:#cdd2de;
|
|
43
|
+
--color-text:#161a24;--color-text-secondary:#4c5366;--color-text-tertiary:#5c6377;--color-text-disabled:#a4aab9;--color-icon:#6a7185;
|
|
44
|
+
--color-primary:#161a24;--color-on-primary:#ffffff;
|
|
45
|
+
--color-accent:#2f6fe4;--color-accent-text:#2463d6;--color-accent-subtle:#ecf2fe;
|
|
46
|
+
--color-success:#1e8e53;--color-warning:#a86a06;--color-danger:#c42828;--color-danger-subtle:#fdeeee;
|
|
47
|
+
--color-phase-ideation:#c13d7e;--color-phase-spec:#0b7f78;--color-phase-implementation:#b44f0c;
|
|
48
|
+
--color-scrim:rgb(22 26 36 / 0.28);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ---- the fixed parts of every preview ---- */
|
|
53
|
+
*{box-sizing:border-box}
|
|
54
|
+
html{scroll-behavior:smooth}
|
|
55
|
+
body{margin:0;background:var(--color-surface);color:var(--color-text);font:13px/20px var(--font-sans);-webkit-font-smoothing:antialiased}
|
|
56
|
+
.page{max-width:1040px;margin:0 auto;padding:40px 32px 72px}
|
|
57
|
+
h1{margin:0;font-size:22px;line-height:28px;font-weight:600;letter-spacing:-.02em}
|
|
58
|
+
h2{margin:0 0 4px;font-size:16px;line-height:24px;font-weight:600;letter-spacing:-.01em}
|
|
59
|
+
h3{margin:0;font-size:13px;line-height:20px;font-weight:500}
|
|
60
|
+
p{margin:0}
|
|
61
|
+
.lede{margin-top:8px;max-width:620px;font-size:14px;line-height:22px;color:var(--color-text-secondary);text-wrap:pretty}
|
|
62
|
+
.meta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:10px;font-size:12px;line-height:16px;color:var(--color-text-tertiary)}
|
|
63
|
+
.hint{color:var(--color-text-tertiary);font-size:12px;line-height:16px}
|
|
64
|
+
.mono{font-family:var(--font-mono);font-size:12px}
|
|
65
|
+
section{margin-top:44px}
|
|
66
|
+
.sub{margin-top:28px}
|
|
67
|
+
.sub > .hint{margin:2px 0 12px}
|
|
68
|
+
svg.i{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
69
|
+
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
|
|
70
|
+
:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:var(--radius-sm)}
|
|
71
|
+
.jump{display:flex;flex-wrap:wrap;gap:2px;margin-top:20px;padding-bottom:12px;border-bottom:1px solid var(--color-border-subtle)}
|
|
72
|
+
.jump a{padding:4px 10px;border-radius:var(--radius-md);color:var(--color-text-secondary);text-decoration:none;transition:background .12s var(--ease-app)}
|
|
73
|
+
.jump a:hover{background:var(--color-surface-hover);color:var(--color-text)}
|
|
74
|
+
.jump a b{font-weight:400;color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}
|
|
75
|
+
|
|
76
|
+
/* a decision's number, wherever that decision is visible: <a class="pin" data-d="3">3</a> */
|
|
77
|
+
.pin{display:inline-grid;place-items:center;min-width:18px;height:18px;padding:0 4px;border-radius:var(--radius-sm);
|
|
78
|
+
background:var(--color-accent-subtle);color:var(--color-accent-text);font:500 11px/1 var(--font-mono);text-decoration:none;vertical-align:1px;cursor:pointer}
|
|
79
|
+
.pin:hover{background:var(--color-accent);color:#fff}
|
|
80
|
+
|
|
81
|
+
/* a segmented toggle, for switching a figure between states */
|
|
82
|
+
.seg{display:inline-flex;gap:2px;padding:2px;border-radius:var(--radius-md);background:var(--color-surface-inset)}
|
|
83
|
+
.seg button{height:24px;padding:0 10px;border-radius:var(--radius-sm);color:var(--color-text-secondary);transition:background .12s var(--ease-app)}
|
|
84
|
+
.seg button:hover{color:var(--color-text)}
|
|
85
|
+
.seg button[aria-pressed="true"]{background:var(--color-surface-selected);color:var(--color-text)}
|
|
86
|
+
.dot{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--color-text-disabled);flex:none}
|
|
87
|
+
.dot.ok{background:var(--color-success)}.dot.no{background:var(--color-danger)}.dot.live{background:var(--color-accent)}
|
|
88
|
+
|
|
89
|
+
/* the framed panel a mocked screen sits in; inside it, use the project's own styling */
|
|
90
|
+
.screen{border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);position:relative}
|
|
91
|
+
|
|
92
|
+
/* decided / left out / under the hood */
|
|
93
|
+
.dlist{border-top:1px solid var(--color-border-subtle);margin-top:12px}
|
|
94
|
+
.dlist details{border-bottom:1px solid var(--color-border-subtle)}
|
|
95
|
+
.dlist summary{display:flex;align-items:center;gap:12px;min-height:40px;padding:6px 8px;cursor:pointer;list-style:none;border-radius:var(--radius-md)}
|
|
96
|
+
.dlist summary::-webkit-details-marker{display:none}
|
|
97
|
+
.dlist summary:hover{background:var(--color-surface-hover)}
|
|
98
|
+
.dlist summary svg{margin-left:auto;color:var(--color-icon);transition:transform .18s var(--ease-app)}
|
|
99
|
+
.dlist details[open] summary svg{transform:rotate(90deg)}
|
|
100
|
+
.dlist details p{padding:0 8px 12px 42px;color:var(--color-text-secondary);max-width:700px}
|
|
101
|
+
.dlist details.flash summary{background:var(--color-accent-subtle)}
|
|
102
|
+
.tag{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:var(--radius-sm);background:var(--color-surface-inset);color:var(--color-text-tertiary);font-size:11px;flex:none}
|
|
103
|
+
.dlist summary .tag{margin-left:auto}.dlist summary .tag + svg{margin-left:0}
|
|
104
|
+
/* check first: built from the C list, absent when it is empty */
|
|
105
|
+
.cf{margin-top:12px}
|
|
106
|
+
.cf-item{border:1px solid var(--color-border);border-left:2px solid var(--color-warning);border-radius:var(--radius-md);padding:8px 10px;margin-top:8px;background:var(--color-surface-raised)}
|
|
107
|
+
.cf-item .t{display:flex;align-items:center;gap:8px;font-weight:500}
|
|
108
|
+
.cf-item dl{display:grid;grid-template-columns:86px 1fr;gap:2px 10px;margin:6px 0 0;color:var(--color-text-secondary)}
|
|
109
|
+
.cf-item dt{color:var(--color-text-tertiary);font-size:12px}
|
|
110
|
+
.cf-item dd{margin:0}
|
|
111
|
+
.out2{display:grid;grid-template-columns:1fr 1fr;gap:8px 32px;margin-top:12px}
|
|
112
|
+
.out2 h3{margin-bottom:6px;color:var(--color-text-tertiary);font-weight:400;font-size:12px}
|
|
113
|
+
.out2 li{display:flex;align-items:center;gap:10px;min-height:28px;color:var(--color-text-secondary)}
|
|
114
|
+
.out2 ul{margin:0;padding:0;list-style:none}
|
|
115
|
+
.hood > summary{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none}
|
|
116
|
+
.hood > summary::-webkit-details-marker{display:none}
|
|
117
|
+
.hood > summary svg{color:var(--color-icon);transition:transform .18s var(--ease-app)}
|
|
118
|
+
.hood[open] > summary svg{transform:rotate(90deg)}
|
|
119
|
+
.hood > summary h2{margin:0}
|
|
120
|
+
@media (max-width:760px){
|
|
121
|
+
.page{padding:28px 16px 56px}.out2{grid-template-columns:1fr}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ---- the show's own styles go below this line ---- */
|
|
125
|
+
</style>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
129
|
+
<symbol id="i-chev" viewBox="0 0 16 16"><path d="m6 3.500 4.500 4.500L6 12.500"/></symbol>
|
|
130
|
+
</svg>
|
|
131
|
+
|
|
132
|
+
<div class="page">
|
|
133
|
+
|
|
134
|
+
<!-- 1 · what this is -->
|
|
135
|
+
<header id="what">
|
|
136
|
+
<h1>[Feature title]</h1>
|
|
137
|
+
<p class="lede">[One or two sentences: what this feature is, in the human's words.]</p>
|
|
138
|
+
<div class="meta"><span>Lap 1</span><span><span data-dcount></span> decisions</span><span class="mono">docs/features/[slug]/preview.html</span></div>
|
|
139
|
+
<nav class="jump" aria-label="Parts of this preview">
|
|
140
|
+
<a href="#show">The show</a><a href="#decided">Decided <b data-dcount></b></a><a href="#left-out">Left out</a><a href="#hood">Under the hood</a>
|
|
141
|
+
</nav>
|
|
142
|
+
</header>
|
|
143
|
+
|
|
144
|
+
<!-- 2 · the show -->
|
|
145
|
+
<section id="show">
|
|
146
|
+
<h2>The show</h2>
|
|
147
|
+
<!-- THE SHOW IS FREE-FORM. Replace this comment with whatever explains how the
|
|
148
|
+
feature will work: screens, clickable prototypes, skeletons, flows, before
|
|
149
|
+
and after, scenarios. There is no template. Pin a decision's number where
|
|
150
|
+
it is visible: <a class="pin" data-d="3">3</a>. Diagrams and flows use the
|
|
151
|
+
runcastle tokens above; a mocked screen uses the project's own styling
|
|
152
|
+
inside a .screen panel, wrapped in a screen block.
|
|
153
|
+
|
|
154
|
+
Example screen block (copy it, rename it in kebab case, uncomment the
|
|
155
|
+
panel; delete it if the show has no screen):
|
|
156
|
+
-->
|
|
157
|
+
<!-- screen:example-name -->
|
|
158
|
+
<!--
|
|
159
|
+
<div class="screen">
|
|
160
|
+
... the mocked screen, in the project's own styling ...
|
|
161
|
+
</div>
|
|
162
|
+
-->
|
|
163
|
+
<!-- /screen:example-name -->
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<!-- 3 · what was decided (built from the list in the script below) -->
|
|
167
|
+
<section id="decided">
|
|
168
|
+
<h2>What was decided</h2>
|
|
169
|
+
<p class="hint">Open one for the reason.</p>
|
|
170
|
+
<div class="dlist" id="dlist"></div>
|
|
171
|
+
</section>
|
|
172
|
+
|
|
173
|
+
<!-- 4 · what is left out -->
|
|
174
|
+
<section id="left-out">
|
|
175
|
+
<h2>What is left out</h2>
|
|
176
|
+
<div class="out2">
|
|
177
|
+
<div><h3>Later laps</h3><ul>
|
|
178
|
+
<li><i class="dot"></i>[Deferred to a later lap]</li>
|
|
179
|
+
</ul></div>
|
|
180
|
+
<div><h3>Not this feature</h3><ul>
|
|
181
|
+
<li><i class="dot"></i>[Out of scope]</li>
|
|
182
|
+
</ul></div>
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
185
|
+
|
|
186
|
+
<!-- 5 · under the hood (collapsed by default) -->
|
|
187
|
+
<section id="hood-s">
|
|
188
|
+
<details class="hood" id="hood">
|
|
189
|
+
<summary><svg class="i"><use href="#i-chev"/></svg><h2>Under the hood</h2></summary>
|
|
190
|
+
<div class="sub">
|
|
191
|
+
<!-- [The technical shape, for a reader who wants it. Draw it where you can.] -->
|
|
192
|
+
</div>
|
|
193
|
+
</details>
|
|
194
|
+
</section>
|
|
195
|
+
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
(function () {
|
|
200
|
+
var $ = function (s, r) { return (r || document).querySelector(s) }
|
|
201
|
+
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)) }
|
|
202
|
+
|
|
203
|
+
// every decision in decisions.md, by number and title, in order: [title, reason, mark?]
|
|
204
|
+
// mark is 'assumed' for a decision made without the human; a grilled feature has none
|
|
205
|
+
var D = [
|
|
206
|
+
['[Decision 1 title]', '[Why, in a sentence]']
|
|
207
|
+
]
|
|
208
|
+
// check first, a self-planned feature only (at most five): [decisionNumber, alternative, whyUnsure, tag?]
|
|
209
|
+
// leave it empty for a grilled feature, and no group is drawn
|
|
210
|
+
var C = []
|
|
211
|
+
var list = $('#dlist')
|
|
212
|
+
D.forEach(function (d, i) {
|
|
213
|
+
var el = document.createElement('details')
|
|
214
|
+
el.id = 'd' + (i + 1)
|
|
215
|
+
el.innerHTML = '<summary><span class="pin">' + (i + 1) + '</span><span></span><svg class="i"><use href="#i-chev"/></svg></summary><p></p>'
|
|
216
|
+
$('summary span:nth-child(2)', el).textContent = d[0]
|
|
217
|
+
$('p', el).textContent = d[1]
|
|
218
|
+
if (d[2]) {
|
|
219
|
+
var mark = document.createElement('span')
|
|
220
|
+
mark.className = 'tag'
|
|
221
|
+
mark.textContent = d[2]
|
|
222
|
+
$('summary svg', el).before(mark)
|
|
223
|
+
}
|
|
224
|
+
list.appendChild(el)
|
|
225
|
+
})
|
|
226
|
+
if (C.length) {
|
|
227
|
+
var cf = document.createElement('div')
|
|
228
|
+
cf.className = 'cf'
|
|
229
|
+
cf.innerHTML = '<h3>Check first</h3><p class="hint">At most five. The ones most likely to be wrong for you.</p>'
|
|
230
|
+
C.forEach(function (c) {
|
|
231
|
+
var item = document.createElement('div')
|
|
232
|
+
item.className = 'cf-item'
|
|
233
|
+
item.innerHTML = '<div class="t"><a class="pin" data-d="' + c[0] + '">' + c[0] + '</a><span></span></div>' +
|
|
234
|
+
'<dl><dt>Alternative</dt><dd></dd><dt>Why unsure</dt><dd></dd></dl>'
|
|
235
|
+
$('.t span', item).textContent = D[c[0] - 1][0]
|
|
236
|
+
if (c[3]) {
|
|
237
|
+
var tag = document.createElement('span')
|
|
238
|
+
tag.className = 'tag'
|
|
239
|
+
tag.textContent = c[3]
|
|
240
|
+
$('.t', item).appendChild(tag)
|
|
241
|
+
}
|
|
242
|
+
var dd = $$('dd', item)
|
|
243
|
+
dd[0].textContent = c[1]
|
|
244
|
+
dd[1].textContent = c[2]
|
|
245
|
+
cf.appendChild(item)
|
|
246
|
+
})
|
|
247
|
+
list.before(cf)
|
|
248
|
+
}
|
|
249
|
+
$$('[data-dcount]').forEach(function (el) { el.textContent = String(D.length) })
|
|
250
|
+
|
|
251
|
+
// a pin opens its decision; in-page links scroll by script, because hash
|
|
252
|
+
// navigation is unreliable inside the app's sandboxed frame
|
|
253
|
+
function go(el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) }
|
|
254
|
+
document.addEventListener('click', function (e) {
|
|
255
|
+
var pin = e.target.closest('a.pin[data-d]')
|
|
256
|
+
if (pin) {
|
|
257
|
+
e.preventDefault()
|
|
258
|
+
var d = $('#d' + pin.dataset.d)
|
|
259
|
+
d.open = true; go(d); d.classList.add('flash')
|
|
260
|
+
setTimeout(function () { d.classList.remove('flash') }, 1400)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
var a = e.target.closest('.jump a')
|
|
264
|
+
if (a) {
|
|
265
|
+
e.preventDefault()
|
|
266
|
+
var t = $(a.getAttribute('href'))
|
|
267
|
+
if (t.tagName === 'DETAILS') t.open = true
|
|
268
|
+
t.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
})()
|
|
272
|
+
</script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# The plan preview — the page the human looks at
|
|
2
|
+
|
|
3
|
+
Load this when the decisions are locked and the preview step (§3 of the ideate
|
|
4
|
+
skill) arrives, or when a decision edit at planning means the preview has to be
|
|
5
|
+
redrawn. Otherwise it costs this conversation nothing.
|
|
6
|
+
|
|
7
|
+
The preview is the one page the human reads the feature by. They confirm the
|
|
8
|
+
plan by looking at it, and when tired it is the only thing they read before
|
|
9
|
+
Burn. Draw it for that reader.
|
|
10
|
+
|
|
11
|
+
## The file
|
|
12
|
+
|
|
13
|
+
- **Exactly one file, `preview.html`, at the root of the feature's docs dir**:
|
|
14
|
+
`docs/features/<slug>/preview.html`. Several screens or flows are sections of
|
|
15
|
+
that one page, never more files.
|
|
16
|
+
- **Start from `./preview-starter.html`.** Copy it to that path and fill it in.
|
|
17
|
+
It carries runcastle's theme tokens for both themes and the styles and markup
|
|
18
|
+
for the fixed parts; do not restyle them.
|
|
19
|
+
- **Throwaway explorations** may go under `docs/features/<slug>/prototypes/`.
|
|
20
|
+
They are never shown; the app shows only `preview.html`.
|
|
21
|
+
|
|
22
|
+
## Five parts, in this order
|
|
23
|
+
|
|
24
|
+
1. **What this is.** One or two sentences.
|
|
25
|
+
2. **The show.** Whatever explains how the feature will work. **It is free, with
|
|
26
|
+
no template**: screens, clickable prototypes, skeletons, flows, before and
|
|
27
|
+
after, scenarios. Choose the form that explains *this* feature — work with no
|
|
28
|
+
screen gets flows, use cases, before and after, or a clickable skeleton.
|
|
29
|
+
3. **What was decided.** Every decision in `decisions.md` by its number and
|
|
30
|
+
title, with the reason available on demand (the starter's `D` list builds
|
|
31
|
+
this). Where a decision is visible in the show, pin its number on that spot:
|
|
32
|
+
`<a class="pin" data-d="3">3</a>`.
|
|
33
|
+
**A self-planned feature** (§1a of the ideate skill) opens this part with a
|
|
34
|
+
**Check first** group of at most five decisions, the ones the planner picked.
|
|
35
|
+
Each entry shows the decision's number and title, its alternative, and why it
|
|
36
|
+
is uncertain (the starter's `C` list builds this). The remaining decisions
|
|
37
|
+
follow in the list, each showing that it was assumed (a third element on its
|
|
38
|
+
`D` entry). **A grilled feature has neither**: no group and no assumed marks.
|
|
39
|
+
4. **What is left out.** Out of scope, and anything deferred to a later lap.
|
|
40
|
+
5. **Under the hood.** The technical shape for a reader who wants it,
|
|
41
|
+
**collapsed by default**, drawn where it can be.
|
|
42
|
+
|
|
43
|
+
## Visual first
|
|
44
|
+
|
|
45
|
+
**A short sentence beside a picture, never paragraphs, detail behind a click.**
|
|
46
|
+
If a part reads as prose, draw it instead: a flow, a before and after, a
|
|
47
|
+
toggled state, a list of dots.
|
|
48
|
+
|
|
49
|
+
## The theme rule
|
|
50
|
+
|
|
51
|
+
- **The page and every diagram and flow are runcastle-themed**, through the
|
|
52
|
+
starter's tokens, in light or dark to match the app.
|
|
53
|
+
- **A mocked screen uses the styling of the project it belongs to**, inside the
|
|
54
|
+
framed `.screen` panel. A prototype of another product's UI can only be judged
|
|
55
|
+
in that product's look.
|
|
56
|
+
|
|
57
|
+
## Screen blocks
|
|
58
|
+
|
|
59
|
+
Each screen in the show is a **screen block**: the lines between two markers,
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<!-- screen:<name> -->
|
|
63
|
+
<div class="screen"> … </div>
|
|
64
|
+
<!-- /screen:<name> -->
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
with `<name>` in kebab case and unique in the file. A ticket that builds that
|
|
68
|
+
screen names the block, and its burn agent reads only those lines — so put
|
|
69
|
+
everything the screen needs to be understood between the markers, and nothing
|
|
70
|
+
else. Delete the starter's `example-name` block if the show has no screen.
|
|
71
|
+
|
|
72
|
+
## Inline only
|
|
73
|
+
|
|
74
|
+
**Styles, scripts and images are inline. The file makes no network request** —
|
|
75
|
+
no `<link>`, no `@import`, no external `src` or `url(`; images are `data:` URIs
|
|
76
|
+
or inline SVG. This is enforced, not a preference: the app renders the preview
|
|
77
|
+
in a sandboxed frame that runs scripts and refuses every fetch, external image
|
|
78
|
+
and font, form post and popup. Anything hosted elsewhere simply does not load.
|
|
79
|
+
In-page links scroll by script (the starter does this), because hash navigation
|
|
80
|
+
is unreliable inside that frame.
|
|
81
|
+
|
|
82
|
+
## Sizing
|
|
83
|
+
|
|
84
|
+
**There is no minimum.** A small feature's show can be a single picture and the
|
|
85
|
+
other parts a line each. Reading target, as guidance and not a limit: **about a
|
|
86
|
+
minute for a small feature, a few minutes for a large one.**
|
|
87
|
+
|
|
88
|
+
## Self-check, before you tell the human to look
|
|
89
|
+
|
|
90
|
+
- Every decision in `decisions.md` appears in **What was decided**, by its number
|
|
91
|
+
and its title, in order — none missing, none renumbered.
|
|
92
|
+
- For a self-planned feature, the **Check first** group holds at most five
|
|
93
|
+
decisions, each with its alternative and why it is uncertain, and every
|
|
94
|
+
decision carrying an `**Assumed.**` line in `decisions.md` shows as assumed.
|
|
95
|
+
For a grilled feature, `C` is empty and no decision is marked.
|
|
96
|
+
- Every pin points at the decision the spot actually shows.
|
|
97
|
+
- The five parts are in order; **Under the hood** is closed.
|
|
98
|
+
- The file is still fully inline.
|
|
@@ -16,7 +16,9 @@ That is the ordinary revisit, and it is what the moves below describe. The one e
|
|
|
16
16
|
2. **Context.** Call `mcp__runcastle__get_feature_context` — the feature, its `phase` and `lap`, the canonical docs inlined in `docs[]` (brief, decisions, spec), an index of everything else in `moreDocs[]` (test notes and any other doc the feature's sessions left behind; fetch one with `mcp__runcastle__read_feature_doc({ relPath })`), and every ticket as a summary row with its id, `seq`, status and `goal` — its context, acceptance criteria and digest come from `mcp__runcastle__get_ticket({ seq })`. A canonical doc too large to inline is listed in `notInlined` instead; read it before acting. The injected system prompt carries the slug and paths; trust `get_feature_context` for the live state. When all you need is ticket ids to operate on, `mcp__runcastle__list_tickets({ status? })` is the cheap call — it returns the queue without the docs.
|
|
17
17
|
3. **Docs.** Capture the change as decision prose:
|
|
18
18
|
- Append to `docs/features/<slug>/decisions.md` under a dated `## Revisited <date>` heading — never rewrite old decisions, supersede them ("Supersedes: <old decision>").
|
|
19
|
+
- **Clear the assumed mark.** A decision the session made without the human carries an `**Assumed.**` line, and perhaps a `**Challenged:**` line. When the human confirms or changes such a decision, remove both lines from it in the same edit. Decisions the human did not touch keep theirs.
|
|
19
20
|
- If `spec.md` exists and the change touches it, amend the affected sections in place.
|
|
21
|
+
- **Redraw the preview.** When the feature is at `planning` and `docs/features/<slug>/preview.html` exists, update `preview.html` in the same move as the decisions edit, before anything else, following `../ideate/references/preview.md`, check-first group included: a decision that lost its assumed mark leaves that group. The human reads the plan off the preview, so a preview that disagrees with `decisions.md` misleads them at Burn.
|
|
20
22
|
4. **Ticket surgery.** Walk the ticket list against the new reality:
|
|
21
23
|
- Stale but still needed → `mcp__runcastle__update_ticket({ id, ...changed fields })` (pending/failed only).
|
|
22
24
|
- No longer needed → `mcp__runcastle__cancel_ticket({ id, reason })` (pending/failed only).
|
|
@@ -35,7 +37,7 @@ Your kickoff line reads `PLAN LAP <n> FROM REVIEW`. The human burned the last la
|
|
|
35
37
|
- **What the docs say is not demonstrable yet.** Grep `spec.md` and this feature's tickets for `not demonstrable`, `do not demo` and `later laps`, and tell the human what you found **before** you offer a test drive. A human was once handed a lap-1 build whose own plan said "no UI, nobody should demo this before ticket X"; the drive taught nothing and cost them the time to find that out. Say which parts are drivable and which are not, then offer the drive.
|
|
36
38
|
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.
|
|
37
39
|
4. **Grill, briefly.** A small lap 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.
|
|
38
|
-
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`.
|
|
40
|
+
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`. A lap planned from review does not redraw the preview: leave `preview.html` as it is.
|
|
39
41
|
6. **Emit this lap's tickets, dispositioning the defects as you do.** `mcp__runcastle__emit_tickets({ tickets })` — only the work this lap will burn; the rest stays in `## Later laps`. They land `pending`, and the Burn click carries them onto lap `<n>`. Every ticket that answers a bug the human reported obeys **Stand on the failure** below. Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`; `done` work that is now wrong gets a NEW ticket, as always.
|
|
40
42
|
|
|
41
43
|
The defects an **earlier** lap's review left open arrive in your context as `openDefects` (`carriedDefects` are the ones a lap already parked — agenda, not obligation). Each open one needs exactly one of three answers. Nothing refuses over the ones you leave: the human reads them back as warnings in the Burn dialog, so finishing this triage is yours, not a gate's:
|
|
@@ -71,6 +71,12 @@ And width only counts if it lands: parallel tickets touching the same files coll
|
|
|
71
71
|
A **wide refactor** — one mechanical change (rename a column, retype a shared symbol) whose blast radius breaks thousands of call sites at once — cannot land green as a vertical slice. Sequence it **expand → migrate → contract** instead. Expand: add the new form beside the old so nothing breaks (one ticket). Migrate: move call sites over in batches sized by blast radius (per package/dir), each batch a ticket blocked by the expand, CI green throughout because the old form still exists. Contract: delete the old form once no caller remains, blocked by every migrate batch. If even the batches cannot stay green alone, let them share an integration branch that all block a final integrate-and-verify ticket — green is promised only there.
|
|
72
72
|
</wide-refactor-exception>
|
|
73
73
|
|
|
74
|
+
<preview-screen-block>
|
|
75
|
+
When the feature has a `preview.html` and a ticket **builds a screen that appears in it as a screen block**, point that ticket at **one named screen block**, never the file. Its **context** names the path (`docs/features/<slug>/preview.html`) and the block name, and tells the burn agent to find the two markers, `<!-- screen:<name> -->` and `<!-- /screen:<name> -->`, and **read only the lines between them**. Say plainly that the block shows the approved look and behaviour and **is not the implementation**: build it with the project's real components and never copy the mock's markup. The mock's styles live in the page head and are deliberately not read. **Never tell an agent to read the whole file** — the rest repeats docs it already has.
|
|
76
|
+
|
|
77
|
+
A ticket with no screen does not mention the preview.
|
|
78
|
+
</preview-screen-block>
|
|
79
|
+
|
|
74
80
|
## 2. Build each ticket to the schema
|
|
75
81
|
|
|
76
82
|
Emit an array of `TicketInput`. Number them from 1 in the array in **dependency order (blockers first)**; `blockedBy` holds those numbers (they are the seq numbers the store assigns).
|