Add a plan.html review page to the ticket skill

This commit is contained in:
Gabriel Brown
2026-09-22 12:54:32 -04:00
parent 9b06e9e24b
commit 9022847971
2 changed files with 471 additions and 7 deletions
+49 -7
View File
@@ -1,6 +1,6 @@
--- ---
name: ticket name: ticket
description: End-to-end Jira ticket workflow — fetch a ticket into .claude/docs/epics/, write a plan, implement it with clean commits, drive the pre-mr-review audit to convergence (the user runs it themselves from their work Claude account), and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket. description: End-to-end Jira ticket workflow — fetch a ticket into .claude/docs/epics/, write a plan and its plan.html review page, implement it with clean commits, drive the pre-mr-review audit to convergence (the user runs it themselves from their work Claude account), and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket.
disable-model-invocation: true disable-model-invocation: true
--- ---
@@ -32,6 +32,7 @@ this skill is Phase 2 step 5 and is performed by a dispatched agent, not by a sk
<video>.transcript.txt # only for videos that got transcribed <video>.transcript.txt # only for videos that got transcribed
<video>.transcript.srt <video>.transcript.srt
plan.md plan.md
plan.html # the plan as a page, with mocks when a step changes what a user sees
deliverables/ # only for tickets whose output is documents, not code deliverables/ # only for tickets whose output is documents, not code
<deliverable-slug>/ <deliverable-slug>/
<deliverable-slug>.md <deliverable-slug>.md
@@ -43,6 +44,7 @@ this skill is Phase 2 step 5 and is performed by a dispatched agent, not by a sk
<TICKET-KEY>/ # same shape, for tickets with no epic <TICKET-KEY>/ # same shape, for tickets with no epic
resources/... resources/...
plan.md plan.md
plan.html
deliverables/... deliverables/...
mr.md mr.md
``` ```
@@ -163,7 +165,8 @@ deliverables. It does not govern files only he reads. `plan.md`, `bugs.md`, audi
working notes can name him and record who decided what, because that history is useful. working notes can name him and record who decided what, because that history is useful.
The House style punctuation rule above is the stricter one where the two overlap. Keep The House style punctuation rule above is the stricter one where the two overlap. Keep
it. Personal working files (`plan.md`, `ticket.md`) are exempt, nobody else reads them. it. Personal working files (`plan.md`, `plan.html`, `ticket.md`) are exempt, nobody else
reads them.
## Verification tools available ## Verification tools available
@@ -369,6 +372,41 @@ Reach for it when the spike's real open question is which of several directions
build. Once a direction is settled, the deliverable is the two artifacts above, and both build. Once a direction is settled, the deliverable is the two artifacts above, and both
ship with the ticket. ship with the ticket.
## The plan page (`plan.html`)
`plan.md` is the plan. `plan.html` is the same plan as a page, saved beside it, so Gib
reads the plan in a browser before approving it instead of scrolling a markdown file. It
opens from `file://`, ticks acceptance criteria and steps as he reviews them, and shows
the mocks in place. It is the review artifact, and it exists to be opened.
- **Fill the template.** Copy `~/.agents/skills/ticket/templates/plan.html` to
`<target-dir>/plan.html` and fill every slot its header comment lists, in the order
`plan.md` carries them: the same criteria, the same open questions, the same Approach,
the same steps with the same `(mock)` and `(deliverable)` marks as badges, the same
risks, the same test plan. Fill the slots only. The CSS and script ship as they are, so
every plan page looks and behaves the same. Done when a reader of either file learns
nothing the other omits.
- **Mocks, when a step changes what a user sees.** A new screen, a changed layout, a new
control, or copy a user reads means the page keeps its Mocks section: one artboard per
screen state the plan introduces, drawn in plain HTML and CSS inside its own
`<template>`, with the app's real navigation and density around it rather than a
wireframe of boxes. When the direction is Gib's call, draw each candidate as its own
artboard (Option A, Option B) and name the choice in Open questions, pointing at the
artboards. When the direction is settled, one artboard per screen. Each artboard's
`data-note` states what it shows that the implementation will not do. This is how the
house rule about several static mocks before touching real components is satisfied
inside the ticket flow, so no separate mock server is needed. When no step changes
what a user sees, delete the Mocks section and its nav link.
- **Open it.** After saving, run `xdg-open <target-dir>/plan.html` so it lands in the
browser, and give both paths in the Phase 1 wrap-up.
- **`plan.md` stays the source of truth.** When Phase 0 routes to rewriting the plan,
rewrite `plan.html` with it. Phase 2 checks steps off in `plan.md` only. The page is a
snapshot for the review and is left alone once the plan is approved.
- **Relation to spike mocks.** The plan page's artboards are design intent drawn before
any code exists. A deliverable's polished `mocks/` (see Mocks and datamodel changes)
can start from the approved artboard, and the built screens are still screenshotted
for real.
--- ---
## Branch setup — run this before any repo work in Phase 1 or Phase 2 ## Branch setup — run this before any repo work in Phase 1 or Phase 2
@@ -429,7 +467,8 @@ place from the start.
a plan. Go to **Phase 1** and regenerate from scratch (re-fetch, overwrite a plan. Go to **Phase 1** and regenerate from scratch (re-fetch, overwrite
`ticket.md`, re-check attachments); it's idempotent and cheap. `ticket.md`, re-check attachments); it's idempotent and cheap.
- **`plan.md` exists, no `mr.md`** — ask the user (AskUserQuestion) what they want: - **`plan.md` exists, no `mr.md`** — ask the user (AskUserQuestion) what they want:
- Re-fetch the ticket and rewrite the plan from scratch (they want to start over) - Re-fetch the ticket and rewrite the plan (`plan.md` and `plan.html`) from scratch
(they want to start over)
- Proceed to implementing the existing `plan.md` as-is (they reviewed and approved it) - Proceed to implementing the existing `plan.md` as-is (they reviewed and approved it)
- Resume implementation (some plan steps are already checked off / some commits - Resume implementation (some plan steps are already checked off / some commits
already exist on the ticket branch — pick up from the first unchecked step) already exist on the ticket branch — pick up from the first unchecked step)
@@ -623,10 +662,13 @@ place from the start.
step stays green because the old form still exists until the last one. Say in the step stays green because the old form still exists until the last one. Say in the
Approach that this is what you're doing and why, since the step count looks inflated Approach that this is what you're doing and why, since the step count looks inflated
otherwise. otherwise.
12. Stop here. Tell the user `plan.md` is ready at its path, summarize the approach in 12. Write `plan.html` beside it, per **The plan page** above, and open it with
a couple of sentences, and mention any open questions that need their input before `xdg-open`.
you'd implement it. Do not start implementing in this same run — wait for them to 13. Stop here. Tell the user `plan.md` and `plan.html` are ready at their paths and that
review the plan (editing it directly if they want) and invoke `/ticket <KEY>` again. the page is open in the browser, summarize the approach in a couple of sentences, and
mention any open questions that need their input before you'd implement it. Do not
start implementing in this same run — wait for them to review the plan (editing
`plan.md` directly if they want) and invoke `/ticket <KEY>` again.
## Phase 2 — implement, verify, and hand off ## Phase 2 — implement, verify, and hand off
@@ -0,0 +1,422 @@
<!doctype html>
<!--
plan.html template for the ticket skill. Copy this file next to plan.md and fill
every slot marked "slot:". Everything outside a slot stays as it is, so every plan
page looks and behaves the same. The page is self-contained: no network, no
external assets, opens from file://.
Slots, in order:
KEY, TITLE, JIRA_URL, BRANCH, KIND the header line
criteria one <li> per acceptance criterion
questions one <li> per open question, or the "None" line
approach the Approach narrative as <p> blocks
steps one <li> per step, with a badge for (mock) / (deliverable)
risks one <li> per risk or edge case
tests one <li> per test plan item
mocks one <template> per artboard, plus its tab button;
delete the whole <section id="mocks"> and its
nav link when the ticket changes nothing a user sees
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- slot: KEY and TITLE -->
<title>KACP-00000 plan</title>
<style>
:root {
--bg: #f7f7f5;
--surface: #ffffff;
--ink: #1c1c1a;
--muted: #6b6b66;
--line: #e2e2dd;
--accent: #2f5fd0;
--accent-ink: #ffffff;
--warn-bg: #fff6e0;
--warn-line: #e6c36b;
--badge-mock: #e8f0ff;
--badge-deliverable: #eaf7ea;
--mono: ui-monospace, "JetBrains Mono", "Cascadia Mono", Menlo, monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Inter", sans-serif;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #151614;
--surface: #1e1f1c;
--ink: #ecebe6;
--muted: #9b9a93;
--line: #2e2f2b;
--accent: #7c9cf0;
--accent-ink: #0f1420;
--warn-bg: #2b2513;
--warn-line: #7a6427;
--badge-mock: #1f2a44;
--badge-deliverable: #1d2f1f;
}
}
:root[data-theme="dark"] {
--bg: #151614;
--surface: #1e1f1c;
--ink: #ecebe6;
--muted: #9b9a93;
--line: #2e2f2b;
--accent: #7c9cf0;
--accent-ink: #0f1420;
--warn-bg: #2b2513;
--warn-line: #7a6427;
--badge-mock: #1f2a44;
--badge-deliverable: #1d2f1f;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 16px/1.55 var(--sans);
}
a { color: var(--accent); }
code { font-family: var(--mono); font-size: 0.92em; }
pre {
font-family: var(--mono);
font-size: 0.88em;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 12px 14px;
overflow-x: auto;
}
.layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 32px;
max-width: 1180px;
margin: 0 auto;
padding: 24px 16px 64px;
}
nav {
position: sticky;
top: 24px;
align-self: start;
font-size: 0.92em;
}
nav ol { list-style: none; margin: 0; padding: 0; }
nav li { margin: 0 0 6px; }
nav a { color: var(--muted); text-decoration: none; }
nav a:hover, nav a.active { color: var(--ink); }
nav .progress {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.9em;
}
header.plan { margin-bottom: 28px; }
header.plan h1 { margin: 0 0 6px; font-size: 1.7em; line-height: 1.2; }
header.plan .meta {
display: flex;
flex-wrap: wrap;
gap: 6px 18px;
color: var(--muted);
font-size: 0.92em;
}
header.plan .meta code { color: var(--ink); }
section { margin-bottom: 36px; }
section h2 {
font-size: 1.15em;
margin: 0 0 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--line);
}
section p { margin: 0 0 12px; max-width: 72ch; }
section ul { padding-left: 22px; margin: 0; }
section li { margin: 0 0 8px; max-width: 72ch; }
.checklist { list-style: none; padding: 0; }
.checklist li { display: flex; gap: 10px; align-items: baseline; }
.checklist input { margin: 0; flex: none; position: relative; top: 2px; }
.checklist li.done label { color: var(--muted); text-decoration: line-through; }
.badge {
display: inline-block;
font-size: 0.75em;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
padding: 1px 7px;
border-radius: 999px;
margin-left: 6px;
vertical-align: middle;
}
.badge.mock { background: var(--badge-mock); }
.badge.deliverable { background: var(--badge-deliverable); }
.callout {
background: var(--warn-bg);
border: 1px solid var(--warn-line);
border-radius: 6px;
padding: 12px 16px;
}
.callout ul { margin: 0; }
.callout.quiet { background: var(--surface); border-color: var(--line); color: var(--muted); }
/* Mocks: one artboard per <template>, shown in an iframe so the mock's CSS stays its own. */
.mock-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.mock-bar .spacer { flex: 1; }
.mock-bar button {
font: inherit;
font-size: 0.9em;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 5px 12px;
cursor: pointer;
}
.mock-bar button[aria-pressed="true"] {
background: var(--accent);
color: var(--accent-ink);
border-color: var(--accent);
}
.mock-stage {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}
.mock-stage iframe {
display: block;
width: 100%;
max-width: 100%;
height: 480px;
margin: 0 auto;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
}
.mock-stage[data-width="tablet"] iframe { width: 820px; }
.mock-stage[data-width="phone"] iframe { width: 390px; }
.mock-note { color: var(--muted); font-size: 0.92em; margin-top: 10px; }
.mock-note:empty { display: none; }
@media (max-width: 820px) {
.layout { grid-template-columns: 1fr; gap: 16px; }
nav { position: static; }
nav ol { display: flex; flex-wrap: wrap; gap: 4px 14px; }
nav .progress { display: none; }
.mock-stage[data-width="tablet"] iframe,
.mock-stage[data-width="phone"] iframe { width: 100%; }
}
</style>
</head>
<body>
<div class="layout">
<nav aria-label="Sections">
<ol>
<li><a href="#criteria">Acceptance criteria</a></li>
<li><a href="#questions">Open questions</a></li>
<li><a href="#approach">Approach</a></li>
<!-- slot: mocks nav link, delete with the section when there are no mocks -->
<li><a href="#mocks">Mocks</a></li>
<li><a href="#steps">Steps</a></li>
<li><a href="#risks">Risks and edge cases</a></li>
<li><a href="#tests">Test plan</a></li>
</ol>
<div class="progress" id="progress"></div>
</nav>
<main>
<header class="plan">
<!-- slot: KEY, TITLE, JIRA_URL, BRANCH, KIND (bounded or architectural) -->
<h1>KACP-00000: Short title of the ticket</h1>
<div class="meta">
<span><a href="https://ksense-tech.atlassian.net/browse/KACP-00000">Open in Jira</a></span>
<span>Branch <code>KACP-00000-Short-Title</code></span>
<span>Classified <code>bounded</code></span>
</div>
</header>
<section id="criteria">
<h2>Acceptance criteria</h2>
<p class="mock-note">Tick a box as you confirm the plan covers it. Ticks stay in this browser only.</p>
<ul class="checklist" data-persist="criteria">
<!-- slot: criteria, one <li> per criterion, restated precisely from the ticket -->
<li><input type="checkbox" id="ac-1"><label for="ac-1">First acceptance criterion</label></li>
</ul>
</section>
<section id="questions">
<h2>Open questions</h2>
<!-- slot: questions. Use class="callout" with one <li> per question that needs an answer
before or during implementation. With none, keep the quiet version below instead. -->
<div class="callout">
<ul>
<li>A question whose answer changes the plan. Name the options and which one the plan assumes.</li>
</ul>
</div>
<!-- <div class="callout quiet">None. Nothing here needs an answer before implementation.</div> -->
</section>
<section id="approach">
<h2>Approach</h2>
<!-- slot: approach, the plan.md narrative as <p> blocks; <code> for file and symbol names -->
<p>What will change, why, which files and modules are involved, and how it fits the patterns already in the codebase.</p>
</section>
<!-- slot: mocks. Keep this section only when a step changes what a user sees.
One <template> per artboard: a <style> block followed by the screen's markup, rendered
in its own iframe so nothing leaks between the mock and this page.
Several artboards when the direction is the user's call, one when it is settled.
The data-note attribute states what the artboard shows that the implementation will not do. -->
<section id="mocks">
<h2>Mocks</h2>
<div class="mock-bar" role="tablist">
<button type="button" data-mock="mock-a" aria-pressed="true">Option A</button>
<button type="button" data-mock="mock-b" aria-pressed="false">Option B</button>
<span class="spacer"></span>
<button type="button" data-width="desktop" aria-pressed="true">Desktop</button>
<button type="button" data-width="tablet" aria-pressed="false">Tablet</button>
<button type="button" data-width="phone" aria-pressed="false">Phone</button>
</div>
<div class="mock-stage" data-width="desktop">
<iframe title="Mock" sandbox="allow-same-origin"></iframe>
<div class="mock-note"></div>
</div>
<template id="mock-a" data-note="Option A keeps the existing layout and adds the new column at the end.">
<style>
body { margin: 0; padding: 24px; font: 14px/1.5 system-ui, sans-serif; color: #1c1c1a; background: #fff; }
</style>
<h1 style="font-size:18px;margin:0 0 12px">Screen name</h1>
<p>Draw the intended screen here with plain HTML and its own style block.</p>
</template>
<template id="mock-b" data-note="Option B moves the action into the row menu.">
<style>
body { margin: 0; padding: 24px; font: 14px/1.5 system-ui, sans-serif; color: #1c1c1a; background: #fff; }
</style>
<h1 style="font-size:18px;margin:0 0 12px">Screen name</h1>
<p>Second option.</p>
</template>
</section>
<section id="steps">
<h2>Steps</h2>
<ul class="checklist" data-persist="steps">
<!-- slot: steps, one <li> per step, roughly one commit each.
Add <span class="badge mock">mock</span> or <span class="badge deliverable">deliverable</span>
after the label text for steps marked that way in plan.md. -->
<li><input type="checkbox" id="step-1"><label for="step-1">First step</label></li>
<li><input type="checkbox" id="step-2"><label for="step-2">A step that builds a mock</label><span class="badge mock">mock</span></li>
</ul>
</section>
<section id="risks">
<h2>Risks and edge cases considered</h2>
<ul>
<!-- slot: risks, one <li> per edge case: the case, then how the plan handles it -->
<li><strong>Edge case.</strong> How the plan handles it.</li>
</ul>
</section>
<section id="tests">
<h2>Test plan</h2>
<ul>
<!-- slot: tests, one <li> per item: which criterion or ticket test case it proves and how -->
<li><strong>Criterion or ticket case.</strong> The suite to run or the test to add, and what it proves.</li>
</ul>
</section>
</main>
</div>
<script>
(() => {
const key = location.pathname;
// Checkbox ticks persist per page in this browser. Storage can be unavailable on file://
// in some browsers, so every access is guarded.
const store = {
get(name) { try { return JSON.parse(localStorage.getItem(key + ':' + name) || '[]'); } catch { return []; } },
set(name, ids) { try { localStorage.setItem(key + ':' + name, JSON.stringify(ids)); } catch {} },
};
const lists = [...document.querySelectorAll('.checklist[data-persist]')];
const progress = document.getElementById('progress');
const paint = () => {
const boxes = lists.flatMap(l => [...l.querySelectorAll('input[type="checkbox"]')]);
boxes.forEach(b => b.closest('li').classList.toggle('done', b.checked));
if (progress) progress.textContent = boxes.filter(b => b.checked).length + ' of ' + boxes.length + ' reviewed';
};
lists.forEach(list => {
const name = list.dataset.persist;
const saved = new Set(store.get(name));
list.querySelectorAll('input[type="checkbox"]').forEach(box => {
box.checked = saved.has(box.id);
box.addEventListener('change', () => {
const ids = [...list.querySelectorAll('input:checked')].map(b => b.id);
store.set(name, ids);
paint();
});
});
});
paint();
// Mock tabs and width toggle. Each <template> is a full HTML document loaded into the iframe.
const mocks = document.getElementById('mocks');
if (mocks) {
const frame = mocks.querySelector('iframe');
const stage = mocks.querySelector('.mock-stage');
const note = mocks.querySelector('.mock-note');
const press = (buttons, active) => buttons.forEach(b => b.setAttribute('aria-pressed', String(b === active)));
const mockButtons = [...mocks.querySelectorAll('button[data-mock]')];
const widthButtons = [...mocks.querySelectorAll('button[data-width]')];
const fit = () => {
try {
const doc = frame.contentDocument;
if (doc && doc.documentElement) frame.style.height = Math.max(320, doc.documentElement.scrollHeight + 2) + 'px';
} catch {}
};
const show = id => {
const tpl = document.getElementById(id);
if (!tpl) return;
frame.srcdoc = tpl.innerHTML;
note.textContent = tpl.dataset.note || '';
press(mockButtons, mockButtons.find(b => b.dataset.mock === id));
};
frame.addEventListener('load', fit);
mockButtons.forEach(b => b.addEventListener('click', () => show(b.dataset.mock)));
widthButtons.forEach(b => b.addEventListener('click', () => {
stage.dataset.width = b.dataset.width;
press(widthButtons, b);
setTimeout(fit, 50);
}));
if (mockButtons[0]) show(mockButtons[0].dataset.mock);
}
// Highlight the section in view in the nav.
const links = [...document.querySelectorAll('nav a[href^="#"]')];
const byId = new Map(links.map(a => [a.getAttribute('href').slice(1), a]));
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
const a = byId.get(e.target.id);
if (a && e.isIntersecting) { links.forEach(l => l.classList.remove('active')); a.classList.add('active'); }
});
}, { rootMargin: '-10% 0px -70% 0px' });
byId.forEach((_, id) => { const el = document.getElementById(id); if (el) observer.observe(el); });
})();
</script>
</body>
</html>