diff --git a/.claude/hooks/lib.sh b/.claude/hooks/lib.sh new file mode 100755 index 0000000..bea93a8 --- /dev/null +++ b/.claude/hooks/lib.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Shared helpers for the PreToolUse guards. + +# JSON-escapes its argument on stdout without jq - deny() has to work when jq is the very +# thing that is missing. Denial reasons are literals authored in these scripts, so backslash, +# quote and the whitespace that would otherwise break the string are the whole alphabet. +json_escape() { + printf '%s' "$1" \ + | LC_ALL=C sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \ + | LC_ALL=C tr '\t\r' ' ' \ + | LC_ALL=C awk 'BEGIN { ORS = "" } NR > 1 { print "\\n" } { print }' +} + +deny() { + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' \ + "$(json_escape "$1")" + exit 0 +} + +# Fail closed. Without jq, read_command() returns empty, every guard clears its own +# `[ -n "${CMD}" ]` check and exits 0 - a silent pass on exactly the commands these guards +# exist to stop, with nothing printed to say the protection is gone. A guard that cannot +# read the command has not checked it, so it denies instead. +command -v jq >/dev/null 2>&1 || deny "Blocked by repository policy: the agent guards cannot run because 'jq' is not installed, and a guard that cannot read the command has not checked it. Install jq (macOS: brew install jq; Debian/Ubuntu: apt-get install jq - see https://github.com/repository-standards/core/blob/main/docs/method/prerequisites.md) and retry." + +read_command() { + jq -r '.tool_input.command // ""' +} + +# Splits a command line into segments on ; && || | and newlines. +# +# Every guard evaluates segments independently. Judging the whole string lets one harmless segment +# vouch for a dangerous one - `psql -h localhost -c 'select 1' && psql -h prod -c 'DROP TABLE x'` +# reads as local because `localhost` appears somewhere in it. +split_segments() { + printf '%s' "$1" | sed -E 's/(\|\||&&|[;|])/\n/g' +} + +# Matches only when the host ends at the match, so localhost.evil.example.com is not loopback. +LOCAL_HOST_RE='(localhost|127\.0\.0\.1|\[::1\]|::1)([:/[:space:]"'"'"']|$)' diff --git a/.claude/hooks/no-ci-secret-writes.sh b/.claude/hooks/no-ci-secret-writes.sh new file mode 100755 index 0000000..ead3947 --- /dev/null +++ b/.claude/hooks/no-ci-secret-writes.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Denies changes to CI secrets and variables. +set -uo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Fail closed on a missing or unreadable lib.sh: without it deny() is undefined, read_command +# is undefined, CMD comes out empty and the guard exits 0 - protection gone, nothing printed. +. "${DIR}/lib.sh" || { + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked by repository policy: the agent guard could not load .claude/hooks/lib.sh, so this command was never checked."}}\n' + exit 0 +} + +CMD=$(read_command) +[ -n "${CMD}" ] || exit 0 + +while IFS= read -r segment; do + [ -n "${segment}" ] || continue + + if printf '%s' "${segment}" | grep -qiE 'gh[[:space:]]+(secret|variable)[[:space:]]+(set|delete|remove)'; then + deny "Blocked by repository policy: CI secrets and variables are not edited from here. Rotating or changing one is a deliberate human action in the repository settings." + fi + + if printf '%s' "${segment}" | grep -qi 'gh[[:space:]]\+api' \ + && printf '%s' "${segment}" | grep -qiE 'secrets|variables' \ + && printf '%s' "${segment}" | grep -qiE '(-X|--method)[[:space:]=]*(DELETE|PATCH|PUT|POST)|(^|[[:space:]])-(f|F)[[:space:]]|--field|--raw-field|--input'; then + deny "Blocked by repository policy: CI secrets and variables are not edited from here. Rotating or changing one is a deliberate human action in the repository settings." + fi +done <spec drift findings not fixed in the current + change: one item per finding, source = the drift finding. + +In all three cases the agent writes well-formed rows itself using the format below, +de-duplicating against existing items first. + +## Steps + +1. **Check it is a real item.** It must trace to a source: a spec delta, a code<->spec + drift finding, a missing decision, onboarding, or an explicit request. A vague wish + with no source is not a backlog item - drop it. + +2. **De-duplicate.** Scan the ledger; if the item (or a superset of it) is already + there, stop - do not file a second one. Sharpen the existing row instead if needed. + +3. **Place and name it.** Pick the epic it belongs to (or note a genuinely new epic). + Give it a stable, scoped id (`SPEC-3`, `ADR-auth`, `DRIFT-2`) that will not be reused. + +4. **Write the row** with every column the backlog declares: `id`, `title`, `cap` (the + capability it touches, or `-`), `persona` (from `docs/personas.md` - an item that serves + no persona is parked, not queued), `owner` (the **role** that must act: `product`, + `architect`, `dev` or `agent`), `why` (one line), `DoD` (the observable finish line - + "spec is buildable", "ADR Accepted", "drift resolved"), `status: todo`. A row missing + `cap`, `persona` or `owner` fails the backlog's own Definition of Ready, so it cannot be + pulled - writing it short only moves the work to whoever pulls it. Slot it by + **risk x leverage** (money / security / external contracts / data integrity first; then + churn), not at the bottom by default. + + Leave `assignee` empty - a pool item is nobody's yet (ADR-030). Offer a `size` of `S`, `M` + or `L` if the shape is clear enough to be worth one, and skip it otherwise: an unsized row + is a normal row, not an incomplete one. If it looks like an `L`, say so and suggest the + split now, while the work is still fresh in mind - that is the whole job the field does + (ADR-029). + +5. **Do not do the work now**, and do not make the decision here - "write an ADR for X" + is a backlog item; the decision itself is made in the ADR when the item is worked. + +## Not this + +- Not a dumping ground for vague ideas - no source, no DoD, no item. +- Not a duplicate tracker - reconcile with the existing row instead of adding another. +- Not a second issue tracker kept in sync by hand - this is the in-repo, agent-first + view; mirror to an external tracker only if the team already lives there. diff --git a/.claude/skills/adr-write/SKILL.md b/.claude/skills/adr-write/SKILL.md new file mode 100644 index 0000000..6aac7de --- /dev/null +++ b/.claude/skills/adr-write/SKILL.md @@ -0,0 +1,78 @@ +--- +name: adr-write +description: Use when a technical choice gets made that someone will argue about again - "we went with Postgres over Mongo", "we're dropping the queue", "let's use Fastify", "why did we do it this way?". Drafts the decision record from what you say plus what the code and discovery notes already show, then asks only what neither can answer. +--- + +# adr-write + +A technical decision was made. This writes the record **with** the user rather than handing +them a template - they say what they remember, the agent drafts, they correct. + +## Is it an ADR at all? + +Two questions, in this order: + +1. **Would someone reasonably have chosen otherwise?** If not, it is a convention, and it + belongs in `AGENTS.md` where the next person will look. One obviously-right answer does + not need a record; it needs a line in the rules. +2. **Who would overrule it - an architect or a product owner?** An architect means ADR. A + product owner means **BDR** and `bdr-write` is the skill. The test itself is R5, and the + standard works it through in [decision records (by reference)](https://github.com/repository-standards/core/blob/main/docs/tree/docs-decision-records.md). + If both would overrule it, write the BDR and let the ADR reference it. + +Say which way you routed and why, in one line. A user who disagrees will say so, and that is +cheaper than discovering it in review. + +## Draft first, ask second + +**Read before asking.** The code already shows what was chosen - the dependency, the +migration, the config. `docs/discovery/` may hold the meeting where it was argued. An +existing spec may state the consequence. Draft from those, then ask only what they cannot +answer, and say where each drafted part came from so the user can correct a wrong inference +instead of re-typing a right one. + +## What an ADR wants, and what to ask for each + +- **Context - the forces.** Not "we needed a database". What made this contestable: the + constraint, the deadline, the thing that broke, the disagreement. Ask: *what made this a + decision rather than an obvious step?* +- **Options considered - at most three, with why the losers lost.** This is the part people + skip and the part that pays. A record listing one option is a note. Ask: *what else was on + the table, and what killed it?* If genuinely nothing else was considered, write that - + "no alternative was evaluated" is a fact a future reader needs. +- **Decision.** One paragraph, in the present tense, as a rule the repo now follows. +- **Consequences.** What this makes easy, what it makes hard, and what someone now has to + live with. Push for the second and third: a consequences section with only upsides is + advertising. +- **Confirmation - how compliance is verified.** A guard, a test, a review step, or honestly + "review only". This is what stops the record being decoration. +- **Revisit when.** The concrete signal that reopens it - a scale threshold, a version, a + cost. Not "if it becomes a problem". Ask: *what would have to be true for us to change our + minds?* + +## Then + +Number it `ADR-NNN` - gapless, never reused. Find the next free number by **reading the +directory** (`ls docs/decision-records/adr/` or wherever this repo's ADRs actually live), +never the README table's row count and never a remembered count - the two can disagree, and +`scripts/decision-records-check.mjs` exists because a stale or missing index row let them. +Do this **as of the latest `main`**, right before you write the file: `git fetch` and +re-check the number is still free immediately before committing, not only when you started - +two branches minting the same number from an older `main` is a real collision this project +has hit, and rebasing late narrows the window but does not close it. Copy +`docs/decision-records/adr/_template.md`, add the row to `adr/README.md`, set +`Status: Accepted` when the user confirms. + +**Never edit an accepted record into a different decision.** If this supersedes one, the old +record keeps its text, flips to `Superseded`, and names this one in its `Superseded by` row. + +## Not this + +- **Do not write a record for a decision nobody made.** If the user is thinking out loud, it + is an idea (`docs/ideas/`) until they approve it - `Proposed` is for a decision awaiting + ratification, not for a maybe. +- **Do not invent the options.** Reconstructing what was on the table is the user's memory or + the discovery dossier's, and an invented rejected option is worse than none: it will be + cited later as if it were considered. +- **Do not bury the decision in the context.** If a reader cannot answer "what did we decide" + from the Decision section alone, it is not written yet. diff --git a/.claude/skills/bdr-write/SKILL.md b/.claude/skills/bdr-write/SKILL.md new file mode 100644 index 0000000..0c7c1fa --- /dev/null +++ b/.claude/skills/bdr-write/SKILL.md @@ -0,0 +1,74 @@ +--- +name: bdr-write +description: Use when a product or business call gets made - "we're charging per seat, not per user", "we're not supporting self-hosting", "this launches to the agency segment first", "we decided to drop the free tier". Drafts the record from what you say plus the product frame and discovery notes, then asks what neither can answer. +--- + +# bdr-write + +A business or product decision was made. This writes the record **with** the user - they say +what was decided and roughly why, the agent drafts, they correct. + +## Is it a BDR? + +Ask who would overrule it. **A product owner means BDR; an architect means ADR** and +`adr-write` is the skill. If both would, the BDR is the parent and the ADR references it - +"we charge per seat" is the decision, "we model seats in the licences table" is its +technical consequence, and conflating them is how a pricing change later reads as a schema +change. + +Say which way you routed, in one line. + +## What a BDR wants - and why the questions differ from an ADR's + +An ADR asks about forces and options. A business decision has those too, but the parts that +decide whether it was a good one are different, and asking an ADR's questions produces a BDR +that reads like an ADR with the wrong nouns. + +- **Who it serves - by name from `docs/personas.md`.** Which persona is better off, and which + one is not. A decision that serves everyone equally is usually one nobody made. Ask: *who + is this for, and who loses?* +- **What changes for them.** Concretely, in their words: what they can now do, what it costs + them, what they will notice. Not "improved flexibility". +- **What it costs us.** Money, scope, a door closed, a segment given up. Ask directly: *what + are we giving up by doing this?* A BDR with no cost is a wish. +- **What we are deliberately not doing.** The non-goals that come with it. This is the + section that stops the decision quietly expanding for a year. +- **How we would know we were wrong.** The observable signal - a churn number, a support + pattern, a segment that never converts. Ask: *what would we see in three months if this was + the wrong call?* Push for something checkable; "if it doesn't work out" is not one. +- **Revisit when.** Usually the same signal, with a date or a threshold attached. + +These are the template's own sections - `Who it serves`, `What this rules out` and `How we +would know we were wrong` exist in the BDR template and not in the ADR one, which is the +whole reason the two records are not the same form with different nouns. + +## Draft first, ask second + +Read `docs/PRODUCT.md` for the frame this sits in, `docs/personas.md` for who is affected, +and `docs/discovery/` for the conversation where it was argued. Most BDRs are the written +form of something already half-recorded. Draft from that and say where each part came from. + +If the decision contradicts `PRODUCT.md`'s stated goals or non-goals, **say so before +writing** - either the product frame moved and should be updated in the same change, or this +decision is not yet made. + +## Then + +Number `BDR-NNN` - gapless, never reused. Find the next free number by **reading the +directory** (`ls docs/decision-records/bdr/`), never the README table's row count and never a +remembered count - the two can disagree, and `scripts/decision-records-check.mjs` exists +because a stale or missing index row let them (it minted a second `BDR-004` once). Do this as +of the latest `main`, right before writing: `git fetch` and re-check the number is still free +immediately before committing - two branches minting the same number from an older `main` is +a real collision this project has hit. Copy `docs/decision-records/bdr/_template.md`, add the +row to `bdr/README.md`, `Status: Accepted` on confirmation. An accepted record is superseded, +never edited into a different decision. + +## Not this + +- **Do not write the technical consequence here.** Which table, which service, which library + is an ADR. If the record starts naming files, it has drifted. +- **Do not soften the cost.** The section exists so a future reader can tell whether the + trade was worth it, and a BDR whose costs are all upside teaches them the records lie. +- **Do not record a decision the user is still thinking about.** That is an idea + (`docs/ideas/`) until approved. diff --git a/.claude/skills/cycle-close/SKILL.md b/.claude/skills/cycle-close/SKILL.md new file mode 100644 index 0000000..e449acb --- /dev/null +++ b/.claude/skills/cycle-close/SKILL.md @@ -0,0 +1,106 @@ +--- +name: cycle-close +description: Use when a stretch of work ends - "close the cycle", "the sprint is over", "we shipped what we were going to". Checks each intent against its definition of done, returns what did not finish to the backlog, and records the one measurement of the cycle that cannot be recovered afterwards. +--- + +# cycle-close + +A cycle is ending. This is the step that keeps the backlog true and leaves behind the only +execution record the repo keeps. *(scale profile only.)* + +## Why the record is written at all + +ADR-010 decided that work history lives in the tracker, not the repo, and it was right about +per-item state. ADR-028 narrowed it for exactly one thing, and the argument matters here +because this skill is what writes it: **the grouping is not recoverable afterwards.** Git +can always count commits between two dates. It cannot reconstruct that *these seven intents* +were what the team believed it would finish - the pool mutates, items get re-scoped, and the +version of the backlog that made the plan is only reachable by knowing which commit to read. + +So this writes one block. Not a log, not per-item history. If you find yourself recording +who did what, stop - that is the tracker's. + +## Steps + +0. **Resolve the backlog path once, the way the guard does.** Check `docs/backlog.md` first, + then `backlog.md` (the manifest's primary path, R15) - whichever exists is the pool for + every step below. + +1. **Check each intent against its own definition of done, and say what you checked.** The + DoD is in the row. "The spec is buildable", "the ADR is Accepted", "the drift is + resolved" - these are verifiable, so verify them rather than asking the user whether it + feels finished. Report per item: met, not met, or **cannot tell from here** (then say + what would settle it). Refusing to close on a cycle whose items were never checked is the + point of this step; a close that rubber-stamps teaches everyone the DoD column is + decoration. + +2. **Return what did not finish - and split it rather than re-sizing it.** Unfinished rows are + **cut** back into the backlog (step 0) at their risk x leverage position, not appended to + the bottom, which quietly demotes work that was important enough to commit to. Clear the + `assignee` on the way out: the pool holds nobody's work (ADR-030). + + An item that did not fit is **split into what finished and what remains**, not given a + bigger size (ADR-029). Propose the split; the user confirms it. Cut a **new** backlog row + for what remains (its own id, its own DoD for the remainder), and set the original row's + status in this cycle to **`split:`** - the template's status vocabulary, not a + spelling invented per repo. This is what keeps item counts comparable without an + estimation currency, and skipping it is how throughput quietly stops meaning anything. + +3. **Report rows whose status never moved.** An item that entered the cycle `todo` and left it + `todo` is the one worth naming - usually blocked without a `blocked:` reference, or + assigned to someone who was never really on it. State the observation, not the cause. + +4. **Ask the one question the data cannot answer**: did anything get done that was never in + the cycle? If yes, it goes into the outcome block as a count and a line, because a cycle + that "missed" three items while absorbing two emergencies is not a cycle that + underdelivered, and a timeline built without that reads the team as slower than it is. + +5. **Write the outcome block**, once, in the cycle file: + - planned, finished, returned to the pool + - **returned to the pool: name the ids**, not only the count (`Returned to the pool: PAY-7, + PAY-9`, or `Returned to the pool: none`) - `cycle-guard` checks that every id named here + actually landed back in the backlog (step 0), and it can only do that if the row says + which ones + - unplanned work absorbed, if any + - commits in the window: `git log --oneline --since= --until= | wc -l` + - days elapsed, opened to closed + Flip `Status` to `closed` and record the actual close date, which is often not the target. + +6. **Remove the pointer row** from the backlog's active-cycles table. + +7. **Prove it.** `node scripts/cycle-guard.mjs --block` - every returned row must now be in + exactly one place again. + +8. **Offer the retrospective the data supports, and no more.** Say what the numbers show - + "planned seven, finished four, absorbed two unplanned" - and stop. Do not narrate why. + A single cycle is one data point; `/timeline-update` is what turns several into + throughput, and it refuses to project from too few. + +## Show the close, do not just file it + +This is the moment a team looks at, so end with a table a person can read: each intent, its +assignee, whether its DoD was met, and where it went (stayed / returned / split). Then the +counts in one line - planned, finished, returned, unplanned absorbed, days elapsed. + +That table is the artifact someone screenshots into a channel. It should need no editing, and +it should not hide the rows that did not make it. + +## Done when + +- [ ] Every intent was checked against its DoD and the result reported +- [ ] Unfinished rows are back in the pool at their position, not at the bottom, with the + assignee cleared +- [ ] Anything that overran was split, not re-sized +- [ ] The outcome block is written and `Status` is `closed` +- [ ] The pointer row is gone from the backlog +- [ ] `cycle-guard --block` passes +- [ ] A readable close table was shown, including what did not finish + +## Not this + +- **Do not close a cycle to tidy up.** An open cycle past its date is honest and the + timeline reports it. A cycle closed with unmet items marked done is a lie the estimation + arithmetic then inherits. +- **Do not write per-item history.** One aggregate block. Who did what, when it moved, how + long review took - the tracker's, unchanged. +- **Do not compute velocity here.** One cycle is not a rate. diff --git a/.claude/skills/cycle-open/SKILL.md b/.claude/skills/cycle-open/SKILL.md new file mode 100644 index 0000000..411f949 --- /dev/null +++ b/.claude/skills/cycle-open/SKILL.md @@ -0,0 +1,115 @@ +--- +name: cycle-open +description: Use when a team is picking up work for the next stretch - "let's start a cycle", "what are we doing this month", "pull the top three payment items into a sprint". Creates the cycle with its goal and agreed end date, and moves the chosen intents out of the backlog pool so each one lives in exactly one place. Also owns reading an open cycle back as a status board ("how is the dispatch cycle going?") and mid-cycle edits - moving a status, reassigning a holder - that happen between opening and closing. +--- + +# cycle-open + +A team is committing to a stretch of work. This creates the cycle and moves the intents +into it. *(scale profile only - a `core` repo has a backlog and needs nothing else.)* + +What a cycle is, and what belongs in one: [cycles (by reference)](https://github.com/repository-standards/core/blob/main/docs/tree/docs-cycles.md). +This is how one is opened. + +## Steps + +0. **Resolve the backlog path once, the way the guard does.** Check `docs/backlog.md` first, + then `backlog.md` (the manifest's primary path, R15) - whichever exists is the pool for + every step below. Do not assume `docs/backlog.md`; a repo that satisfies the manifest at + its primary path has no other file to read. + +1. **Which team, and is there already an open cycle for them?** Look under `docs/cycles/`. + A team with one already open is the common case worth catching: ask whether this is a + second parallel cycle (legitimate - a team can run two threads) or whether the open one + should be closed first. Do not open a duplicate silently. + +2. **Ask for the goal, and push back once if it is a list.** The goal is the outcome, not + the items: "checkout stops losing carts", never "do PAY-2, PAY-3 and PAY-7". If the + answer restates the items, ask what becomes true when they are done - a cycle whose goal + is its own contents tells the timeline nothing and tells the team nothing either. + +3. **Ask for the target date, and say what it is.** Agreed and movable, not a deadline; + nothing enforces it and the timeline reports a cycle past its date rather than failing + it. If the user has no date in mind, propose one from the last closed cycle's length - + and say that is where it came from. + +4. **Propose the intents, do not ask for a list.** Read the backlog (step 0) and offer the top + items by the order already there (risk x leverage), grouped by capability, with the count + the team can realistically hold if past cycles give any evidence. The user corrects a + proposal far faster than they assemble one. Confirm before moving anything. + +5. **Move the rows, do not copy them.** Each chosen row is **cut** from the backlog and + pasted into the cycle file unchanged - same columns, same values. An intent lives in + the pool or in exactly one cycle, and copying is how that stops being true. + + One cell fills on the way in: **`assignee`**, the person taking it (ADR-030). Ask for the + names once, as a set, rather than row by row. A row arriving with no assignee is work the + cycle has not really committed to - say so, and accept it if the team means it, because a + deliberately unassigned item is honest and a silently unassigned one is not. + + **Flag any `L` that is being pulled unsplit.** An `L` means split before pulling + (ADR-029); pulling one anyway is a choice the team can make, but not one to make by + accident. + +6. **Write the cycle file** from [`docs/cycles/_template.md`](../../../docs/cycles/_template.md) + at `docs/cycles//.md` - lowercase kebab-case, a slug that will still mean + something in six months (`2026-08-checkout`, not `sprint-4`). + + Keep the template's structure: the rows sit under the `## Intents` H2, the id is the first + cell and the status is the last. That is `cycle-guard`'s interface, not a house style - it + reads intents only from that section, and a file that renames or drops the heading yields + no rows at all, which is indistinguishable from a cycle with nothing wrong in it. + +7. **Add the pointer row** to the backlog's active-cycles table: team, goal, target, + link, item count. The pool stays the single entry point without duplicating a row. + +8. **Prove it.** Run `node scripts/cycle-guard.mjs --block`. A failure here means a row was + copied rather than moved, and it is the whole reason the guard exists. + +## Done when + +- [ ] The cycle file exists with a goal that is an outcome, a target date, and its rows +- [ ] Every moved row is **gone** from the backlog (whichever path resolved in step 0) +- [ ] The pool carries a pointer row for the new cycle +- [ ] `cycle-guard --block` passes + +## Not this + +- **Do not invent intents.** A cycle holds items that were already in the pool. Work + discovered while opening a cycle goes through `add-to-backlog` first, then in - so it + keeps its source and its definition of done. +- **Do not record who *used to* hold an item.** `assignee` is present tense and reassignment + overwrites it; the trail of who touched what is the tracker's (ADR-010, narrowed by + ADR-030). If a team needs that trail, they need a tracker. +- **Do not assign in the pool.** An item nobody has picked up is not yet anyone's, and a pool + that assigns work has quietly become a queue of orders. +- **Do not set a length policy.** Two-week cycles are a choice a team may make; the standard + has no opinion and should not grow one. + +## Reading a cycle back + +A cycle is a markdown file - read it directly for one row's answer. For "how is the dispatch +cycle going", render it as a board instead, because a person acts on the shape faster than on +a table: group every row under `## Intents` by its status cell into three lanes, `done`, +`doing` (a `blocked:` row stays in `doing` and says what it is waiting on), and `todo`. +Show id, title and holder (`assignee`) per row - the three things the file exists to answer. +An empty `assignee` is worth naming, the same way the file itself calls that a gap. + +This is a grouping of what the row already stores, nothing computed and no date attached - +that is `timeline-update`'s job, and it correctly refuses to project an open cycle. This only +shows what the file says right now. + +## Editing a cycle mid-flight + +Between opening and closing, a cycle changes hands and status without either boundary skill +running. These are plain table edits - say so precisely rather than leaving it to inference: + +- **Moving a status** ("PAY-3 is done now", "start on PAY-4"): edit that row's status cell in + place - the last cell, whatever the column count. Use `blocked:` when it is waiting on + another intent named by id, plain `todo` / `doing` / `done` otherwise. +- **Reassigning a holder** ("give PAY-3 to Ravi"): overwrite the `assignee` cell in place. It + is present-tense state, not a log (ADR-030) - the previous holder is not kept here; a team + that needs that trail needs a tracker. +- **Prove it after either edit.** `node scripts/cycle-guard.mjs --block` - a status edit that + invents a `blocked:` pointing at nothing, or an edit that duplicates the row instead of + changing it in place, is exactly what the guard exists to catch. diff --git a/.claude/skills/discovery-digest/SKILL.md b/.claude/skills/discovery-digest/SKILL.md new file mode 100644 index 0000000..50b219b --- /dev/null +++ b/.claude/skills/discovery-digest/SKILL.md @@ -0,0 +1,111 @@ +--- +name: discovery-digest +description: Use when someone hands over raw material rather than a request - meeting notes, a mail thread, a transcript, "here's what they said on the call", a half-decision nobody wrote down. Files it into the topic's dossier with its provenance, flags where it contradicts what is already recorded, and says whether the topic is ready to be specced. Never writes specs itself. +--- + +The curator of `docs/discovery/` (ADR-024). This skill maintains dossiers; it +**never writes specs** - when a topic is ripe, it says so and hands off to +`/spec-specify`. The user's habit is one line: "had a meeting? drop the +extract" - everything else is this procedure. + +## User Input + +```text +$ARGUMENTS +``` + +The input is raw material (pasted notes, a transcript, a mail, a finding) plus +enough context to name the topic. If the topic is ambiguous - the material +could belong to more than one existing dossier - ask the user which; never +guess between dossiers. + +**If more than one source landed in the same handover** (a Slack thread and a mail +pasted together, "here's what Dana said and here's what Marcus said back") - each +source is still its own entry, written and filed one at a time, in the order given. +Diff the second against the dossier **and** against the first before it moves on to +the third: two sources from the same day that disagree with each other are exactly +the kind of contradiction step 3 exists to catch, and they only surface if each +entry is diffed against everything already filed, including entries this same +handover just added. + +## Procedure + +1. **Resolve the dossier.** Slugify the topic (`booking-changes`, not a spec + or ticket name - a dossier is per discovery topic, ADR-024). If + `docs/discovery//` does not exist, create it with a `README.md` + holding: a one-paragraph summary, `Last reconciled: never`, an empty + entries list, and an empty `## Contradictions to resolve` section. + +2. **Write the entry - essence, not transcript.** Create + `docs/discovery//YYYY-MM-DD-.md` (source names where it came + from: `kickoff-meeting`, `mail-from-`, `support-ticket-123`). Content: + - a provenance line: date, source, participants/author, and a link to the + raw material (recording, thread, mail) - the raw itself stays OUT of the + repo (volume, noise, personal data); + - the essence as attributable points: *who* said *what mattered* - + decisions argued (and whether they were settled), constraints stated, + numbers given, promises made. Keep the "it was said at THAT meeting" + value; drop the small talk. + +3. **Update the dossier README.** + - Refresh the summary if the material moved the topic. + - Add the entry to the entries list with state `new`. + - **Diff against every earlier entry**: where the new material contradicts + an earlier entry or an assumption ("kickoff assumes same-day refunds; + this mail says T+3"), add a row under `## Contradictions to resolve` + naming both sources. Do not resolve it yourself - contradictions are for + the humans in the next round (or a clarify question when the spec drafts). + - Never touch the `Last reconciled:` stamp - only the `spec-*` skills move + it, when they fold the dossier into a spec. + +4. **Check it against every `Revisit when` (grep, not judgment).** Every ADR/BDR + carries a `## Revisit when` field naming the concrete signal that should reopen + it - nobody read it back before this step existed, so a decision could sit past + its own trigger with only an agent's own unbroken context noticing (a fresh + agent, or the same agent on a later date, would not). Pull each record's + `Revisit when` line across `docs/decision-records/` (or wherever this repo's + records live) and check the new entry's text against it - a textual match ("10k + customers", "self-hosting", a named competitor) is a hit; this is a grep, not a + semantic read of every record. On a hit, add a row under a `## Revisit signals + hit` section (create it if this dossier has never needed one) naming the record + and the matching text, and carry it into the readiness report below - resolving + it (write the superseding record, or decide the signal does not really apply) is + a human call, exactly like a contradiction. **What this catches:** a condition + whose wording shows up in the new material. **What it cannot catch:** a signal + that is true in the world but never gets written down here, or one worded so + differently from the record that no grep finds it - a tripwire, not a monitor. + +5. **Route what is already ripe.** If the material contains a *settled* + decision (a fork was taken, on the record), offer to draft the ADR/BDR now - + consent-gated, the user says yes or no. If it contains a clear work item, + offer the backlog. Everything else stays in the dossier as material. + +6. **Report readiness.** End with a one-paragraph status: how many entries are + `new`/`open` vs consumed, the open contradictions, any `Revisit when` signal + hit (step 4), and a verdict - "ripe + for `/spec-specify`" (core questions answerable, actors and boundaries + visible) or "still discovering" (name what is still missing). If a spec + already exists for this topic, say instead: "spec exists - route this + through `/spec-clarify` / `/spec-impact`" and name the entries newer than + the stamp - **unless its `Status` is `retired`**, in which case say so and stop: + new material about a retired capability is either about the vendor/replacement + now doing the job (out of scope here) or a case for a genuinely new capability, + not a reason to reopen this one. + +## Hard rules + +- A dossier is **never normative**: if material differs from an existing spec + or accepted record, note it as history or a contradiction row - never as + "the spec is wrong", and never edit a spec or record from this skill. +- One dossier per topic; entries are append-only (fix a typo, yes; rewrite + history, no). The dossier README is the only file this skill rewrites. +- Personal data discipline: the extract carries roles and first names at most; + full transcripts, recordings and attachments stay in their tools, linked. + +## Done When + +- [ ] The entry file exists, provenance-stamped, essence-only, raw linked +- [ ] The dossier README lists it (`new`), summary current, contradictions diffed +- [ ] The new material was checked against every decision record's `Revisit when`; any hit is on the record and in the readiness report +- [ ] Ripe decisions/work items offered onward (consent-gated), not silently taken +- [ ] Readiness verdict reported ("ripe for spec-specify" / "still discovering" / "route via clarify") diff --git a/.claude/skills/idea-write/SKILL.md b/.claude/skills/idea-write/SKILL.md new file mode 100644 index 0000000..db7d9b5 --- /dev/null +++ b/.claude/skills/idea-write/SKILL.md @@ -0,0 +1,88 @@ +--- +name: idea-write +description: Use when someone floats something that might never ship - "what if we let hosts pre-approve repeat guests", "should we offer a paid tier", "I wonder whether we need multi-currency". Captures it end-to-end (including its provisional technical/business shape) in docs/ideas/ without minting a record or a spec, and moves it through idea -> exploring -> approved | parked | dropped as it firms up. +--- + +# idea-write + +Someone is thinking out loud about something that might never happen. This gives that +first-class treatment - captured in the repo, not lost in chat - **without** pretending a +decision was made (R14, ADR-010). `adr-write` and `bdr-write` both refuse to write a record +for a maybe and point here instead; this is the skill that actually catches it. + +## Is it an idea at all? + +**A decision already made** is `adr-write`/`bdr-write`'s job, not this one - if the user is +telling you what was decided, not what they are wondering about, route there instead and say +so. + +**A topic already accumulating real material** - meeting notes, mails, findings whose +provenance matters - is discovery, not an idea (`discovery-digest`, ADR-024). An idea is one +person's (or one conversation's) speculation; the moment it is being actively investigated +with sourced material, it graduates to a dossier even before anyone approves anything. + +If in doubt, ask which: "is this a decision you've made, something you want investigated, or +still just a maybe?" + +## New idea + +1. **Slugify the title** (`repeat-guest-preapproval`, not a ticket name) and check + `docs/ideas/` for an existing file first - do not fork a second doc for the same idea + under a different name. + +2. **Copy `docs/ideas/_template.md`** to `docs/ideas/.md`. Fill it from what the user + said: + - **The itch** - the problem or opportunity, one paragraph, plain language. + - **For whom** - name the persona(s) from `docs/personas.md` this would serve. An idea + that serves no persona is parked, not explored (ADR-006) - say so rather than filing it + anyway. + - **Provisional shape** - whatever the idea needs: business model, UX sketch, technical + approach, pricing. Speculation is welcome here and nowhere else in the repo; nothing in + this section is a decision. + - **Open questions** - the unknowns that decide approve vs. park vs. drop. + +3. **Set `Status: idea`**, today's date, and an owner (who is weighing this - not necessarily + who thought of it). + +## Moving it along + +The statuses are the whole lifecycle (ADR-010): `idea -> exploring -> approved | parked | +dropped`, and `approved` graduates into `graduated`. + +- **`idea -> exploring`**: someone is actively working the open questions, not just holding + the thought. Update the file in place - add what was learned, narrow the open questions. +- **`-> approved`**: the fork was taken - the idea is going to happen. **Do not stop here.** + Hand off immediately: a backlog intent for the work (`add-to-backlog`), a behavioral or + buildable spec for what it changes (`spec-specify`), and any ADR/BDR the shape now + demands (`adr-write`/`bdr-write` - a decision now exists to write). Flip the idea doc's + `Status` to `graduated` and fill the **Graduation** section with the backlog id, spec + path, and record ids - the idea doc becomes the historical "here's how we got here," the + new artifacts are where the work actually lives now. +- **`-> parked`**: not now, not never. One line on why, so the next person who has the same + thought finds this instead of re-arguing it from zero. +- **`-> dropped`**: decided against. One line on why - same reason: cheap memory beats a + re-litigated conversation. + +Never delete an idea doc. `parked`/`dropped` files are the record that the idea was +considered and what happened to it - that is the value, not the idea itself. + +## Not this + +- **Do not mint an ADR, BDR or spec for an idea that is not `approved`.** `Proposed` in a + record means a decision is awaiting ratification, not that someone is still thinking about + it - dressing speculation as a record implies a fork was taken when none was (R14). +- **Do not write only the itch and skip the provisional shape.** A one-line idea with no + technical or business shape is not explored, it is a title - the template's "one place + speculation is welcome" exists so the next conversation does not start from zero. +- **Do not leave an idea at `approved` without graduating it.** `approved` is a transition + state, not a resting one - the moment it is confirmed, the handoff to backlog/spec/records + is this skill's job to trigger, not something the next person has to notice is missing. + +## Done When + +- [ ] `docs/ideas/.md` exists, filled from what the user actually said +- [ ] `For whom` names a real persona, or the idea is marked `parked` instead +- [ ] `Status` reflects where it actually is, dated +- [ ] On approval: backlog intent + spec + any records exist, and the idea doc reads + `graduated` with links to all three +- [ ] On park/drop: one line says why diff --git a/.claude/skills/personas-write/SKILL.md b/.claude/skills/personas-write/SKILL.md new file mode 100644 index 0000000..ffc64f9 --- /dev/null +++ b/.claude/skills/personas-write/SKILL.md @@ -0,0 +1,91 @@ +--- +name: personas-write +description: Use when the repo needs to name who it is for - "we don't have personas", specs written against "the user", an existing roster that no longer matches reality, or an argument about what a feature should do that keeps stalling on who it serves. Interviews for real users, or reconstructs candidates from the code when nobody remembers. +--- + +# personas-write + +Specs in this standard are written against named personas, so a repo with no roster writes +specs against "the user" - and "the user" wants everything, which is why those specs never +settle anything. + +## Where the names come from + +Ask which of these the repo actually has, because the work differs completely. + +- **Real users the team knows.** Interview - below. Best case. +- **Nobody remembers / nobody asked.** Reconstruct **candidates** from evidence: roles in the + auth model, permission tiers, distinct entry points, admin surfaces, differently-shaped API + consumers, support tickets if present. Present them as candidates to confirm or reject, + never as findings. The code shows who the system was built for, which is a good hypothesis + and not the same thing as who uses it. +- **Only a founder's intuition.** Write it, mark it, and say plainly that it is untested. An + honest guess labelled as one is usable; a guess dressed as research is not. + +**A repo may legitimately have one persona.** Do not manufacture a roster for symmetry - +three thin personas are worse than one real one, and they will produce three thin specs. + +## What to ask for, field by field + +`docs/personas.md` fixes the fields; this skill decides how to ask for them. Do not invent a +field the template does not have, and do not skip one because the user did not volunteer it - +a persona missing its anti-goals is the one that gets gold-plated for. + +- **Who / context** - role, environment, tech comfort, constraints. Ask what their day looks + like around this task, not who they are as a person: demographics and biography change no + decision. +- **Jobs to be done** - *when \_\_\_, I want to \_\_\_, so I can \_\_\_*, in their words. The + durable part, and the part a spec cites. If the user answers with a feature, ask what it + would let the person accomplish, and write that instead. +- **Goals** - what success looks like from inside their head. +- **Pains / frictions** - what blocks the job today, and **what they must not lose**. Ask the + second half explicitly; it is the constraint a spec is checked against, and users almost + never offer it unprompted. +- **Decisions they influence** - which ADRs/BDRs this persona pulls on. Often empty at first + and filled later; leave it empty rather than guessing. +- **Success signals** - a metric, a behaviour, an outcome. "They are happy" is not one, and + the template says so. +- **Anti-goals** - what they explicitly do not need. Ask directly: *what would we be wasting + our time building for them?* + +## Where each part goes + +Two places, and mixing them up defeats the gate: + +- **The roster table** under `## The roster` - one row per persona, name in backticks, primary + marked. **`scripts/spec-structure.mjs` reads this table and only this table**, so a persona + described in detail but missing from it does not exist as far as R10 is concerned. +- **A detail block** per persona, copied from `## Persona template` as ``### `Name` ``. + +Then **delete the worked example** section. It ships filled, from a rental-property product, +and it stays in the template on purpose - but left in a real repo it lets a spec claim to +serve a persona from someone else's domain and pass the gate. + +## Draft, then check it earns its place + +For each drafted persona ask: **would any spec come out differently if this one did not +exist?** If not, it is a duplicate of another persona wearing a different job title - merge +them and say so. This is the test that keeps a roster from inflating, and it is worth running +out loud so the user can overrule it. + +Then check the reverse against `docs/PRODUCT.md`: does the product frame promise something to +someone with no persona here? That gap is either a missing persona or scope the product should +drop, and either way the user should see it now. + +## Then + +- Mark unverified assumptions `[NEEDS INPUT: ...]` rather than smoothing them over. Report how + many are left. +- Run `node scripts/spec-structure.mjs` and show the result. A roster that does not satisfy + the gate is not finished, and finding that out now costs a minute. +- Offer, in this order: `product-write` if the frame is missing or now contradicted; + `spec-specify` for a capability whose persona just became clear. + +## Not this + +- **Do not write personas for internal roles that are really job titles.** "The developer who + maintains this" is not a persona unless the product is built for them. +- **Do not let a persona become a feature list.** What they need, not what we plan to give + them - the second belongs in specs and dates within a month. +- **Do not silently upgrade a guess to a finding.** A reconstructed candidate stays labelled + as reconstructed until a human confirms it. diff --git a/.claude/skills/pre-pr-review/SKILL.md b/.claude/skills/pre-pr-review/SKILL.md new file mode 100644 index 0000000..a90256a --- /dev/null +++ b/.claude/skills/pre-pr-review/SKILL.md @@ -0,0 +1,65 @@ +--- +name: pre-pr-review +description: Use before pushing a branch or opening a pull request - "is this ready?", "can I push?". Runs the repo's local checks, then reads the diff as if someone else wrote it, and fixes what it finds first. A review after the push is a review of something already published. +--- + +# Pre-PR review + +Run this before opening a pull request. Goal: catch the obvious defects locally, +cheaply, in a fresh perspective - so the PR that reviewers (human or CI) see is +already clean. + +This does NOT replace an independent CI review: it shares the author's blind +spots and only fires when an agent that ran this skill opens the PR. Its value is +tightening the loop early, not being the gate. (The gate is CI + human review.) + +## Steps + +1. **Scope the change.** `git fetch origin` then look at the full diff against the + base branch: `git diff origin/main...HEAD`. Know exactly what you are shipping. + +2. **Check the branch shape (R23).** Three things, all from + `git log --oneline origin/main..HEAD`: + - it sits on current `main` - `git merge-base --is-ancestor origin/main HEAD` + exits 0; if not, rebase (never merge `main` in); + - no merge commit rode in - `git log --merges origin/main..HEAD` is empty; + - every commit listed belongs to *this* PR and stands on its own. Another PR's + commits in that range means you are stacked on its branch: rebase onto + `main`, or land the parent first. Squash the wip/fixup noise now + (`git rebase -i origin/main`), before review, not after. + +3. **Run the repo's local checks** (whatever this repo defines - do not invent): + format, lint, typecheck, and the unit tests the repo expects before a PR. + That includes the repo's **full audits**, not only diff-scoped checks - in a + repo on the standard, run the exact invocation the PR gate runs, flags + included: `node scripts/self-verify.mjs`, `node scripts/spec-guard.mjs --base + origin/main --block` **and** `node scripts/spec-guard.mjs --audit --block`. + Naming `--audit` without `--block`, or dropping `--base` entirely, makes the + local run advisory where CI is not - everything this step names can come back + green while CI goes red on the same branch. (At core profile the shipped + workflow template only blocks on the audit, not the base-diff check - run both + with `--block` here anyway; a local run stricter than CI costs a moment, a + local run looser than CI costs a red PR.) + Fix anything red before continuing. Do not open a PR with red local checks. + +4. **Independent diff review (the important part).** Review the diff as if a + stranger wrote it - read *what the code does*, not *what you meant it to do*. + Prefer a clean context. If your agent has a command that reviews a diff in a + fresh sub-agent, use it; otherwise re-read the diff in a new session. What must + not happen is reviewing it in the session that wrote it - that session already + believes the code is right, which is the belief under test. + Look for: correctness bugs, missing edge cases / error handling, security + issues (injection, secrets, authz), violations of this repo's ADRs and coding + standards, missing or stale tests, **narration comments** that restate the line + below them, **duplication** of something the repo already has, and **scope + creep** - changes this PR did not need. + +5. **Fix findings, then re-run step 3.** Loop until clean. + +6. **Only then open the PR.** Fill the PR template honestly, including ADR impact. + +## What this is not + +- Not a substitute for CI secret-scanning, CI review, or human review. +- Not a place to rationalize ("I know why I wrote it this way") - if the code does + not make the intent obvious to a fresh reader, fix the code, not the review. diff --git a/.claude/skills/product-write/SKILL.md b/.claude/skills/product-write/SKILL.md new file mode 100644 index 0000000..217e944 --- /dev/null +++ b/.claude/skills/product-write/SKILL.md @@ -0,0 +1,78 @@ +--- +name: product-write +description: Use when the repo needs to state what it is building and for whom - starting a product, "we never wrote down what this app actually does", a PRODUCT.md that is stale or empty, or an argument about scope that keeps recurring because nothing settles it. Interviews for the frame, drafts it, and marks what is still unknown rather than inventing it. +--- + +# product-write + +`PRODUCT.md` is the top of the altitude ladder - the thing specs, decisions and backlog items +get judged against. This writes it by interview, not by template-filling. + +## First: is it empty, stale, or contested? + +Three different jobs, and doing the wrong one wastes the session. + +- **Empty** - a full interview, below. +- **Stale** - the product moved and the file did not. Do not rewrite from scratch: read it, + read the specs and the accepted BDRs that came after it, and show the user the specific + contradictions. Ask about those. A frame that survived contact deserves editing, not + replacement. +- **Contested** - the file is fine but people disagree with it. That is not a writing task; + it is a decision, and `bdr-write` is the skill. Say so. + +## The interview + +Ask few questions, in this order, and **stop after each** - the answers change what is worth +asking next. Draft as you go and play it back in the user's own words. + +1. **What is it, in one sentence a stranger would understand?** If the answer needs a second + sentence, the product is not one product yet - and that is worth naming now rather than + discovering it three capabilities in. +2. **Who is it for?** Names, roles, or segments. If `docs/personas.md` exists, read it first + and ask whether it is still right; if it does not, this is where it starts, and + `personas-write` takes over once names appear. +3. **What do they do today instead?** The status quo, including "nothing" and "a + spreadsheet". A product with no alternative being displaced usually means the problem has + not been found yet. +4. **What has to be true for this to be worth building?** The bet underneath. Ask plainly: + *what would make us stop?* +5. **What is explicitly out of scope?** Push here. Non-goals are the highest-value section in + the file and the one users skip - they are what makes the frame able to settle an argument + later. Ask for three, accept fewer, but ask. +6. **How will we know it worked?** Observable, ideally countable. "Users are happy" becomes + "the second visit happens without support contact". + +The answers map onto the template's sections - `What it is`, `What people do today instead`, +`Vision` (which is where the bet goes), `Users / personas`, `Non-goals`, `Success metrics`. +Ask in the order above, which is the order that makes sense to a person; write in the +template's order, which is the order that makes sense to a reader. + +`Current state` and `Key capabilities` are not interview questions - fill them from what the +repo actually contains, and mark what you could not determine. + +## Unknowns get marked, not filled + +Anything the user does not know becomes `[NEEDS INPUT: what is unknown]` - the same marker the +rest of the standard uses, so the gate counts it and it cannot quietly become a fact. **A +guessed success metric is worse than a marked one**: it will be measured against. + +Say at the end how many markers are left and that the file is usable with them in place. + +## Then + +- Write `docs/PRODUCT.md` (`docs/product/` in a repo that has one). +- If the interview produced named users, offer `personas-write` next - the two files reference + each other and a persona invented later rarely matches the frame. +- If it produced decisions already made ("we're not doing self-hosting"), offer `bdr-write` + for each. Decisions embedded in a product frame are invisible; as records they are citable. +- If it produced distinct areas of behaviour, name them as candidate capabilities and offer + `spec-specify`. Do not write the specs here. + +## Not this + +- **Do not write a product frame for a repo that is a library or a tool with no users beyond + its callers.** `README.md` is the right file. Ask if unsure. +- **Do not turn it into a roadmap.** What is being built and why, not when and in what order - + that is the backlog and the cycles, which change weekly while this should not. +- **Do not invent the metric.** See above; this is the single most common way this file + becomes a thing nobody trusts. diff --git a/.claude/skills/spec-clarify/SKILL.md b/.claude/skills/spec-clarify/SKILL.md new file mode 100644 index 0000000..35a62fe --- /dev/null +++ b/.claude/skills/spec-clarify/SKILL.md @@ -0,0 +1,269 @@ +--- +name: spec-clarify +description: Use right after a spec is drafted, or whenever one still has open questions in it - "let's clarify this spec", "answer the open questions before we plan it". Asks one question at a time, each leading with a recommended answer you can accept by saying yes, and writes every answer into the spec - including a deliberate "decide that later", which is an answer and gets recorded as one. Planning and building refuse to start until this reaches zero open questions. +--- + + + +**Discovery first, the user second.** Before asking the user anything, check `docs/discovery/` for the topic's dossier. Answers may already be there: use entries **newer** than the dossier README's `Last reconciled:` stamp (plus entries still `new`/`open`) as an answer source, and record their provenance in `## Clarifications` ("per discovery//"). Never re-ask what an entry marked `folded-into-spec` or `superseded-by:` already settled, and never treat a dossier-vs-spec difference as a question - a dossier is not normative; the spec has already won. The marker family is wider than questions: `NEEDS DECISION` / `NEEDS INPUT` / `NEEDS ASSET` markers are not clarify questions - report them as the outstanding gap list (what is missing, who brings it) and leave them open until the decision/input/asset lands. When this loop folds dossier material into the spec, mark those entries and update the stamp. + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Outline + +Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. + + +Note: this workflow runs and completes BEFORE `/spec-plan`. There is no skip: the gate is +what earns `Status: ready-to-develop`, and `/spec-plan` and `/spec-tasks` refuse a spec that +has not passed it. If the user asks to skip ahead, show what is still open instead - an +exploratory spike is a legitimate reason to *defer* an answer, and a recorded deferral is an +answer. It is not a reason to leave the question unwritten. + +Execution steps: + +1. Run `scripts/spec/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields: + - `FEATURE_DIR` + - `FEATURE_SPEC` + - (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.) + - If JSON parsing fails, abort and instruct user to re-run `/spec-specify` or verify feature branch environment. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. **IF EXISTS**: Load `specs/constitution.md` for project principles and governance constraints. + +3. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). + + Functional Scope & Behavior: + - Core user goals & success criteria + - Explicit out-of-scope declarations + - User roles / personas differentiation + + Domain & Data Model: + - Entities, attributes, relationships + - Identity & uniqueness rules + - Lifecycle/state transitions + - Data volume / scale assumptions + + Interaction & UX Flow: + - Critical user journeys / sequences + - Error/empty/loading states + - Accessibility or localization notes + + Non-Functional Quality Attributes: + - Performance (latency, throughput targets) + - Scalability (horizontal/vertical, limits) + - Reliability & availability (uptime, recovery expectations) + - Observability (logging, metrics, tracing signals) + - Security & privacy (authN/Z, data protection, threat assumptions) + - Compliance / regulatory constraints (if any) + + Integration & External Dependencies: + - External services/APIs and failure modes + - Data import/export formats + - Protocol/versioning assumptions + + Edge Cases & Failure Handling: + - Negative scenarios + - Rate limiting / throttling + - Conflict resolution (e.g., concurrent edits) + + Constraints & Tradeoffs: + - Technical constraints (language, storage, hosting) + - Explicit tradeoffs or rejected alternatives + + Terminology & Consistency: + - Canonical glossary terms + - Avoided synonyms / deprecated terms + + Completion Signals: + - Acceptance criteria testability + - Measurable Definition of Done style indicators + + Misc / Placeholders: + - TODO markers / unresolved decisions + - Ambiguous adjectives ("robust", "intuitive") lacking quantification + + For each category with Partial or Missing status, add a candidate question opportunity unless: + - Clarification would not materially change implementation or validation strategy + - Information is better deferred to planning phase (note internally) + + +4. Build a prioritized queue. **There is no question limit** - the loop is bounded by *coverage*, + not by a count. + - **What must be asked:** anything whose absence means the capability cannot be built or + verified from the spec alone. At the `buildable` tier that means every field name, type, + enum value, endpoint, error code, ordering rule, boundary and invariant the spec asserts + but does not pin down. Paraphrase is not a contract. + - **What must not be asked:** anything that changes no contract and no test - stylistic + preference, plan-level execution detail, or something a sensible default already covers. + A question the user cannot tell the point of costs more than it buys. + - Rank by (Impact x Uncertainty). Prefer the question whose answer unblocks a whole section + over three that each polish one line. + - Exclude anything already answered in the spec, in `## Clarifications`, or in the discovery + dossier. + +5. Ask, in rounds, and **stop on coverage rather than on a number**: + - **Batch by contract, not one question per message forever.** Questions that belong to the + same contract are one conversation - a field's name, type and nullability get asked + together, in one message, numbered. Unrelated questions stay separate. Asking six things + about one table across six messages is not thoroughness, it is a worse interface. + - Keep a round to roughly **five messages**, then **check in**: say how many open items + remain and what they block, and offer three ways forward - keep going, park the rest as + markers, or park a named subset. Parking is safe *because* it writes markers, and the gate + then refuses to plan. Say that when offering it. + - **Stop when** every section the declared tier requires either carries a real contract or + carries a typed marker; or the user says stop. Never stop merely because a number was hit. + + - Lead each with `**Question:** ?` - answerable as written. NEVER use a + topic label, a section heading or a requirement id as the question itself: "Retention + policy" and "FR-023" are subjects, not questions. An id may trail it: + `**Question:** How long are booking records kept after cancellation? (FR-023)`. + - Under it, one plain-language sentence on why it matters - what changes depending on the + answer. Everyday wording; introduce a term only if the same sentence defines it. + - For multiple-choice questions: + - **Analyze the options** and pick the most suitable, on best practice for this project + type, common patterns, risk (security, performance, maintainability), and the spec's own + stated goals and constraints. + - Present the recommendation first: `**Recommended:** Option [X] - <1-2 sentence reason>`. + - Then the options as a table: + + | Option | Description | + |--------|-------------| + | A |