From 29a4823136d749dd1aeb4b70aceb25870b706196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baldur=20P=C3=A1ll=20H=C3=B3lmgeirsson?= Date: Sun, 18 Oct 2026 18:33:56 +0000 Subject: [PATCH 1/2] chore(context): update ai-workflow to 0.26.0 --- .agents/skills/feature-close/SKILL.md | 74 +++++- .agents/skills/feature-close/tracker.md | 8 + .agents/skills/feature-implement/SKILL.md | 62 ++++- .agents/skills/feature-implement/parallel.md | 238 +++++++++++++++++ .agents/skills/feature-implement/tracker.md | 24 +- .agents/skills/feature-plan/SKILL.md | 64 ++++- .agents/skills/feature-plan/split.md | 62 +++++ .agents/skills/feature-plan/tracker.md | 91 +++---- .agents/skills/feature-status/SKILL.md | 3 + .agents/skills/onboard/SKILL.md | 66 ++++- .agents/skills/orchestrate/SKILL.md | 106 +++++++- .agents/skills/prototype/SKILL.md | 5 +- .agents/skills/release/SKILL.md | 251 ++++++++++++++++++ .agents/skills/tracking-migrate/SKILL.md | 11 +- .claude/agents/planner.agent.md | 23 +- .claude/agents/reviewer.agent.md | 30 ++- .claude/skills/feature-close/SKILL.md | 74 +++++- .claude/skills/feature-close/tracker.md | 8 + .claude/skills/feature-implement/SKILL.md | 62 ++++- .claude/skills/feature-implement/parallel.md | 238 +++++++++++++++++ .claude/skills/feature-implement/tracker.md | 24 +- .claude/skills/feature-plan/SKILL.md | 64 ++++- .claude/skills/feature-plan/split.md | 62 +++++ .claude/skills/feature-plan/tracker.md | 91 +++---- .claude/skills/feature-status/SKILL.md | 3 + .claude/skills/onboard/SKILL.md | 66 ++++- .claude/skills/orchestrate/SKILL.md | 106 +++++++- .claude/skills/prototype/SKILL.md | 5 +- .claude/skills/release/SKILL.md | 254 +++++++++++++++++++ .claude/skills/tracking-migrate/SKILL.md | 11 +- AGENTS.md | 8 +- context/.state/manifest.json | 76 +++--- context/executors.notes.md | 5 + context/plan-template.md | 30 ++- context/plan-template.notes.md | 33 ++- context/release.notes.md | 23 +- context/roles/coder.md | 51 +++- context/tracking.notes.md | 21 +- context/verify.notes.md | 32 ++- context/workflow.md | 87 ++++++- context/workflow.notes.md | 74 +++++- 41 files changed, 2331 insertions(+), 295 deletions(-) create mode 100644 .agents/skills/feature-implement/parallel.md create mode 100644 .agents/skills/feature-plan/split.md create mode 100644 .agents/skills/release/SKILL.md create mode 100644 .claude/skills/feature-implement/parallel.md create mode 100644 .claude/skills/feature-plan/split.md create mode 100644 .claude/skills/release/SKILL.md diff --git a/.agents/skills/feature-close/SKILL.md b/.agents/skills/feature-close/SKILL.md index db57596..2bddc88 100644 --- a/.agents/skills/feature-close/SKILL.md +++ b/.agents/skills/feature-close/SKILL.md @@ -45,12 +45,56 @@ owned. There is nothing left for a second refusal to catch. A refusal here is the workflow working, not a problem to route around. If the user overrides after being told, say plainly what is being overridden, then proceed. +### Go through the plan's open questions — before anything is written + +Read the plan's **§9 Open questions**, and the **Note** column of its ledger. `/feature-implement` surfaced +§9 at activation and nothing has read it since, and **this is the last command that ever will** — after the +`git mv` it is in `context/archive/`, where no command looks. + +**What is still open here is an owner check** — an item no gate can close because it needs a person: a +reading in a language nobody on the branch speaks, a screen-reader pass, a judgement about how something +looks, a check on a device this project does not drive. *What happens to a question the plan could not +settle* in [`context/workflow.md`](../../../context/workflow.md) is the standing rule; the ends are here. + +**Every item that still needs a person gets one of three:** + +- **answered** → write the answer into the item, in the plan, in place. A phase that settled one has + already done this; the user settling one here is the same end. +- **filed** → one issue, wherever this project already files bugs, carrying this project's own labels, + saying what has to be looked at and who it waits on. **Never the backlog label**, and not written up as a + defect — it is a check nobody has run, not a thing known to be wrong. **It is filed as what it is, and + not as a bug**: a pending screen-reader pass recorded as a defect is a defect report nobody can + reproduce. **The person who does the check is what closes it**, and the workflow neither reads it nor + ranks it. +- **waived** → the user says in this turn that it does not matter. Mark the item waived and move on. + +**The user picks, item by item, and silence is not a waiver.** Use your runtime's question mechanism if it +has one. **Where this project files bugs nowhere, there are two ends rather than three** — say so, and the +item is answered or waived. Do not invent a store for it, and do not hold up the retirement over it. + +**This runs before the writes below** because its result is what the `history.md` row's Why and the archived +document both carry. Nothing afterwards would have anything to amend. + +**This adds no status and no fourth phase state.** An owner check is not a phase's and does not gate one: +a phase whose scope landed and whose gates passed is `done` whether or not a person has since looked at +what it produced, and the feature's Outcome stays `shipped` for the same reason. + +**Most of §9 is not an owner check.** The filter is one bit — *does this need a person* — applied to a list +the plan already wrote, not a disposition assigned to every line. **This is not the `notes.md` triage +refused further down**, and that refusal stands: that file is a gate's and grows without bound, while §9 is +the plan's and is bounded by it. + +**Then say in the report where each one went**, including when the answer is *none*. An empty report reads +as "nobody looked", exactly as it does for documentation and for the release note. + ### Then, as one reviewed change 1. **Move the entry into history.** Remove it from `context/roadmap.md` entirely, and append one row to `context/history.md`: date, name, outcome `shipped`, a one-line why, and a link into `context/archive/`. One line. That file **indexes** depth, it does not duplicate it — the reasoning stays in the archived - plan. + plan. **Where an owner check was filed, the why names it and the issue that carries it**, and the + outcome stays `shipped` and gains no value — *The history row says what is waiting, in the Why* in + [`context/workflow.md`](../../../context/workflow.md) says why it could not be anything else. 2. **`git mv` the plan** from `context/plans/` to `context/archive/`. Use `git mv`, not `mv` — the file's history is the record of how the feature was actually built. 3. **Rewrite the document's header to point at the history row.** In the moved document, replace @@ -63,6 +107,10 @@ told, say plainly what is being overridden, then proceed. Add a note if its §-references are cited from source comments. **Do not stamp the outcome and date into the document.** `history.md` owns them, and a copy in the header is a second place to maintain. Replacing the old header is what stops a stale status surviving the move. + + **§9 travels with it, carrying the ends the step above gave it** — each item answered, marked waived, or + naming the issue it was filed as. That is what makes the archived plan the record for the two ends that + have no other one. 4. **Sweep every reference to its old path**, and **show the sweep for review before committing.** ### The sweep @@ -143,10 +191,19 @@ retirement as one reviewable change and hand it over. **Without it, this command writes the note and stops.** That is the default and it is the right one. [`context/workflow.md`](../../../context/workflow.md) forbids running what bumps, tags, publishes or -deploys, with one exception — *when the user asks for it in that turn* — and **this flag is what that -asking looks like.** So it is never inferred: not from "and ship it" earlier in the session, not from a -plan, not from the notes looking ready, and not from a release being obviously due. A sentence is not a -flag. +deploys, with one exception — *when the user asks for it in that turn* — and **this flag is one of the two +shapes that asking takes.** So it is never inferred: not from "and ship it" earlier in the session, not +from a plan, not from the notes looking ready, and not from a release being obviously due. A sentence is +not a flag. + +**The other shape is `/release`, and it is the one to name when the user has not typed this flag.** This +flag **fuses** the release into the feature's own merge: one branch, one pull request, and a diff where the +version move sits among the feature's code. `/release` separates them — the note waits on the default +branch with everybody else's, and the release is cut later on a branch of its own, which is where a reader +can see that the version move is the whole of the change. Neither is the better answer in general: this one +suits a repository that releases about as often as it merges, and the separated one suits a repository +where notes accumulate. **If the user asked for a release and typed no flag, name `/release` rather than +assuming this one.** Read [`context/release.md`](../../../context/release.md) and run **the script its Bump wire names**. That file says what it is; naming a tool here would be wrong in half the repositories this command runs in. @@ -213,6 +270,12 @@ ended by then. For an entry that will not be built. **There is no ledger check in this mode** — unfinished phases are expected. +**The open-questions pass still runs, and it still runs first**, by the rule the release note already +follows at the end of this mode: an owner check belongs to the code that landed, not to the outcome the +feature was later given. So an item about a phase that landed is an owner check like any other, and an item +about work that was never written is **waived by construction** — say that in one line rather than walking +it. A feature with no landed phase has nothing here at all. + 1. Append a `context/history.md` row with outcome `dropped` (or `superseded by `) and **the reason the user gave**, verbatim in substance, not softened. That row is what stops the idea being re-proposed, so a vague reason makes it worthless. @@ -222,6 +285,7 @@ expected. stamped outcome, same rule as Mode 1), and sweep. 4. **Delete `context/notes.md`** if it exists, per *Delete `context/notes.md`* above. A feature that will not be built has nothing its notes should outlive. + **`--release` is refused in this mode**, and not as a technicality: there is no note, so there is nothing this retirement would be releasing. If a release is due anyway, that is its own act and not this one's. diff --git a/.agents/skills/feature-close/tracker.md b/.agents/skills/feature-close/tracker.md index 34f5e58..4bc02a3 100644 --- a/.agents/skills/feature-close/tracker.md +++ b/.agents/skills/feature-close/tracker.md @@ -17,6 +17,9 @@ phase finished — and so is everything about pushing. | `git mv` the plan to `archive/` | nothing moves; the issue keeps its body and its whole thread | | rewrite the document header | nothing — a closed issue does not claim to be open | | the reference sweep | **nothing to sweep.** No path changed, so no link broke | +| the plan's §9, read out of the document | **unchanged** — §9 is a section of the body, and the body is the plan | +| answered or waived, written into the item | a body edit like any other | +| the why names the filed issue | the closing comment names it, by number — both issues are in one tracker, so it is a reference rather than a path | | delete `context/notes.md` | **unchanged** — it is a file in a working tree under either answer, and it is deleted the same way | **The archive is the closed issue**, and it is more than the file it replaces: the plan, the discussion @@ -42,6 +45,11 @@ This is the only place `git.md`'s answer changes what this command does, and it [`context/tracking.md`](../../../context/tracking.md) recommends pairing this substrate with the push answer rather than requiring it. +**A filed owner check is an issue here like anywhere else, and it stays outside the one bit.** No backlog +label, no planned label, and **no `blocked by` relationship** — that primitive is between features, and an +owner check is not one. It carries this project's own labels and whatever assignee the person who will do +the check has, and nothing in the workflow reads it again. + **The release note is unchanged.** It is an artifact of the change rather than workflow state, so it is a file in this repository under both of [`context/tracking.md`](../../../context/tracking.md)'s answers — written, confirmed and committed exactly as above, and carried by the same pull request that closes the diff --git a/.agents/skills/feature-implement/SKILL.md b/.agents/skills/feature-implement/SKILL.md index 3cce0a2..52564bc 100644 --- a/.agents/skills/feature-implement/SKILL.md +++ b/.agents/skills/feature-implement/SKILL.md @@ -6,8 +6,10 @@ description: "Activate a planned feature and run the next phase of its plan — # /feature-implement Owns the transition from *has a plan* to *being worked*, **and** the phases within it. One invocation runs -**one phase**: pick it, do it, gate it, close out its ledger row. `--all` (step 14) runs that same loop -back to back instead of stopping after the first. +**one round**: pick a phase, do it, gate it, close out its ledger row — and where phases of this plan are +independent of each other, up to the cap in [`context/executors.md`](../../../context/executors.md) of them +side by side, per [`parallel.md`](parallel.md). `--all` (step 14) runs that same loop back to back instead +of stopping after the first. Read [`context/workflow.md`](../../../context/workflow.md) for the tier model, the one-active-feature rule and the gate contract. This skill cites those rather than restating them. @@ -76,12 +78,25 @@ entries are all `done`.** If it is already `in progress`, **read its Note and resume from there — do not restart it.** +**Then see what can run beside it.** Where this phase and another runnable one are both `not started` — +every `Depends on` `done` — and share **no path** on their **Files:** lines, prose included, the two can +run at the same time, each with its own coder, up to the cap in +[`context/executors.md`](../../../context/executors.md). Where that is true, read +[`parallel.md`](parallel.md) beside this file before step 6: it holds how the round is formed, +every line that makes it fall back to one phase at a time, and how the work comes back. **No flag asks for +this and none turns it on** — it is this command's own decision, each round, and a round of one phase is +the ordinary run the steps below describe. + ## 4. Read the row you are about to work If the phase you picked reads `blocked`, its Note says what stopped it. **That is the work** — clear it before starting anything new, or say plainly why it cannot be cleared and stop. A `blocked` row is the only durable record a previous run left, so it is read rather than overwritten. +**If the Note names a tree, this phase's work is already in it** — a round left it there. Read +[`parallel.md`](parallel.md) and land that tree before briefing anybody; a coder re-run over finished work +throws it away. + ## 5. Stop on disagreement If the ledger's claim contradicts the repo — a phase marked `done` whose **Files:** do not exist, or work @@ -97,8 +112,9 @@ cancelled — leaves a working tree with half a phase in it. A row still reading run into step 5's disagreement stop, or into redoing work that is already there. One token and one Note, in the row that is already there — do not move the entry, restructure the table, or -write a summary anywhere else. If the row is already `in progress` because you are resuming it, leave it -alone; step 11 rewrites the Note. +write a summary anywhere else. **One line**, by the same rule step 11 applies: what is underway, so a +session arriving after an interruption knows where it landed. If the row is already `in progress` because +you are resuming it, leave it alone; step 11 rewrites the Note. **This write is not a change of its own.** Leave it in the working tree — it lands with the phase's work under either answer in [`context/git.md`](../../../context/git.md). Never commit it on its own. @@ -113,6 +129,15 @@ paths are on the same **Files:** line as the code. Per the standing rule in phase. If the work turned out differently from the plan and made something *else* untrue — a README the plan never listed — fix that too and say so; the sweep happened before the code existed. +**An open question this phase settles is answered in the plan's §9, by this phase** — in place, in the +item it settles, riding the same change as the ledger row. Same rule as the documentation rows above and +the same reason: the answer is cheapest to write while the work that produced it is still in front of you, +and `/feature-close` goes through that section before the plan is archived and has to give whatever is left +an end. **Leave the items this phase did not settle alone**, and do not invent an answer to close one — an +honest open question is what that section is for, and the one no phase could finish is exactly what the +retirement is there to catch. Settling one is not part of this phase's scope and step 11 does not test for +it. + **Read [`context/release.md`](../../../context/release.md) and find out whether this phase owes a release note.** That file answers per path and at one granularity, and only one of its two granularity answers reaches this step: @@ -159,11 +184,17 @@ the first answer and implements in-host; that is a fallback, not a failure, and coder produced. A coder that reports its own success has reported nothing — that is what step 11 means by refusing `done` on a self-report. -The brief **cites paths, it does not paste files.** Point at the phase's own section in the plan — its -`Files:`, and its `Standards:` line, which is the plan's one traversal of `context/standards/README.md` and -what the coder loads instead of the table — and at `context/stack.md`. Anything that can read this -repository can open them, and a brief that inlines them is a brief that goes stale. A plan written before -phases carried a `Standards:` line has none; then say to load per that README's conditional table. +The brief **cites paths, it does not paste files, and it names the phase rather than the plan.** Point at +the phase's own §6.2 section — its `Files:`, its `Standards:` line, which is the plan's one traversal of +`context/standards/README.md` and what the coder loads instead of the table, and its `Decisions:` line, +which is the plan's one traversal of §3 — and at `context/stack.md`. Anything that can read this repository +can open them, and a brief that inlines them is a brief that goes stale. + +**A whole-plan read is the fallback, not the route.** Plans in the field run tens of thousands of +characters and three readers open one on every phase; the §6.2 section plus the two lines is what a coder +needs, and a brief that says *read the plan* spends the rest on §1, §5 and the phases that are already +`done`. A plan written before phases carried these lines has neither: then say to load per that README's +conditional table, and to read §3 for the decisions that bind this phase. Describe **what** needs to happen, never **how** to code it. Scope each delegated task to specific files. @@ -185,7 +216,9 @@ Dispatch per [`context/executors.md`](../../../context/executors.md), which hold The last is the fallback and the weakest — the session that wrote the code judging whether the code is good — so **say which one you ran**, every time. The first is the cheapest real independence available: a reader that never saw the implementation being written, only what it produced. Brief it with the phase's -section — its `Files:`, its review expectations and its `Standards:` line — and the diff. Where the runtime +§6.2 section — its `Files:`, its review expectations, its `Standards:` line and its `Decisions:` line — +and the diff. **The section, not the plan**, for the reason step 7 gives; a reviewer judging whether the +change honoured a decision needs the decisions the phase cites, not all of §3. Where the runtime has no such mechanism, fall back to reviewing the diff yourself against the plan's review expectations and the standards, and say that is what happened. @@ -242,6 +275,12 @@ The row is part of the same change as the work — never a separate step afterwa - **Some landed** → stays `in progress`, Note rewritten to name exactly what remains. - **A gate hit its cap, or something external blocks it** → `blocked`, with the blocker in the Note. +**The Note is one line**, per §6.1 of the plan itself: what a later session needs in order to understand +or resume the Status beside it, and nothing else. **Gate 1's output, the test counts and the measurements +do not go in it.** They go in the commit message that carries the row, under +[`context/git.md`](../../../context/git.md)'s answer, or in step 13's report. The ledger is read in a hurry +by a session that has just arrived, and a Note that has become a phase log is one it cannot skim. + **Never mark `done` on a coder's self-report** — the gate output is the evidence. A blocking item that was not fixed means the gate did not pass, and a gate that did not pass means the phase is not `done` — there is no second check to run, because `done` already says both gates passed. @@ -283,6 +322,9 @@ side-effect of the last phase finishing. Without the flag, this invocation is over at step 13. With it, **go back to step 3 and run the next phase**, and keep going until the stop list below says otherwise. +**The unit is step 3's round.** This flag decides whether there is a next one, never how wide one is — +that is [`parallel.md`](parallel.md)'s, and it is decided again each time step 3 runs. + Phase to phase is not a tier boundary — this command already owns "activation, and the phases within a plan" — so the flag crosses nothing. What it removes is the pause where a user reads a phase's report before the next one builds on it, and everything below exists to replace that pause with something diff --git a/.agents/skills/feature-implement/parallel.md b/.agents/skills/feature-implement/parallel.md new file mode 100644 index 0000000..b282203 --- /dev/null +++ b/.agents/skills/feature-implement/parallel.md @@ -0,0 +1,238 @@ +# /feature-implement — a round of phases, side by side + +Read this where step 3 formed a round of **more than one phase**, and where step 4 found a row whose Note +names a tree a previous round left. Every step in [`SKILL.md`](SKILL.md) holds; this says what a round +adds. A round of one phase with nothing left behind is an ordinary run and reads none of this. + +**There is no flag.** How wide a round is is this command's own decision, made fresh each round from the +ledger, the phases' **Files:** lines and the cap in +[`context/executors.md`](../../../context/executors.md). `--all` is unchanged: it decides whether there is +a *next* round, never how wide one is. + +## What forms a round + +Start from the phase step 3 picked — the lowest-numbered phase that is not `done` and whose `Depends on` +are all `done`. Then walk the remaining runnable phases in number order and add each one that **shares no +path with any phase already in the round**, up to the cap. + +- **Runnable** is step 3's test and nothing weaker: not `done`, every `Depends on` `done`. +- **A round holds `not started` phases only.** A phase reading `in progress` or `blocked` has half-work and + a Note of its own in the feature's tree — step 3 resumes it and step 4 reads its row — and that state + cannot be branched into a tree beside it. **A resumed phase runs alone**, exactly as it does today. +- **Shares no path** is read off the phases' **Files:** lines, and **a path that contains another is an + overlap**: a directory or a glob on one line and a file under it on another are the same place, however + differently the two phases spelled it. **The plan's own path is the one exclusion** — only this command + writes the ledger, so two phases naming it are not two writers. **Prose counts** otherwise — a `.md` + file, a README, anything under `context/`. Two phases that each add a section to one file both append, in + two trees, from one base: that is a conflict every time, and in the files that answer what this project's + commands are. +- **The cap** is the one in *Coder* in [`context/executors.md`](../../../context/executors.md) — **3 where + that file says nothing, and 1 turns this off.** It counts coders, which is all there is to count: the + gates below are serial. + +**Say the round before any coder starts.** Which phases are in it, and for every runnable phase left out, +which test left it out — an overlap, **naming the path**, or the cap. + +**A `Files:` line is a prediction.** Phases in the field grow one while they run, which is why the same +test runs again, against the real diffs, before anything lands. + +## What makes it fall back to one phase at a time + +**Nothing here refuses.** Each line below means this run does what it has always done — one phase, in the +feature's own working tree, through steps 6 to 13 — and **says so once, before the first phase**, naming +the file and the answer that decided it. + +| The line | Why a round cannot run under it | +|---|---| +| [`git.md`](../../../context/git.md) *Who commits* — **the user commits** | every mechanism below is a commit, and a tree carrying three phases cannot be cut back into three. Step 14's argument, one level down | +| [`executors.md`](../../../context/executors.md) *Coder* — not configured, or in-host | one coder, by construction. That is what ships, so a fresh install runs one at a time | +| the host cannot run two coders at once, whatever that answer says | **say which of the two it was**, the recorded answer or the runtime; they are fixed in different places | +| the host will not let a coder write in the tree this command can make, or confines it to one of the host's own | a coder that has to be asked about every file it writes is not running unattended, and one sealed into a tree the host chose cannot be pointed at this one. **Name the limit**, take the runtime's tree where it passes the three tests below, and otherwise run one at a time | +| [`verify.md`](../../../context/verify.md) has no **Prepare a fresh tree** answer | a fresh checkout has no installed dependencies, so a coder in one cannot write a failing assertion and run it. Name the file and `/onboard` | +| `git check-ignore -q .phase-trees/` exits non-zero, run at the top of the working tree | the trees would be in Gate 1's way — a linter would lint the other phases' half-written work and a test runner would run their copies of the suite. **Keep the trailing slash**, or the test answers for a file that is not there. Name `/onboard`, which writes the entry and proves it against this project's commands | +| the cap is **1** | the project said one at a time. Not a gap — an answer | +| `git worktree add` fails, for any reason | report the error and run the phase. A tree that cannot be made is not a phase that cannot be run | + +## The scratch tree + +One per phase in the round, made by this command with plain git, at the feature branch's HEAD, **detached**, +in one fixed directory inside the working tree this run is standing in: + +```bash +git worktree add --detach ".phase-trees/-phase-" HEAD +``` + +- **No branch is created**, and **the invocation [`executors.md`](../../../context/executors.md) names is + not used.** That one makes a *workspace* — env files, a naming convention, a directory somebody opens, + something it is handed to. This is a checkout nobody looks at, and the two are not interchangeable. +- **Inside this working tree**, because a host that scopes a coder's writes to the session's own working + directory can write here and the repository's admin directory is not that: under + [`git.md`](../../../context/git.md)'s worktree answer it is not even inside this directory, and a host + may hold it to a stricter rule than the files beside it. +- **And ignored, which is what keeps Gate 1 honest.** The leading dot is not enough — a linter or a test + runner that walks the tree itself finds these checkouts, lints the other phases' half-written work and + runs their copies of the suite, and some will *write* to them. What stops it is the entry in this + project's `.gitignore`, which is where most tools take their ignore list from, and `/onboard` **proves** + that against this project's own Gate 1 commands, because honouring that file is a per-tool setting. The + entry is also what keeps a tree out of `git status` and out of `git add -A`. +- **The path is fixed** — it is the only thing identifying these trees as this command's — and is + **recorded absolute**, `$(git rev-parse --show-toplevel)` and the path above, because a Note is read from + wherever the next session is standing. Each feature's trees sit in that feature's own tree, so the sweep + below cannot reach another's. +- **Record the sha `HEAD` resolved to.** It is this tree's base, `..HEAD` is the phase's whole work, + and the feature branch moves as the round lands — so the base cannot be read back afterwards. +- **Make it runnable.** Run what *Prepare a fresh tree* in + [`context/verify.md`](../../../context/verify.md) says, in the tree, before the coder starts. That is + paid per phase per round — the one cost that grows with the width being bought, so it is named in the + report rather than hidden. **If it fails in a tree, the round does not run** — a command there may bind a + port the tree beside it holds. No coder has started yet: remove the trees, name the command and its + output, and run one at a time. +- **Then the tree has to be clean: `git -C status --porcelain` prints nothing.** A prepare step that + rewrites a lockfile or writes a generated file nobody ignores has dirtied the tree **before the phase + started**, and from then on the phase's own state is unreadable: landing reads a clean phase as a coder + that stopped short, and the overlap check sees a path every phase in the round touched. Same outcome as a + failure — remove the trees, **name the files it changed**, and run one at a time. An installed dependency + directory is not this: `--porcelain` does not list ignored files. + +**Where the runtime can give a coder its own temporary tree of this repository — one whose commits are +objects in this repository — take that and make nothing.** A copy or a fresh clone is not that: its commits +cannot be cherry-picked from here, so it falls back like any line above. **Three things have to hold**, and +the first is a command rather than a claim: `git -C rev-parse HEAD` is **the sha this round +recorded**, since a runtime that cuts its trees from the default branch has handed the coder a tree with +none of this feature's work in it — a different base, not an isolated one; **something removes it when the +phase lands**, the runtime or this command by the path it recorded; and it is **outside this working tree +or dotted inside it**, for the Gate 1 reason above. Any of the three failing is a fallback like the rest. + +**It is not this command's tree, so it is reported rather than swept.** It has a branch: the detached test +below does not cover it, removing it leaves the branch behind because **nothing here deletes a ref**, and +`/feature-status` reads a branch on a worktree as a feature in flight for as long as the tree exists. +**Name its path, its branch and its tip.** + +**The brief is step 7's, plus four sentences and no more:** work in this directory; the plan here is a +stale copy and is not to be edited; commit your work in this tree when you are done, because that commit is +what the caller takes; **other coders are running beside you and this tree is a fresh checkout, so a test +that needs a server, a fixed port, or a secret the tree does not hold — nothing kept out of git came across +into it — is Gate 1's: leave it, and say in your output which assertions you did not run.** +[`roles/coder.md`](../../../context/roles/coder.md) is unchanged — one prompt for +every way a coder is dispatched, and committing is a property of this dispatch rather than of the role. + +## The gates, and the ledger + +**Both gates run in the feature's own tree, one phase at a time, in phase order** — never in a scratch +tree. Phase *k* is gated on a tree holding this round's phases up to *k*, and the tree holding up to *k*−1 +has just passed, so a failure at *k* is *k*'s, exactly as in a sequential run. The loopback cap is +unchanged: two loops per gate, per phase. Nothing runs the verification stack in two trees at once. + +**Only this command writes the ledger, and only in the feature's own tree.** Coders never touch the plan, +even where a phase's **Files:** line names it. + +- **The round's rows open in one write**, before any tree is made: one `in progress` and one Note each, + naming the round and the absolute path of the tree the phase will be worked in. Step 6's rule, for every + phase in it. +- **A scratch tree's copy of the plan is stale, and that is harmless.** It is branched before the opening + write is committed, and nothing in the tree reads it. +- **A closing row names the tip its work was at**, beside the path — the path is what a later sweep + destroys, and the tip is what survives one. `git merge-base ` recovers the base if the + recorded one is lost. +- **An open question this phase settles is still answered in the plan's §9**, per step 7 — but not by the + coder, which never touches the plan. **The coder reports what its phase settled and this command writes + the answer**, in the commit that carries the row. + +## Landing — the whole tree, as a range, in phase order + +Per phase, in phase order: + +1. **Check the coder finished.** `git -C status --porcelain` is empty, or **nothing from that phase + lands** — it is a coder that stopped short, so its row stays `in progress` naming what remains, per step + 11, and its tree is kept. Uncommitted work in a scratch tree is not a candidate to land. +2. **Check the real diff.** `git diff --name-only ..` — the base recorded when the tree was + made, the tip its HEAD — read from the feature's own tree with no checkout, against the paths this round + has already landed. A shared path is the next section. +3. `git cherry-pick -n ..` — **the whole tree, however many commits are in it**, staged and not + committed, so the row can ride the work. +4. **Gate 1**, then **Gate 2** on the staged diff, per steps 8 and 9. +5. **Close the row**, per step 11. +6. **One commit**, carrying the phase's change and its row — [`git.md`](../../../context/git.md)'s *one + commit per phase*, unchanged. +7. **A phase that closed `blocked` ends the landing here**, with every later tree kept — *Mixed results*. + +**A kept tree is landed by the run that resumes its phase** — these same steps, from the base and tip its +Note recorded, before any coder is briefed. Re-running a coder would throw away a phase's finished work to +arrive back where it already is. + +**The range, not the tip.** How many commits a coder makes in its own tree is its own business, and +`..` is the phase either way. Landing the tip alone would drop work silently. + +**Cherry-pick reads the scratch tree without touching it**, which is what lets a tree be kept exactly as +its coder left it. A merge would add a commit per phase that no granularity answer names; a rebase would +rewrite the only durable record of what Gate 2 reviewed. + +**A gate failure loops back in the feature's own tree** — never in the scratch tree, and **nothing is +reset.** The cherry-pick is already staged here, which is the state step 10 re-briefs a coder in on any +ordinary phase, so re-brief it here against this tree. The scratch tree is missing the phases that landed +earlier in the round, so a failure the combination caused — a shared budget two phases each fit under +alone — cannot be seen from there, let alone fixed there. + +## When two phases touched the same path + +The round was formed on a prediction; the diffs are the fact. Where a later phase's diff shares a path with +one this round has already landed: + +- **The earlier phase stands.** It is already a commit. +- **The later phase's work is thrown away.** Nothing is cherry-picked from it, and the two versions are + never merged — nothing here has the standing to decide which of two coders' versions of a file wins. + Name the discarded tip in the report; its range is readable until git collects it. +- **Its Note stops naming its tree.** A Note that names one means *land that tree* — step 4 — and this is + the one tree whose work must not be landed. It is kept, and it is named in the report instead. +- **Run that phase again, one at a time, on the combined tree**, from step 7, with its row still + `in progress` and its Note saying so. It can happen at most once per phase per round: the re-run is + alone, so there is nothing left for it to overlap with. +- **Say it in the report** — the path, the phase that kept it, and the phase that ran again. + +**The check is before the cherry-pick, not after.** A range that conflicts stops part-applied with the +sequencer open, and getting out of it means choosing between two coders' versions of a file — the one +decision nothing here has the standing to make. Checking the diffs one command earlier means that state +never exists. + +## Mixed results + +- **A phase that ends `blocked` ends this round's landing.** What landed before it stands — those are + commits, and withholding them buys nothing. **Every later tree is kept**, with its path and tip in the + report, and the next run lands it: those rows still read `in progress` from the opening write, so step 3 + resumes each one alone. Landing is in phase order, each phase on top of the last, so carrying on past a + blocked phase would cherry-pick *k*+1 onto *k*'s half-work and gate it there — the failures from then on + belong to a phase that did not cause them, and the loopback would brief a coder to fix somebody else's. +- **A blocked phase's Note names the tree its work is in, and the tip it is at.** `git worktree list` shows + the tree and nothing else says which phase it belongs to. One line, by the same rule as every other Note. +- **The next round does not start**, and **each phase is reported as it lands** — step 14's stop list and + its per-phase report, neither of them widened by a round. + +## The trees this command removes, and the ones it never touches + +[`context/workflow.md`](../../../context/workflow.md) says nothing removes a worktree. **This is the one +exception, and it is this narrow:** + +- **Only a tree this command made, at the path above, detached and with no branch** — and **never with + `--force`.** `git worktree list --porcelain` prints `branch` for every worktree the recorded invocation + has ever made and `detached` for these. A tree with a branch on it is never touched. +- **A tree goes when its phase's row reads `done`** — `git worktree remove `, never `--force` — and + is **otherwise kept, with its path and its tip in the report.** One rule, covering a blocked phase, a + discarded one and a coder that stopped short alike. A record whose directory has already gone is removed + the same way; nothing here prunes. +- **Without `--force`, removal cannot destroy work a coder failed to commit.** Git refuses a tree holding + modified or untracked files and succeeds where the only leftovers are ignored — which is what an + installed dependency directory is. A refusal is reported, not forced. +- **A later run sweeps that directory first, and removes only a tree whose row reads `done`.** **Read the + ledger before the sweep.** A clean tree is not an abandoned one: a coder that did its job committed, so + removal succeeds without `--force` and the commit it leaves behind has no branch and nothing else + pointing at it. That is exactly the tree an `in progress` or `blocked` row is keeping for the next run, + and the Note would be left naming a path that no longer exists. **A tree whose path names another plan is + left alone** and named in the report — this run has not read that plan's ledger and is not entitled to. + +## The report + +Step 13's report, plus three things: + +- **Which phases ran side by side**, and which ran alone. +- **Every fallback, with the file and the answer that caused it** — once, not per phase. +- **Every tree still on disk, by path**, and the sha of anything thrown away. diff --git a/.agents/skills/feature-implement/tracker.md b/.agents/skills/feature-implement/tracker.md index 513ee37..a41f853 100644 --- a/.agents/skills/feature-implement/tracker.md +++ b/.agents/skills/feature-implement/tracker.md @@ -52,9 +52,11 @@ be editing at the same time — refining the plan while you flip a status — an loses their edit with no trace. This hazard does not exist under the working-tree answer, where the plan sits in a tree only you are working in. -**The body has a size limit, and the row outranks everything else in it.** -[`context/tracking.md`](../../../context/tracking.md) says what it is. `/feature-plan` leaves room for the -rows this command writes, so reaching the limit here means the body grew after the plan landed. If a write +**The body has a size limit, and the row outranks everything else in it.** *How much a plan may hold* in +[`context/tracking.md`](../../../context/tracking.md) says what it is. `/feature-plan` held a reserve back +for the rows this command writes, and step 11's one-line Note is what that reserve was sized against — so +reaching the limit here means either the body grew after the plan landed or a Note stopped being one +line. If a write would overflow, **shorten your own Note until the row fits and say the body is at its limit** — the Status column is the phase's only home and a row that never lands is state that is simply gone. Never drop a row, never delete someone else's text to make space, and never move a row or a note out of the body. @@ -108,6 +110,22 @@ that is never written is worse than one whose evidence is still owed, and this i **Never edit a row to get past a refusal**, exactly as no phase is marked `done` to get past one. +### A round of phases, in steps 6 and 11 + +[`parallel.md`](parallel.md) holds the round itself. Three things change under this answer: + +- **The round's rows open in one body write.** Fewer windows against a person editing the body, which is + this answer's one hazard the tree answer lacks — and the opening write is the one that can be batched, + because no phase has landed yet. +- **They close one write per phase, as it lands.** The tree answer's row travels inside the commit; here it + does not, so a run that dies two phases into a round would leave two commits recorded nowhere but git, + and the sha is the row's only evidence. One extra window is worth less than that. +- **The sha in a closing row is the commit on the feature branch**, never the one in the phase's scratch + tree: that one has no branch and stops existing when the tree is removed. A row naming a sha nothing can + resolve is worse than a row naming none. +- **The heartbeat names every phase in the round**, at the boundary it opens and the one it closes. An + assignee is a lock with no expiry, and a run that dies mid-round leaves three open rows instead of one. + **A blocking defect is unchanged too**, because it was never a separate object: it is fixed, or it is the `blocked` status in the row this section already describes, or it is an issue. Under this answer the third of those is an issue in the same tracker as everything else, which is the one place the file answer had to diff --git a/.agents/skills/feature-plan/SKILL.md b/.agents/skills/feature-plan/SKILL.md index eb41003..e9f282c 100644 --- a/.agents/skills/feature-plan/SKILL.md +++ b/.agents/skills/feature-plan/SKILL.md @@ -78,9 +78,10 @@ upper-cased. - **If the entry's **Doc** names a draft in `context/drafts/`**, `git mv` it to that path and build the plan on top of its content. Use `git mv`, not `mv`. Do not create a second file and do not leave the draft behind — a draft and a plan for the same feature is two documents disagreeing about one thing. -- **Otherwise** copy `context/plan-template.md` to that path. Copy it verbatim; it is a bare skeleton with - nothing to strip. [`context/plan-template.notes.md`](../../../context/plan-template.notes.md) says what - goes in each section. +- **Otherwise** copy [`context/plan-template.md`](../../../context/plan-template.md) to that path. Copy + it verbatim; it is a bare skeleton with nothing to strip. + [`context/plan-template.notes.md`](../../../context/plan-template.notes.md) says what goes in each + section. **The draft is the most valuable input you have** — material the user gathered deliberately, often from somewhere you cannot reach. Carry its specifics forward; do not summarise them away, and do not silently @@ -105,6 +106,10 @@ planner's own default shape: - **A `Standards:` line on every phase** — the files from that table the phase will be reviewed against, the always row included and the surface rows below added. The plan is the one traversal of the table: whoever implements the phase and whoever reviews it read that line, and neither opens the README again. +- **A `Decisions:` line on every phase** — the §3 decisions that govern it, by number and in a clause + each. Same principle one section over: whoever implements a phase is briefed with that phase's own + section, so a constraint living only in §3 either rides a whole-plan read or gets re-decided + downstream. A phase genuinely governed by none says `—`. - **The surface question, asked out loud** — per the standing rule in [`context/workflow.md`](../../../context/workflow.md): does this feature put something in front of a person, sit in a hot path, or cross a trust boundary? Each *yes* names the standards rows the phases will @@ -123,7 +128,39 @@ planner's own default shape: **Files:** line naming every path it touches. That line is what makes reconciliation a check rather than a judgement call. -### 5. Write the document +### 5. Measure against the budget, before you write + +[`context/tracking.md`](../../../context/tracking.md)'s *How much a plan may hold* gives a **ceiling** and +a **reserve**. Measure the drafted plan's own size, add the reserve for each phase in it, and compare the +total to the ceiling. + +**Measure the file, not your impression of it.** Nothing can count the characters of a draft it is holding +in context. Write the draft to a path outside `context/` and run `wc -m` on it — the plan is written from +there in step 6, or thrown away — and **say the number in the report.** + +**Do this before the write, not after one fails** — under the tracker answer the body is a single write and +a failed one leaves the research with nowhere to go, and in the tree a plan that is already on disk is a +plan somebody will keep rather than split. + +**If that section is not there** — an install from before it shipped — **name the gap, do not invent a +number.** Say the budget is not recorded, name `/onboard`, and write the plan. Inventing a ceiling +mid-run would write policy nobody chose, and refusing to plan over a missing answer would be worse: the +same rule this workflow applies to a path [`context/release.md`](../../../context/release.md) does not +cover. `check` carries its own fallback and says when it used it, so an oversized plan is still reported in +that install. + +**It is not *does this fit*. It is *does it fit with room for the rows still to be written*.** The ledger +is edited for the feature's whole life: every row opens to `in progress` and then closes with a sha and +its Note. That is what the reserve is, and a plan that only just fits has already failed. + +**Under the budget** → carry on to step 6. Say the measurement in the report either way; a check nobody +can see the result of is one nobody trusts. + +**Over the budget** → the feature is more than one feature, and the answer is to split it. Read +[`split.md`](split.md) beside this file — it holds the workarounds that are refused, how the split is +proposed, and what happens to each chunk. Do not improvise one, and **write no plan until it is settled.** + +### 6. Write the document Fill in the template's shape. Then: @@ -140,10 +177,14 @@ Fill in the template's shape. Then: is `context/verify.md`'s answer; **if that file names nothing that drives a browser, say so in §9 rather than inventing one** — a standard named in a phase's review expectations and nowhere checkable is a rule nobody runs, and `/onboard` is what fills the gap. -- Fill in **§9 Open questions** honestly. An honest gap is worth more than an invented decision. +- Fill in **§9 Open questions** honestly. An honest gap is worth more than an invented decision. **Say + which of them need a person** — a reading, a judgement, a pass on a device + [`context/verify.md`](../../../context/verify.md) names no driver for — because `/feature-close` goes + through this section and each one of those has to be answered, filed or waived before the feature + retires. - Every phase is `not started`. -### 6. Update the roadmap entry +### 7. Update the roadmap entry Repoint the entry's **Doc** field at the new `plans/` path. If you `git mv`d a draft, that same edit is what fixes the now-dead `drafts/` link, so do it together. @@ -156,7 +197,7 @@ state for a planned-but-not-started feature. plan, skip the activation, and name the feature that holds it.** The plan is valuable and harmless on its own; discarding it over a marker would undo the point of the split. -### 7. The plan and its entry travel together +### 8. The plan and its entry travel together They are one change: the entry's **Doc** field points at the document, and `check` reports a dead link if the entry exists where the plan does not. Never leave one behind. @@ -171,7 +212,7 @@ This is the one place the ordering is not obvious: everywhere else in this workf it describes land together, and here the document has to land *first*, in a different tree from the one that will use it. -### 8. Report and stop +### 9. Report and stop State the document path, the phase count, the documentation surfaces §7 commits to updating, and the open questions. Then say plainly that **what you produced @@ -184,6 +225,9 @@ once they are satisfied. - **Never implement anything.** Not "just the first phase", not "a quick scaffold". - **Never mark a phase `done`**, and never mark a phase anything other than `not started`. - **Never write outside `context/`.** No source files, no config. -- **Never shrink a plan to fit where it is kept.** A plan too large for its home says the feature is too - large; splitting it is the answer, and trimming the research to fit hides the finding. +- **Never shrink a plan to fit its budget.** A plan over the line in + [`context/tracking.md`](../../../context/tracking.md) says the feature is too large; splitting it is the + answer, and trimming the research to fit hides the finding. Step 5 is where that is measured, and it is + measured under both tracking answers — a plan document having no hard ceiling is not the same as having + no budget. - Do not fold the draft's content into `context/roadmap.md`. Tier 1 stays high-level. diff --git a/.agents/skills/feature-plan/split.md b/.agents/skills/feature-plan/split.md new file mode 100644 index 0000000..f8481b3 --- /dev/null +++ b/.agents/skills/feature-plan/split.md @@ -0,0 +1,62 @@ +# /feature-plan — a plan over its budget + +Read this from step 5 of [`SKILL.md`](SKILL.md), and only when the measurement there came out over the +budget in [`context/tracking.md`](../../../context/tracking.md). Nothing else needs it, which is why it is +not in the body. + +## Over the budget, the feature is more than one feature + +A plan that reaches the ceiling holds the design, the phases and the risks of **more than one piece of +work** — which is already a defect by *Feature or task?* in +[`context/workflow.md`](../../../context/workflow.md), since a feature is work you would want one +`history.md` row for. Under the working-tree answer nothing else in the repository will ever say so. + +**The only honest response is to split the feature.** The budget is not the thing to argue with: it was +telling you something true about the scope, and every way of satisfying it other than the split costs more +than the split does. + +## Three ways out are refused + +Naming them matters because each looks reasonable at the moment it is reached. + +| Workaround | What it costs | +|---|---| +| **Trimming the plan until it fits** | Discards exactly the research the plan was written to hold. And a trimmed plan reads identically to one that was small enough, so nothing downstream can tell them apart | +| **Continuing the plan into a second document** | The plan stops having a single home, and a reader cannot tell which half is current | +| **Linking out to a gist or a paste** | The same, plus a second object to keep in step | + +## Propose, then ask + +**The phase list is already the seam.** Phases are commit-sized units with real `Depends on` values, so a +cut across a dependency boundary is one the research has already justified. The plan's own structure is the +split proposal; you are not inventing a boundary, you are reading one. + +Offer the split as a numbered list — each chunk with the phases it carries and what it waits on — and +**write nothing until the user answers.** Splitting one feature into three is a scope decision, and this +command asks before it commits to far less than that. + +## Once they agree + +- **The entry being planned keeps the first chunk**, and its plan is written as normal. Do not retire it + and open replacements: it is the entry the user chose, and under the tracker answer it is also a thread + with a reporter and subscribers on it. +- **Every other chunk becomes a new Tier-1 backlog entry** — `pending`, with the one or two lines of why + and a size. **No plan and no ledger**: each gets its own `/feature-plan` run when its turn comes, and a + ledger written now would be a plan nobody wrote. +- **Record the order between the chunks** the way [`context/tracking.md`](../../../context/tracking.md) + says. The cut has just established which chunk waits on which, and that is the strongest dependency this + workflow ever knows about. +- **No parent entry, and no entry whose phases are separate objects.** The ledger lives in the plan; a + hierarchy laid over the chunks puts the phase ordering into a second home. + +Then plan the first chunk only, and **report the split first** — what was cut where, what was opened, and +which chunk this plan covers. That is the most consequential thing the run did. + +## If the user declines + +Say plainly that the plan cannot be written as one feature, and **stop**. Do not write a shortened version +as a compromise: a plan trimmed to fit reads exactly like a plan that was small enough, and nothing +downstream can tell the two apart. + +The research is not wasted. It is in the transcript, and the entry is still there to plan once it has been +cut down to one feature. diff --git a/.agents/skills/feature-plan/tracker.md b/.agents/skills/feature-plan/tracker.md index 2d0ff68..d311663 100644 --- a/.agents/skills/feature-plan/tracker.md +++ b/.agents/skills/feature-plan/tracker.md @@ -19,69 +19,42 @@ research, write, report — and only where the plan lands changes. | `git mv` a draft into `plans/` | nothing moves — **the plan replaces the body of the same issue** | | copy `plan-template.md` to a new path | write the template's sections into that issue's body | | the phase ledger, Status column and all | **unchanged** — the same table, written into the body | -| step 6, repoint **Doc** | nothing — a plan is the observation that the body holds a ledger, plus the planned label, which is a rendering of it and is read by nothing | +| step 7, repoint **Doc** | nothing — a plan is the observation that the body holds a ledger, plus the planned label, which is a rendering of it and is read by nothing | | step 2's *already planned* check | the body already holds a phase ledger | -| nothing — a plan document has no size limit | **the body does**, and a plan that overflows it is split rather than trimmed | +| step 5's budget, which nothing in the tree enforces | **the body enforces it** — the same number, and a write that overruns it simply fails | **The ledger does not change shape.** `#`, `Phase`, `Depends on`, `Status`, `Note` — the table a plan document carries, written into the body with every row `not started`. There is no second object to create, nothing to reconcile it against, and **no window in which a plan is half-written: the body is one write.** -### A plan that will not fit is a plan for too much - -That one write has a ceiling. [`context/tracking.md`](../../../context/tracking.md) says how large a body -may be; **measure the plan against it before writing, not after a write fails.** - -The plan must fit with **room to spare**, because the same body is edited for the feature's whole life: -each phase row moves to `in progress` and then to `done`, gaining a commit sha and a note as it closes. A -plan that only just fits has already failed — the last phase would not land. - -**Overflow is a scope signal, and the only honest response is to split the feature.** A filled plan is a -few thousand characters. One that reaches the ceiling is the design, the phases and the risks of more than -one piece of work written into a single document, and the substrate is the first thing that has said so. - -**Three ways out are refused, and naming them matters because each looks reasonable at the moment it is -reached:** - -- **Trimming the plan until it fits.** That discards exactly the research the plan was written to hold, to - satisfy a limit that was telling you something true. -- **Continuing the plan into comments.** The plan would then have no single home, and a reader could not - tell which half is current — the thing putting the ledger back in the body settled. -- **Linking out to a document or a paste.** Same failure, plus a second place to keep in step. - -#### How to split - -**Propose, then ask.** The phase list is already the seam — phases are commit-sized units with real -`Depends on` values, so a cut across a dependency boundary is a cut the research has already justified. -Offer the split as a numbered list, each chunk with the phases it carries and what it depends on, and -**write nothing until the user answers.** Splitting a feature in two is a scope decision, and this command -asks before it commits far less than that. - -Once they agree: - -- **The issue being planned keeps the first chunk**, and its plan is written into the body as normal. Keep - the id: the thread, the reporter and everyone subscribed are the same reason adoption does not open a - second issue about one thing. -- **Every other chunk becomes a new backlog issue** — carrying the backlog label, `pending`, with the one - or two lines of why, a `Size:` and a `Priority:`. No plan and no ledger: they are Tier-1 entries, and each - gets its own `/feature-plan` run when its turn comes. -- **The order between the chunks is a relationship, not a sentence.** A cut along a dependency boundary has - just established which chunk waits on which, so record it the way - [`context/tracking.md`](../../../context/tracking.md) says — one write per edge, visible from both ends. - This is the strongest dependency the workflow ever knows about, and writing it into a body instead would - leave the backlog's next reader to notice it by eye. -- **Cross-link anything the relationship does not carry**, so the split is visible from any one of them. - Two chunks that share a subject without either waiting on the other get a mention in each body and **no - relationship** — recording one there would hide runnable work behind nothing. -- **No parent issue, and no issue whose phases are separate objects.** The ledger lives in a body; a - hierarchy laid over that is a second home for the same ordering. - -Then plan the first chunk only, and **report the split first** — what was cut where, which issues were -opened, and which one this plan covers. That is the most consequential thing the run did. - -**If the user declines the split**, say plainly that the plan cannot be written into this substrate as one -feature, and stop. Do not write a shortened version as a compromise: a plan trimmed to fit reads exactly -like a plan that was small enough, and nothing downstream can tell the two apart. +### A plan that will not fit — the same check, with a harder consequence + +Step 5 measures the plan against *How much a plan may hold* in +[`context/tracking.md`](../../../context/tracking.md) and sends an over-budget plan to +[`split.md`](split.md). **Both hold here unchanged**, and the number is the same number. What differs is +what the ceiling is and what the chunks are made of. + +**The ceiling is the substrate's, not a budget.** The body holds exactly that many characters and the +write is refused at one more, so overflow is not a report — it is a failed write with the research done +and nowhere to put it. **The body is one write**, so the measurement is the only thing standing between a +finished draft and a lost one. That is also why `/tracking-migrate` refuses an oversized plan rather than +carrying it: the split happens in the tree first. + +**`split.md`'s chunks are issues here.** Each new one carries the backlog label, `pending`, the one or two +lines of why, a `Size:` and a `Priority:`. **The issue being planned keeps its id** — the thread, the +reporter and everyone subscribed are the same reason adoption does not open a second issue about one +thing. + +**The order between chunks is a relationship, not a sentence.** One write per edge, visible from both +ends, as [`context/tracking.md`](../../../context/tracking.md) says. Writing it into a body instead would +leave the backlog's next reader to notice it by eye. + +**Cross-link anything the relationship does not carry**, so the split is visible from any one of them. Two +chunks that share a subject without either waiting on the other get a mention in each body and **no +relationship** — recording one there would hide runnable work behind nothing. + +**The planned label goes on the chunk that was planned and on nothing else.** The others are Tier-1 +entries with no plan. ### What it waits on — read before the ranking, set after the research @@ -160,12 +133,12 @@ it and say so once — it is metadata, not a gate. **`--activate` assigns the issue** rather than editing a marker, subject to the same one-active-feature rule and the same outcome when the slot is held: write the plan, skip the activation, name the holder. -**Step 7's ordering problem disappears, and that is worth knowing.** Under the working-tree answer the plan +**Step 8's ordering problem disappears, and that is worth knowing.** Under the working-tree answer the plan has to be committed and pushed *before* a worktree exists, because a tree carries only what its source ref holds. An issue is in no ref: it is visible from every tree the moment it exists. Plan, then create the tree, in whatever order suits — nothing here has to land first. -**Step 5's `**Status:**` rule applies to the issue body verbatim, and only to the feature.** The body holds +**Step 6's `**Status:**` rule applies to the issue body verbatim, and only to the feature.** The body holds the plan and never a line claiming where the *feature* stands; that fact is the assignee. The ledger's Status column answers a different question at a different scope, and it belongs in the body exactly as it belongs in a plan document. diff --git a/.agents/skills/feature-status/SKILL.md b/.agents/skills/feature-status/SKILL.md index 4ff1a83..ae5a667 100644 --- a/.agents/skills/feature-status/SKILL.md +++ b/.agents/skills/feature-status/SKILL.md @@ -38,6 +38,9 @@ branch merges. So enumerate them. 1. `git worktree list` — every working tree, its path and its branch. **That is the state.** Nothing records which features are in flight; a worktree existing is the claim, and removing it withdraws the claim. Do not write a file to track this, and do not read one if you find it. + **Skip the ones with no branch.** A detached tree under `.phase-trees/` is a phase's scratch checkout, + made by `/feature-implement`, and a tree with no branch is nobody's claim. One left behind belongs to a + phase that did not land — name its path in the report, read nothing out of it, and remove nothing. 2. For each tree other than this one, read `/context/roadmap.md` for its `active` entry and that entry's ledger at `/context/plans/-PLAN.md`. Two numbers per tree — phases `done` out of phases total — and its lowest not-`done` phase. diff --git a/.agents/skills/onboard/SKILL.md b/.agents/skills/onboard/SKILL.md index ab9f41a..dbfdacb 100644 --- a/.agents/skills/onboard/SKILL.md +++ b/.agents/skills/onboard/SKILL.md @@ -293,6 +293,23 @@ Three outcomes, and only the first writes the answer alone: **Never write the tracker answer and delete the tree files in the same run.** Removal is what makes the migration irreversible, and this command has no way to show a remote write as a diff first. +### How much a plan may hold — leave it alone, and say it is there + +That section of [`context/tracking.md`](../../../context/tracking.md) ships with both its numbers filled +in and **holds under either answer above**, so there is nothing to ask. Two things are still yours: + +- **Say the budget exists and what it is**, in one line, for the same reason this step reports at all. It + is the one answer in this file a user is most likely to want to change and least likely to discover. +- **If the section is absent** — an install from before it shipped, which `update` reports and cannot fix, + since this file is the project's — **write it from + [`context/tracking.notes.md`](../../../context/tracking.notes.md) with the shipped numbers** and say you + did. Until it is there `/feature-plan` names the gap and plans anyway, so this is the thing that closes + it. + +**Do not change the numbers unless the user asks.** The ceiling is the issue body's hard limit under the +tracker answer and the working-tree answer takes the same number on purpose, so raising it here is a +choice to write plans a tracker would not accept. If they ask, say that before editing. + ### Under the tracker answer Write the tracker answer's paragraph from @@ -385,7 +402,9 @@ in [`context/verify.notes.md`](../../../context/verify.notes.md). request, because Gate 1 runs per phase and would flag every docs-only change. 2. **Show the candidates and ask** which belong in Lint, Typecheck, Build and Test, and whether anything is missing. Ask about prerequisites too — a package manager version, an install step, a service that must - be up. + be up. **Whatever has to be *run* to make a clean checkout pass goes in *Prepare a fresh tree***, not + into the preamble: `/feature-implement` runs that section when it prepares a tree for a phase, and + without it runs one phase at a time. 3. **Ask what else this project runs to prove a change is good.** The four headings are the ones every project has, not the whole of what one checks — an end-to-end run, an accessibility suite, a size budget, a coverage floor, a visual snapshot, a dependency audit. **Ask specifically about the end-to-end @@ -402,6 +421,15 @@ in [`context/verify.notes.md`](../../../context/verify.notes.md). when an advisory is published against a lockfile nobody touched, and a phase gate cannot tell that from a defect the phase introduced. Ask before sorting one there — a project that holds the check clean as a standing invariant has decided the opposite, and for that project a red one is this change's problem. + **Then offer one candidate of your own, and only this one: a check that no test in the suite is + skipped, focused or expected to fail.** A runner reports a skipped test as a pass and a focused one + silences every test beside it, so the Test section can exit 0 while the suite asserts almost nothing, + and nothing else you write here would notice. It is cheap and the change is what makes it fail, so it + belongs above *Not run by Gate 1* — inside Test where this project's runner or its lint rules can + assert it, otherwise under a heading of its own. **Offer it; do not assume it.** A project that marks + work in progress with a skip has chosen something this check forbids, and declining is an answer. If it + is taken, the command is whatever this project already has or a short one written for it, and like every + other it is run in step 4 before it is written down. 4. **Run every candidate bound for a gate section.** Actually run it, from the repo root. One sorted under *Not run by Gate 1* is not run here — this command has no Docker, no deploy target and no reason to spend minutes driving a browser — and step 6 says what is recorded in its place. @@ -415,6 +443,36 @@ in [`context/verify.notes.md`](../../../context/verify.notes.md). a check this project has and this gate does not run; without the name it reads as a check nobody runs, which is a different and much worse fact. +7. **Verify *Prepare a fresh tree* in a tree that is actually fresh.** Step 4 does not run it — it is below + the Gate 1 line, and it is an answer about a checkout nobody has lived in, which this one is not. Clone + the repository into a temporary directory instead: `git clone --no-hardlinks . `. **That is not a + worktree**, and it is not the invocation [`context/executors.md`](../../../context/executors.md) names; + nothing here makes one of those. Run the candidates there, in order, and then **check the tree is still + clean** — `git -C status --porcelain` must print nothing. **A dirty tree is a wrong answer, not + something to work around:** name what it changed and fix the answer — the install form that reads the + lockfile instead of rewriting it, or a `.gitignore` entry for a file a step generates. Remove the + directory afterwards. Where this repository has no commit yet there is nothing to clone, so say the + section was written unverified and name what you would have run. + +8. **Then put the phase trees out of Gate 1's way, and prove they are.** `/feature-implement` runs a round + of phases in detached checkouts under `.phase-trees/` **inside this working tree**, so every command + above has to pass over that directory — or phase *k*'s gate lints the other phases' half-written work, + runs their copies of the suite, and in at least one real configuration *writes* to them. + - **Write `.phase-trees/` into this project's `.gitignore`** at the repository root. Not + `.git/info/exclude`: that is per-clone rather than shared, and some hosts refuse to write inside the + git directory at all. The entry is where most tools take their ignore list from, and it is also what + keeps these checkouts out of `git status` and out of `git add -A`. + - **Then prove it, because honouring that file is a per-tool setting and not every tool does.** Plant + two files under `.phase-trees/probe/`: one the lint command must reject, and one test that must fail. + Run every command bound for a gate section. **None of them may see either file.** + - **Where one does, fix that tool's own configuration** — whichever it has: a setting that reads the + ignore file, a file list, a test exclude — then run it again. **Say which tool and which setting**; + that edit is this project's, not this workflow's. + - **Where it cannot be fixed, set the cap to 1** in *Coder* in + [`context/executors.md`](../../../context/executors.md), with the reason beside it. That is the + recorded way to say one phase at a time, and it is an answer rather than a gap. + - **Remove the planted files**, and report what the proof found either way. + Explain what you are doing: this turns `verify.md` from someone's guess into something verified at install time, which is the one moment it is cheap to catch. @@ -581,8 +639,10 @@ leaving it implied. the unit. - **What a release ships, and on what event** — fill in what bumps a version, what tags, what cuts a release, what publishes and what deploys, one line each, and write "nothing yet" where that is the truth. - **Nothing in this workflow does any of the five**, and this section is what stops the answer being read as - "releases happen automatically". + **Nothing in this workflow does any of the five on its own initiative** — `/release` runs the *Bump* wire + in the turn somebody types that command, and nothing at all runs the other four — and this section is what + stops the answer being read as "releases happen automatically". **Say who runs the Bump wire while writing + it**: a wire nobody is recorded as running is the one most likely to be read as firing by itself. Then, from the sweep's fourth line, **the event and what it ships per path**. There is **one event, not one per artifact kind**: the merge of the pull request where the notes were consumed and the versions diff --git a/.agents/skills/orchestrate/SKILL.md b/.agents/skills/orchestrate/SKILL.md index a9df40b..5af16e1 100644 --- a/.agents/skills/orchestrate/SKILL.md +++ b/.agents/skills/orchestrate/SKILL.md @@ -1,6 +1,6 @@ --- name: orchestrate -description: "Run one ad-hoc, commit-sized change through the same verification and review gates the feature loop uses, without a roadmap entry or a phase ledger. Explicit invocation only — run this when the user types /orchestrate. Do NOT match on 'build X', 'implement X', 'orchestrate the work', or any request that belongs to a planned feature." +description: "Run one ad-hoc, commit-sized change through the same verification and review gates the feature loop uses, without a roadmap entry or a phase ledger — or, with --pr, end that run at a pull request instead of in the working tree. Explicit invocation only — run this when the user types /orchestrate. Do NOT match on 'build X', 'implement X', 'orchestrate the work', or any request that belongs to a planned feature." --- # /orchestrate @@ -20,6 +20,8 @@ Read [`context/workflow.md`](../../../context/workflow.md) for the gate contract ``` /orchestrate "" /orchestrate # # the issue is the brief +/orchestrate --pr "" # end at a pull request, not in the working tree (step 7) +/orchestrate --pr # ``` **The second form is how a bug reaches this loop.** A defect too small to plan has no backlog entry and @@ -49,7 +51,8 @@ A refusal here is the workflow working. ## 2. Do the work -Read `context/stack.md` and load `context/standards/README.md` per its conditional table. +Read [`context/stack.md`](../../../context/stack.md) and load `context/standards/README.md` per its +conditional table. **Ask the surface question first**, per the standing rule in [`context/workflow.md`](../../../context/workflow.md): does this change put something in front of a person, @@ -104,9 +107,19 @@ found* in [`context/workflow.md`](../../../context/workflow.md). - `PASS` or `PASS WITH NOTES` → done. - `FAIL` → loop back. -**A non-blocking observation goes in this run's report and dies with the session** — unless it needs code -changes, in which case it is work and belongs in the backlog per -[`context/tracking.md`](../../../context/tracking.md). +**Every non-blocking observation gets one of the ends in *What happens to a defect the gate found* in +[`context/workflow.md`](../../../context/workflow.md), and kind decides which** — not size, and **not +whether it needs code changes**, which every true observation about code does. + +- **A real defect, and it is user-visible or a regression would land green** → a **bug**, wherever this + project already files bugs, carrying this project's own labels. **Never the backlog label** — see *A bug + is not a backlog entry* in that same file. The backlog is a list of features and this is not one. +- **Not a defect, but work you would want a `history.md` row for** → that is a backlog entry, and adding + one is `/roadmap`'s. **Name it and say what you would add**; do not append to the backlog from here. That + command applies the worth-adopting test and writes what the ranking reads, and a gate doing it by hand + produces neither. +- **Anything else** → this run's report, and it dies with the session. **There is no `context/notes.md` in + this command** — step 5 says why. ## 5. Loopback @@ -128,22 +141,25 @@ resume — the tree either carries the change or it does not. **This command writes no `context/notes.md`**, and that is the one place it departs from *What happens to a defect the gate found* in [`context/workflow.md`](../../../context/workflow.md). That file's cheap end is bounded by `/feature-close` deleting it, and an ad-hoc change has no close — so a note written here is the -one that would outlive every branch. **An observation that is work becomes an issue; every other one goes -in the report and dies with the session.** +one that would outlive every branch. **So step 4's cheap end has nowhere to live here: a bug is filed, a +feature is `/roadmap`'s to admit, and everything else goes in the report and dies with the session.** ## 6. Land it — read [`context/git.md`](../../../context/git.md) -**Do not commit unless that file says the agent does.** If it does not exist, the answer is *the user -commits*: say so once, and name `/onboard`. +**Do not commit unless that file says the agent does, or `--pr` was typed** — that flag is the same file's +other source of permission and step 7 is where it is spent. If the file does not exist, the answer is *the +user commits*: say so once, and name `/onboard`. -**Nothing here branches, worktrees or pushes**, whatever *Where work lands* and *Push and pull request* -say. Both of those answers are about a feature — one branch or tree per entry, one push at +**Without `--pr`, nothing here branches, worktrees or pushes**, whatever *Where work lands* and *Push and +pull request* say. Both of those answers are about a feature — one branch or tree per entry, one push at `/feature-close` — and an ad-hoc change has no entry and no feature to close. It lands on whatever branch is already checked out. **A worktree answer is not permission to move an ad-hoc change into a tree of its -own**; that is the commonest way this rule gets read backwards. +own**; that is the commonest way this rule gets read backwards, and **the flag does not change it** — step 7 +pushes a branch and creates no tree. - **The user commits** → leave the change **unstaged** in the working tree and hand it over. Staging it is - not a head start; it is half a commit. + not a head start; it is half a commit. **Under `--pr`, go to step 7 instead**: a pull request with nothing + committed to it is not a smaller version of this flag, it is nothing. - **The agent commits** → one commit, at the granularity that file names. **Run from `#`, the commit is what closes it.** Put `Closes #` in the commit message, the @@ -153,13 +169,74 @@ user commits, say the line rather than writing it**: it is their commit, and a c on their behalf. Never close the issue by hand as a separate act — a closed issue whose fix is sitting unstaged in somebody's tree is worse than an open one. -## 7. Report +## 7. `--pr` — the change leaves the machine + +Without the flag this invocation is over at step 6, and the change is committed or waiting on whatever +branch you were already on. With it, the run ends at a **pull request**: one commit, the branch pushed, and +the work opened for a person to read. + +**The flag is the permission, not a new `git.md` answer.** That file names two sources and only two — an +answer covering this command at this point, *or* the user asking in this session in plain words — and a +typed flag is the second one. So the flag does not need *Who commits* to say the agent does, and it does +not read *Push and pull request*: both of those answers are about a feature, and this command has none. +What it authorises is **this invocation**. Nothing it does becomes policy for the next one, and nothing +here is written into `git.md`. + +It buys exactly three things, in this order: **the commit, a push of the branch you are on, and the pull +request.** Anything past that is refused below, under *What the flag is not*. + +**The body is this run's report** — step 8's content, which is the only description of the change that +exists. Run from `#`, step 6 already put `Closes #` in the commit, so the body does not repeat +it: the trailer closes the issue on merge whichever text the forge reads. + +### It cannot invent a branch + +- **HEAD is not the default branch** → commit, push that branch, open the pull request. Nothing was created, + which is why this is the case the flag is built for. +- **HEAD is the default branch** → **stop before the commit**, unless *Branch and worktree* in + [`context/executors.md`](../../../context/executors.md) names a command — then run that one. Pushing to + the default branch is asked for by name each time under every `git.md` answer, and an empty *Branch and + worktree* means the workflow makes no branch, not that it should improvise `git checkout -b`. Say which + of the two stopped you, leave the change in the tree, and let the user make a branch and run it again. +- **That command makes a worktree rather than a branch** → stop and say so. Step 6 refuses to move an + ad-hoc change into a tree of its own, and a flag on this command is not the user lifting that. + +### Stop, and hand back + +The flag is permission to finish, not an instruction to land. **Stop, report, and name the line that +stopped you**, when: + +- **step 1 refuses the scope.** A flag is not a reason to admit work that is not commit-sized, that a + roadmap entry already covers, or that arrived carrying the backlog label. +- **Gate 1 does not come back clean, or Gate 2's verdict is `FAIL` on its last allowed loop.** Nothing is + committed and nothing is pushed: a pull request is where finished work goes to be read by a person, not + where unfinished work goes to be verified. Step 5's handback is unchanged — the change stays in the tree. +- **the push will not fast-forward**, or the repository has no remote to push to. + +**Step 2's release-note ask still happens.** The level is a judgment and `release.md` has it confirmed +before the note is written; this flag names where the run ends, not that nobody is reading. A run that +waits there is working as intended. + +### What the flag is not + +**Nothing merges it.** Not on green CI, not on an approving review, not after any wait. `git.md` reserves +review and merge in one line — *review and merge are yours* — and a pull request nobody reads is the one +thing this flag must not produce, since then the push bought nothing the working tree did not already have. +It also does not delete a branch, enable the forge's own auto-merge, or stay alive watching a check run. + +**It lifts no refusal and widens nothing.** It does not skip a gate, raise a loopback cap, admit a scope +step 1 refused, or turn an ad-hoc change into a feature. A stop list worked around once is not a stop list. + +## 8. Report What changed, whether it is committed or waiting in the tree, the Gate 1 output, the Gate 2 verdict, any loopbacks, any non-blocking observations the review raised — which die here — any bug filed for work that outlived the change, whether the commit closes the issue it ran from, and any release note written, with the paths that were checked and owed nothing. +**Under `--pr`**, also the branch that was pushed and the pull request that was opened — or the line in +step 7 that stopped short of one — and that nothing merged it. + ## Rules - **No ledger row is touched.** This command has no phase and does not belong to a feature. @@ -168,3 +245,4 @@ the paths that were checked and owed nothing. - **The release note is not deferred to a later command.** There is no `/feature-close` behind this one to write it, which is exactly why the change is the unit. - **Never skip Gate 1 to save time.** The gates are the entire reason this command exists. +- **Nothing merges, and nothing waits for CI.** `--pr` ends at a pull request a person has not read yet. diff --git a/.agents/skills/prototype/SKILL.md b/.agents/skills/prototype/SKILL.md index 644e4ff..3fb171d 100644 --- a/.agents/skills/prototype/SKILL.md +++ b/.agents/skills/prototype/SKILL.md @@ -39,8 +39,9 @@ Say which question the mockup exists to answer, in one line, before writing anyt **Borrow before inventing.** This repository exists already, so most of the look is decided — and a mockup built from values the project does not have is a picture of an app nobody is going to build. -Read `context/stack.md`, then find the real thing: the stylesheet or token file, the design system or -component library, and two or three existing screens close to what is being mocked. Copy the actual token +Read [`context/stack.md`](../../../context/stack.md), then find the real thing: the stylesheet or token +file, the design system or component library, and two or three existing screens close to what is being +mocked. Copy the actual token names, spacing scale, radii and type stack out of them. Invent only where there is genuinely nothing to borrow — a new surface, or a project with no styling of its diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 0000000..b68652b --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,251 @@ +--- +name: release +description: "Cut a release from the notes already accumulated on the default branch — make the branch the recorded command makes, run the script context/release.md's Bump wire names, and end at a pull request for a person to approve. Explicit invocation only — run this when the user types /release. Do NOT match on 'ship it', 'cut a release', 'release this', 'publish it', or a release being obviously due." +--- + +# /release + +Consumes the notes that are waiting, moves the versions, and ends at a **pull request nobody has read +yet.** No roadmap entry, no ledger, **no tier boundary crossed** — and nothing released: the version moved +in a branch, and what ships it is the merge. + +It exists because [`context/release.md`](../../../context/release.md) has always named the ship event as +*the merge of the release pull request* — and nothing in this workflow created one. `/feature-close +--release` fuses the release into a feature's own merge, which is right for a repository that releases +about as often as it merges. This command is the separated shape: the notes accumulate on the default +branch as features land, and somebody decides when they go out. + +Read [`context/workflow.md`](../../../context/workflow.md) for the standing rules — above all +*Writing a note is the workflow's half; consuming notes is not*, which this command is the asked-for +exception to. + +## Usage + +``` +/release # the branch is release/, as YYYY-MM-DD +/release "" # the branch is release/ +``` + +**Typing the command is the permission.** [`context/workflow.md`](../../../context/workflow.md) forbids +running what bumps, tags, publishes or deploys, with one exception — *when the user asks for it in that +turn* — and **a typed command is what that asking looks like**, the same way `/orchestrate --pr` and +`/feature-close --release` are. So it is never inferred: not from "and ship it" earlier in the session, not +from a plan, not from the notes looking ready, and not from a release being plainly overdue. **A sentence +is not a command.** + +**The name is the act, not the version.** The version is not knowable until the script that consumes the +notes has run, and that script **cannot be run twice** — so a branch named for the version cannot exist +before the thing that would name it. The version goes in the commit message and the pull request title, +where it is known. A name that already exists **stops the run**: do not suffix it, because an existing +release branch is either one in progress or one somebody abandoned, and both want a person. + +## 1. Refuse, before anything else + +Nothing is created and nothing is run until all five pass. They are in this order because the first two +cost nothing and are the common case. + +1. **No *Bump* wire in [`context/release.md`](../../../context/release.md)** → nothing in this repository + is written down as consuming the notes, so there is nothing to run and inventing it would be guessing at + how somebody releases. Refuse and name `/onboard`. The notes are unharmed; they wait, which is what + they are for. +2. **That file's *What records a note* answer is *nothing*** → this project records no notes, so there are + none to consume. Refuse and name `/onboard`. +3. **No notes are pending** → there is nothing to release. A run that moves no version publishes nothing + and writes a changelog entry describing nothing. Say so and stop; do not reach for whatever the + mechanism offers to make the run proceed. +4. **No invocation under *Branch and worktree* in + [`context/executors.md`](../../../context/executors.md)** → the workflow makes no branch, and **that is + not a licence to improvise one.** Never run a bare `git checkout -b`, `git branch` or `git worktree + add` because that section is empty. The alternative to a branch here is moving a version on the default + branch and pushing it, which is asked for by name each time under every + [`context/git.md`](../../../context/git.md) answer and is not what typing this command asked for. + Refuse, name `/onboard`, and say the release can still be cut by hand. +5. **HEAD is not the default branch, or the working tree is not clean** → a release is cut from the ref the + notes accumulated on, and uncommitted work would be swept into the release commit. Say which of the two + stopped you and stop. + +**A feature in flight is not a reason to refuse.** Its notes are not on this ref yet, so this release does +not include it — and that is the property the whole command rests on: **the notes on this ref are exactly +the changes on this ref.** Say which features are in flight and that they are not in this release. + +A refusal here is the workflow working, not a problem to route around. + +## 2. Gate 1, before anything is consumed + +Read [`context/verify.md`](../../../context/verify.md) and run every section above *Not run by Gate 1*, in +order — Lint → Typecheck → Build → Test first, then anything that file adds after them. The gate contract +is in [`context/workflow.md`](../../../context/workflow.md); a missing section is skipped, never faked, and +exit 0 is the verdict regardless of what any summary text claims. + +**It runs here, before the branch exists and before the script runs, and that ordering is the whole +reason this section is second.** The script that consumes the notes cannot be run twice. A gate run +afterwards, on a default branch that turns out to be red, leaves a tree whose notes are gone and whose +release cannot be cut again — **the one unrecoverable state in this design.** Finding out first costs one +gate run. + +**Gate 1 not clean → stop.** Nothing created, nothing consumed, nothing to undo. Report what failed and +say the notes are untouched. + +**There is no Gate 2, and that is deliberate.** Gate 2 reviews code against this project's standards, and +this run writes none — the version move and the changelog are a script's output. The review this change +needs is a **person's, on the pull request**, which is the entire reason the command ends at one. Do not +dispatch a reviewer to read a generated changelog. + +## 3. Show what it will consume, and confirm — before anything is created + +**This is the last moment anything here is free to change, and it is the only confirmation this command +has.** + +List **every pending note.** Not this session's, not the ones you can account for — every one, including +notes other people wrote for work that merged while nobody was releasing. For each, say what it describes +and what level it carries. Then, from [`context/release.md`](../../../context/release.md): + +- **what version each package lands on**, per note level; +- **what the merge of this pull request will do to each path**, from that file's *what a release ships* + answer — published, deployed, or nothing — and **what it leaves behind**. Read it; never infer it from + the fact that a version exists. + +Then confirm, using your runtime's question mechanism if it has one. + +**Every level is now final, and say so while asking.** [`context/release.md`](../../../context/release.md) +has the level confirmed where a note is written rather than here, on the reasoning that a note is a tracked +file publishing nothing and a wrong level is cheap to correct right up to the release. **This command is +that release.** A level that looks wrong is corrected by editing the note before the script runs, and after +it there is nothing left to edit — the note is gone and the version has moved. + +**Where that file records no answer for a path this release moves**, name the path, say the answer is +missing, and name `/onboard`. Do not guess at what the merge does to it, and do not hold up the release +over a gap in a configuration file. + +**Name no release tool.** That file says what records a note here and what consumes them. + +## 4. Make the branch, then run the Bump wire — in that order + +**The branch exists before the notes are consumed.** A script that eats the notes in a tree with no branch +to carry them has put the only copy of every pending note into whatever was checked out, and guard 5 says +that is the default branch. + +1. **Create it with the invocation [`context/executors.md`](../../../context/executors.md) names under + *Branch and worktree*.** That invocation, or nothing — never the bare git command, which skips whatever + the recorded one does around it. Say which invocation you ran. + + **A separate working tree is welcome here**, and this is the one place in the workflow where that is + true. `/orchestrate --pr` refuses one because an ad-hoc change has no feature and a tree of its own was + not what any answer authorised; a release is not ad-hoc work, and a branch of its own is the point of + the command rather than a side effect of it. Run whichever the recorded invocation makes, say which, and + **where it makes a separate tree, run everything below inside it.** + +2. **Then run the script the *Bump* wire names, exactly once.** It consumes every note, moves the versions + and writes the changelogs. [`context/release.md`](../../../context/release.md) says what it is; naming a + tool here would be wrong in most repositories this command runs in. + +3. **If it fails part-way, stop.** Do not re-run it and do not reach for a repair: the notes may already be + gone, and a second run on a half-consumed set is how a release goes out describing half of itself. + Report what it printed, what state the tree is in, and that the branch is there to inspect. + +## 5. Read the diff + +The versions that moved and the changelog text are the whole of it, and **the changelog text is what the +release page will say** — that is what +[`context/release.md`](../../../context/release.md)'s *Release* wire does with it, and it is where a note +finally reaches the audience it was written for. Read it rather than trusting the script's summary, and say: + +- **every version that moved, from what to what**, and whether that matches what step 3 confirmed. A + disagreement is a finding, not a rounding error. +- **every package that moved which no note named.** A major can drag a dependent in with a patch where it + puts that dependent's range out of range — and **where the dependent is a path that deploys, its version + moved, so this merge deploys it.** Say so plainly; it is the consequence least likely to be noticed and + most likely to matter. +- **any changelog entry that reads wrong.** Report it. The text came from a note, the note is gone, and + rewriting a shipped changelog is the user's call on the pull request rather than something to tidy up + here. + +**Never hand-edit a version.** The Bump wire owns every version it moved, and a hand-typed one is a number +nothing agrees with. + +## 6. Commit, push, open the pull request + +Read [`context/git.md`](../../../context/git.md). **The typed command is the permission, not a new answer +in that file.** It names two sources and only two — an answer covering this command at this point, *or* the +user asking in this session in plain words — and a typed command is the second. So this command does not +need *Who commits* to say the agent commits, and it does not read *Push and pull request*: both of those +answers are written about a feature, and a release is not one. What it authorises is **this invocation**. +Nothing it does becomes policy for the next one, and **nothing here is written into +[`context/git.md`](../../../context/git.md).** + +**One commit**, carrying the version moves and the changelogs and nothing else — guard 5 made sure there +was nothing else. Name the version in its subject. + +**Push that branch and open the pull request.** Its title carries the version; its body is: + +- every version that moved, +- the changelog entry for each, or a link to it in the file — **never a summary written in its place.** + That text is the description of this release and it was written by whoever wrote each note; +- what the merge will ship per path, and what it will leave behind, from + [`context/release.md`](../../../context/release.md); +- which features' notes are in it. + +**The push is a plain push of this branch.** If it will not fast-forward, or there is no remote, **stop and +say so** — and say it precisely, because the state is unusual: the branch exists, the notes are consumed, +and the commit is sitting there for the user to push. Force-pushing and pushing to the default branch are +asked for by name each time, under every answer, and this command has not been given either. + +### Nothing merges it + +Not on green CI, not on an approving review, not after any wait. +[`context/git.md`](../../../context/git.md) reserves it in one line — *review and merge are yours* — and a +release pull request nobody reads is the one thing this command must not produce, since the whole of what +it buys over moving the version on the default branch is that a person looks first. It also does not delete +a branch, enable the forge's own auto-merge, or stay alive watching a check run. + +**It removes nothing either.** Where the invocation in step 4 made a separate working tree, that tree is +still there after this command ends, and removing it is the job of whatever +[`context/executors.md`](../../../context/executors.md) names — after the merge, and not this command's at +any point. Say in the report that it is there. + +### The note check will go red, and a note is not what fixes it + +Where [`context/release.md`](../../../context/release.md)'s *What records a note* answer names a check that +asks *are there pending notes*, it will fail on this pull request, every time: there are none, **because +they became the changelog.** The description exists, more permanently than before. + +**Do not write a note to silence it.** These mechanisms offer a placeholder note carrying no change, and it +is the obvious way past a check that has gone red on the one commit that owes nothing — which is exactly +why reaching for it is the signal that **the check is asking the wrong question.** That file says what to +do about it, and the condition that resolves it is the one its ship answer already uses: *this path's +version moved in this merge.* + +Report it and leave it. Where the answer records the exemption and the check is red anyway, say that too — +the answer claims something the check does not do, and closing that gap is editing a workflow, which is not +this command's to edit. + +## 7. Report + +- the branch, and the invocation that made it — and where that made a separate working tree, that it is + still there and what removes one. +- **every note consumed**, and whose work each described. +- **every version that moved, from what to what**, including any package no note named. +- the pull request, and **that nothing merged it.** +- **what the merge will ship, per path, and what it will leave behind** — read from + [`context/release.md`](../../../context/release.md), never inferred. +- any path that file has no answer for, named rather than guessed at, with `/onboard`. +- the note check, if it went red, and that no note was written to quiet it. + +**Say what this is waiting for.** The merge. **Never report this as released, published, deployed or +live** — a version moved in a branch, and that is all that has happened. For a path that deploys, the merge +is what puts the change in front of users, and saying so is how somebody knows what approving the pull +request does. + +## Rules + +- **The Bump wire runs exactly once, and never twice.** It consumes the notes as it goes. +- **Never cut a release on the default branch.** Pushing to it is asked for by name each time, under every + answer, and this command was not given that. +- **Never improvise the branch.** [`context/executors.md`](../../../context/executors.md) names the + invocation or there is no branch. +- **Never write a release note here.** This command consumes notes; it has no change of its own to + announce, and a release commit carrying a note is a release describing itself. +- **Never hand-edit a version or a published changelog.** +- **No ledger row is touched, and no roadmap entry is created, activated or retired.** A release is not a + feature. +- **Nothing merges, nothing waits for CI, and nothing is removed.** diff --git a/.agents/skills/tracking-migrate/SKILL.md b/.agents/skills/tracking-migrate/SKILL.md index 1f83e55..7ca224e 100644 --- a/.agents/skills/tracking-migrate/SKILL.md +++ b/.agents/skills/tracking-migrate/SKILL.md @@ -60,10 +60,13 @@ list exists to prevent. `/feature-status` is where that gets resolved. - **A plan in `plans/` with no roadmap entry, or an entry whose **Doc** points at nothing.** The entry and its document travel together, and a migration is the wrong moment to decide which of the two is right. -- **A plan too large for an issue body.** [`context/tracking.md`](../../../context/tracking.md) says how - large a body may be; measure every plan against it, leaving room for the ledger rows that will be written - into the body later. Name each feature that overflows and the margin by which it does, and stop. **A plan - that does not fit is a feature that is several features**, and splitting one in two is a scope decision — +- **A plan too large for an issue body.** *How much a plan may hold* in + [`context/tracking.md`](../../../context/tracking.md) gives the ceiling and the per-phase reserve; + measure every plan against both. **A repository that has been measuring never reaches this refusal** — + the number is the same under both answers, so a plan `/feature-plan` passed in the tree fits a body. + Reaching it means these plans predate the budget, or somebody raised it here. Name each feature that + overflows and the margin by which it does, and stop. **A plan that does not fit is a feature that is + several features**, and splitting one in two is a scope decision — the same reason the row above refuses. Split it in the tree first, into an entry and a plan apiece, then re-run. Never trim a plan to fit, and never spill one into comments or a linked paste: this command moves data, and a plan that arrives shorter than it left has not been moved. diff --git a/.claude/agents/planner.agent.md b/.claude/agents/planner.agent.md index 43a2906..cedd68d 100644 --- a/.claude/agents/planner.agent.md +++ b/.claude/agents/planner.agent.md @@ -35,6 +35,17 @@ The default shape below applies only when the brief names none. `Standards:` line naming the files it will be reviewed against — the rows the table gave you for that phase's work, the always row included, and the rows the surface question in step 5 adds. The coder and the reviewer load that line, not the table. + + **Every phase also carries a `Decisions:` line** — the §3 decisions that govern it, by number and in a + clause each. Same reason: the coder and the reviewer are briefed with the phase's own section, so a + constraint that lives only in §3 either rides a whole-plan read or gets re-decided downstream. A phase + genuinely governed by none says `—`. + + **Keep the plan within its budget.** *How much a plan may hold* in `context/tracking.md` gives a + ceiling and a per-phase reserve, and the caller measures your draft against them before writing it. A + draft that overruns the ceiling is not trimmed — it is a feature that is more than one feature, and the + caller will come back asking for a split along your phase boundaries. Write the research the plan needs + and say in your report if you think it is carrying more than one feature's worth. 2. **Read the project.** `context/stack.md` for what this repo is; `context/verify.md` for how it proves itself. Search the codebase and find the patterns that already exist. 3. **Find the documentation.** `context/stack.md`'s Documentation section says where this project explains @@ -59,7 +70,10 @@ testable acceptance criteria, and the review checklist items (naming the specifi **Edge cases** — the cross-cutting ones not tied to a single step. -**Open questions** — anything you could not resolve. Mark them clearly. +**Open questions** — anything you could not resolve. Mark them clearly, and **say which ones need a +person** rather than more research: a reading, a judgement by eye, a pass on a device nothing here drives. +Those are the ones `/feature-close` has to find and give an end to, and it finds them by reading what you +wrote. ## Rules @@ -73,5 +87,12 @@ testable acceptance criteria, and the review checklist items (naming the specifi Verification says how that is checked — the keyboard pass, the contrast check, an end-to-end pass in a real browser. Name no driver for it: `context/verify.md` is the only file in this project that names a command, and if it names nothing that drives a browser that is an open question, not an invention. +- **Name what decides every value the feature computes.** §8 Verification carries them, one row each: the + requirement, the specification, the published reference or the outside source that says what the value + should be. The coder decides its expected values from those rows and never from the code it is writing — + *An expected value has a source, and it is never the code under test* in `context/workflow.md` — and + finding the source is research, which is yours. A value whose source you could not find is an **open + question**, not a number for the implementation to pick; a feature that computes nothing of its own says + that instead of leaving the table empty. - **Name no verification command.** `context/verify.md` is the only file in this project that does. Point at it. diff --git a/.claude/agents/reviewer.agent.md b/.claude/agents/reviewer.agent.md index fa800b2..ea5e3b2 100644 --- a/.claude/agents/reviewer.agent.md +++ b/.claude/agents/reviewer.agent.md @@ -1,6 +1,6 @@ --- name: reviewer -description: Reviews a completed implementation against the plan's review checklist and the project's engineering standards, returning a verdict with severities and cited evidence. Use as the review gate after verification passes. +description: Reviews a completed implementation against the plan's review checklist and the project's engineering standards, returning a verdict and cited evidence, with every item blocking or not. Use as the review gate after verification passes. model: opus effort: high tools: Read, Grep, Glob, Bash @@ -10,8 +10,10 @@ tools: Read, Grep, Glob, Bash You review a completed implementation. You are an **evidence-gatherer first and a judge second**. -You are called after verification has passed. You receive the implementation output, the plan's review -checklist, and the verification result — for context, not for re-running. +You are called after verification has passed. You receive the implementation output, **one phase of a plan +as its own section** — its review checklist, its `Standards:` line and its `Decisions:` line — and the +verification result, for context rather than for re-running. The section is the brief: the plan around it +holds other phases and the problem statement, and your scope is what this phase landed. Your verdict gates completion: @@ -26,6 +28,9 @@ Your verdict gates completion: source of truth. Otherwise `Read` it. Do this **before** writing a summary, before forming a verdict. - For every "missing X" claim you are considering: run `grep -n "" `. The empty output is your evidence. Cite the search you ran. +- For every decision on the phase's `Decisions:` line: that decision is settled, and your question is + whether the implementation honoured it — not whether it was right. An implementation that took the + rejected alternative is a blocking finding; a decision you disagree with is a non-blocking note. - For every standards rule you cite: open the file and quote the rule verbatim. Load the files the phase's `Standards:` line names — the brief carries it, and it is the plan's one traversal of the conditional table, made with the research in hand. Where the brief has no such line, load per the table in @@ -43,6 +48,25 @@ writing "line 42 says X", verify that line 42 says X. If you cannot, write For every item in the plan's review checklist, write a verdict, using only the evidence from step 1. +**Two checks are standing: they run on every review, whether or not the checklist names them.** Both come +from *An expected value has a source, and it is never the code under test* in `context/workflow.md`, and +both are invisible to the gate that ran before you. + +- **An expected value with no traceable source is a finding, even though the tests are green.** For each + assertion this change added or changed, ask what decided the value: the requirement, the plan's §8 + Verification row, a specification, a reference independent of this codebase. Where the only available + answer is *what the code returns*, say so — that assertion pins the implementation to itself and will + pass through any change the implementation makes. **You are called after verification passed, and that + proves only that the tests and the code agree with each other.** Whether either agrees with the + requirement is your question, and a passing assertion is not evidence for it. +- **A rule the code implements that no test asserts is a finding too.** Where the change honours a + requirement or a §3 decision and nothing in the suite would fail if it stopped honouring it, the next + change is free to break it silently. Name the rule, and name the seam where it could be pinned. + +Judge both by step 3 like anything else — blocking only where all three of its conditions hold. Where the +change is documentation, configuration or wiring that only passes a value through, there is no seam and +neither check has anything to find; say that rather than reporting nothing. + Then check whatever `context/release.md` requires of the paths this change touched. **Read that file's granularity answer before judging:** where it says a note is written once per feature, a phase owes nothing and a missing note is not a finding. Where it says per phase, a path whose row deserves a note and diff --git a/.claude/skills/feature-close/SKILL.md b/.claude/skills/feature-close/SKILL.md index 6d790d0..2f3cfc3 100644 --- a/.claude/skills/feature-close/SKILL.md +++ b/.claude/skills/feature-close/SKILL.md @@ -48,12 +48,56 @@ owned. There is nothing left for a second refusal to catch. A refusal here is the workflow working, not a problem to route around. If the user overrides after being told, say plainly what is being overridden, then proceed. +### Go through the plan's open questions — before anything is written + +Read the plan's **§9 Open questions**, and the **Note** column of its ledger. `/feature-implement` surfaced +§9 at activation and nothing has read it since, and **this is the last command that ever will** — after the +`git mv` it is in `context/archive/`, where no command looks. + +**What is still open here is an owner check** — an item no gate can close because it needs a person: a +reading in a language nobody on the branch speaks, a screen-reader pass, a judgement about how something +looks, a check on a device this project does not drive. *What happens to a question the plan could not +settle* in [`context/workflow.md`](../../../context/workflow.md) is the standing rule; the ends are here. + +**Every item that still needs a person gets one of three:** + +- **answered** → write the answer into the item, in the plan, in place. A phase that settled one has + already done this; the user settling one here is the same end. +- **filed** → one issue, wherever this project already files bugs, carrying this project's own labels, + saying what has to be looked at and who it waits on. **Never the backlog label**, and not written up as a + defect — it is a check nobody has run, not a thing known to be wrong. **It is filed as what it is, and + not as a bug**: a pending screen-reader pass recorded as a defect is a defect report nobody can + reproduce. **The person who does the check is what closes it**, and the workflow neither reads it nor + ranks it. +- **waived** → the user says in this turn that it does not matter. Mark the item waived and move on. + +**The user picks, item by item, and silence is not a waiver.** Use your runtime's question mechanism if it +has one. **Where this project files bugs nowhere, there are two ends rather than three** — say so, and the +item is answered or waived. Do not invent a store for it, and do not hold up the retirement over it. + +**This runs before the writes below** because its result is what the `history.md` row's Why and the archived +document both carry. Nothing afterwards would have anything to amend. + +**This adds no status and no fourth phase state.** An owner check is not a phase's and does not gate one: +a phase whose scope landed and whose gates passed is `done` whether or not a person has since looked at +what it produced, and the feature's Outcome stays `shipped` for the same reason. + +**Most of §9 is not an owner check.** The filter is one bit — *does this need a person* — applied to a list +the plan already wrote, not a disposition assigned to every line. **This is not the `notes.md` triage +refused further down**, and that refusal stands: that file is a gate's and grows without bound, while §9 is +the plan's and is bounded by it. + +**Then say in the report where each one went**, including when the answer is *none*. An empty report reads +as "nobody looked", exactly as it does for documentation and for the release note. + ### Then, as one reviewed change 1. **Move the entry into history.** Remove it from `context/roadmap.md` entirely, and append one row to `context/history.md`: date, name, outcome `shipped`, a one-line why, and a link into `context/archive/`. One line. That file **indexes** depth, it does not duplicate it — the reasoning stays in the archived - plan. + plan. **Where an owner check was filed, the why names it and the issue that carries it**, and the + outcome stays `shipped` and gains no value — *The history row says what is waiting, in the Why* in + [`context/workflow.md`](../../../context/workflow.md) says why it could not be anything else. 2. **`git mv` the plan** from `context/plans/` to `context/archive/`. Use `git mv`, not `mv` — the file's history is the record of how the feature was actually built. 3. **Rewrite the document's header to point at the history row.** In the moved document, replace @@ -66,6 +110,10 @@ told, say plainly what is being overridden, then proceed. Add a note if its §-references are cited from source comments. **Do not stamp the outcome and date into the document.** `history.md` owns them, and a copy in the header is a second place to maintain. Replacing the old header is what stops a stale status surviving the move. + + **§9 travels with it, carrying the ends the step above gave it** — each item answered, marked waived, or + naming the issue it was filed as. That is what makes the archived plan the record for the two ends that + have no other one. 4. **Sweep every reference to its old path**, and **show the sweep for review before committing.** ### The sweep @@ -146,10 +194,19 @@ retirement as one reviewable change and hand it over. **Without it, this command writes the note and stops.** That is the default and it is the right one. [`context/workflow.md`](../../../context/workflow.md) forbids running what bumps, tags, publishes or -deploys, with one exception — *when the user asks for it in that turn* — and **this flag is what that -asking looks like.** So it is never inferred: not from "and ship it" earlier in the session, not from a -plan, not from the notes looking ready, and not from a release being obviously due. A sentence is not a -flag. +deploys, with one exception — *when the user asks for it in that turn* — and **this flag is one of the two +shapes that asking takes.** So it is never inferred: not from "and ship it" earlier in the session, not +from a plan, not from the notes looking ready, and not from a release being obviously due. A sentence is +not a flag. + +**The other shape is `/release`, and it is the one to name when the user has not typed this flag.** This +flag **fuses** the release into the feature's own merge: one branch, one pull request, and a diff where the +version move sits among the feature's code. `/release` separates them — the note waits on the default +branch with everybody else's, and the release is cut later on a branch of its own, which is where a reader +can see that the version move is the whole of the change. Neither is the better answer in general: this one +suits a repository that releases about as often as it merges, and the separated one suits a repository +where notes accumulate. **If the user asked for a release and typed no flag, name `/release` rather than +assuming this one.** Read [`context/release.md`](../../../context/release.md) and run **the script its Bump wire names**. That file says what it is; naming a tool here would be wrong in half the repositories this command runs in. @@ -216,6 +273,12 @@ ended by then. For an entry that will not be built. **There is no ledger check in this mode** — unfinished phases are expected. +**The open-questions pass still runs, and it still runs first**, by the rule the release note already +follows at the end of this mode: an owner check belongs to the code that landed, not to the outcome the +feature was later given. So an item about a phase that landed is an owner check like any other, and an item +about work that was never written is **waived by construction** — say that in one line rather than walking +it. A feature with no landed phase has nothing here at all. + 1. Append a `context/history.md` row with outcome `dropped` (or `superseded by `) and **the reason the user gave**, verbatim in substance, not softened. That row is what stops the idea being re-proposed, so a vague reason makes it worthless. @@ -225,6 +288,7 @@ expected. stamped outcome, same rule as Mode 1), and sweep. 4. **Delete `context/notes.md`** if it exists, per *Delete `context/notes.md`* above. A feature that will not be built has nothing its notes should outlive. + **`--release` is refused in this mode**, and not as a technicality: there is no note, so there is nothing this retirement would be releasing. If a release is due anyway, that is its own act and not this one's. diff --git a/.claude/skills/feature-close/tracker.md b/.claude/skills/feature-close/tracker.md index 34f5e58..4bc02a3 100644 --- a/.claude/skills/feature-close/tracker.md +++ b/.claude/skills/feature-close/tracker.md @@ -17,6 +17,9 @@ phase finished — and so is everything about pushing. | `git mv` the plan to `archive/` | nothing moves; the issue keeps its body and its whole thread | | rewrite the document header | nothing — a closed issue does not claim to be open | | the reference sweep | **nothing to sweep.** No path changed, so no link broke | +| the plan's §9, read out of the document | **unchanged** — §9 is a section of the body, and the body is the plan | +| answered or waived, written into the item | a body edit like any other | +| the why names the filed issue | the closing comment names it, by number — both issues are in one tracker, so it is a reference rather than a path | | delete `context/notes.md` | **unchanged** — it is a file in a working tree under either answer, and it is deleted the same way | **The archive is the closed issue**, and it is more than the file it replaces: the plan, the discussion @@ -42,6 +45,11 @@ This is the only place `git.md`'s answer changes what this command does, and it [`context/tracking.md`](../../../context/tracking.md) recommends pairing this substrate with the push answer rather than requiring it. +**A filed owner check is an issue here like anywhere else, and it stays outside the one bit.** No backlog +label, no planned label, and **no `blocked by` relationship** — that primitive is between features, and an +owner check is not one. It carries this project's own labels and whatever assignee the person who will do +the check has, and nothing in the workflow reads it again. + **The release note is unchanged.** It is an artifact of the change rather than workflow state, so it is a file in this repository under both of [`context/tracking.md`](../../../context/tracking.md)'s answers — written, confirmed and committed exactly as above, and carried by the same pull request that closes the diff --git a/.claude/skills/feature-implement/SKILL.md b/.claude/skills/feature-implement/SKILL.md index 6483c88..100be0f 100644 --- a/.claude/skills/feature-implement/SKILL.md +++ b/.claude/skills/feature-implement/SKILL.md @@ -9,8 +9,10 @@ effort: medium # /feature-implement Owns the transition from *has a plan* to *being worked*, **and** the phases within it. One invocation runs -**one phase**: pick it, do it, gate it, close out its ledger row. `--all` (step 14) runs that same loop -back to back instead of stopping after the first. +**one round**: pick a phase, do it, gate it, close out its ledger row — and where phases of this plan are +independent of each other, up to the cap in [`context/executors.md`](../../../context/executors.md) of them +side by side, per [`parallel.md`](parallel.md). `--all` (step 14) runs that same loop back to back instead +of stopping after the first. Read [`context/workflow.md`](../../../context/workflow.md) for the tier model, the one-active-feature rule and the gate contract. This skill cites those rather than restating them. @@ -79,12 +81,25 @@ entries are all `done`.** If it is already `in progress`, **read its Note and resume from there — do not restart it.** +**Then see what can run beside it.** Where this phase and another runnable one are both `not started` — +every `Depends on` `done` — and share **no path** on their **Files:** lines, prose included, the two can +run at the same time, each with its own coder, up to the cap in +[`context/executors.md`](../../../context/executors.md). Where that is true, read +[`parallel.md`](parallel.md) beside this file before step 6: it holds how the round is formed, +every line that makes it fall back to one phase at a time, and how the work comes back. **No flag asks for +this and none turns it on** — it is this command's own decision, each round, and a round of one phase is +the ordinary run the steps below describe. + ## 4. Read the row you are about to work If the phase you picked reads `blocked`, its Note says what stopped it. **That is the work** — clear it before starting anything new, or say plainly why it cannot be cleared and stop. A `blocked` row is the only durable record a previous run left, so it is read rather than overwritten. +**If the Note names a tree, this phase's work is already in it** — a round left it there. Read +[`parallel.md`](parallel.md) and land that tree before briefing anybody; a coder re-run over finished work +throws it away. + ## 5. Stop on disagreement If the ledger's claim contradicts the repo — a phase marked `done` whose **Files:** do not exist, or work @@ -100,8 +115,9 @@ cancelled — leaves a working tree with half a phase in it. A row still reading run into step 5's disagreement stop, or into redoing work that is already there. One token and one Note, in the row that is already there — do not move the entry, restructure the table, or -write a summary anywhere else. If the row is already `in progress` because you are resuming it, leave it -alone; step 11 rewrites the Note. +write a summary anywhere else. **One line**, by the same rule step 11 applies: what is underway, so a +session arriving after an interruption knows where it landed. If the row is already `in progress` because +you are resuming it, leave it alone; step 11 rewrites the Note. **This write is not a change of its own.** Leave it in the working tree — it lands with the phase's work under either answer in [`context/git.md`](../../../context/git.md). Never commit it on its own. @@ -116,6 +132,15 @@ paths are on the same **Files:** line as the code. Per the standing rule in phase. If the work turned out differently from the plan and made something *else* untrue — a README the plan never listed — fix that too and say so; the sweep happened before the code existed. +**An open question this phase settles is answered in the plan's §9, by this phase** — in place, in the +item it settles, riding the same change as the ledger row. Same rule as the documentation rows above and +the same reason: the answer is cheapest to write while the work that produced it is still in front of you, +and `/feature-close` goes through that section before the plan is archived and has to give whatever is left +an end. **Leave the items this phase did not settle alone**, and do not invent an answer to close one — an +honest open question is what that section is for, and the one no phase could finish is exactly what the +retirement is there to catch. Settling one is not part of this phase's scope and step 11 does not test for +it. + **Read [`context/release.md`](../../../context/release.md) and find out whether this phase owes a release note.** That file answers per path and at one granularity, and only one of its two granularity answers reaches this step: @@ -162,11 +187,17 @@ the first answer and implements in-host; that is a fallback, not a failure, and coder produced. A coder that reports its own success has reported nothing — that is what step 11 means by refusing `done` on a self-report. -The brief **cites paths, it does not paste files.** Point at the phase's own section in the plan — its -`Files:`, and its `Standards:` line, which is the plan's one traversal of `context/standards/README.md` and -what the coder loads instead of the table — and at `context/stack.md`. Anything that can read this -repository can open them, and a brief that inlines them is a brief that goes stale. A plan written before -phases carried a `Standards:` line has none; then say to load per that README's conditional table. +The brief **cites paths, it does not paste files, and it names the phase rather than the plan.** Point at +the phase's own §6.2 section — its `Files:`, its `Standards:` line, which is the plan's one traversal of +`context/standards/README.md` and what the coder loads instead of the table, and its `Decisions:` line, +which is the plan's one traversal of §3 — and at `context/stack.md`. Anything that can read this repository +can open them, and a brief that inlines them is a brief that goes stale. + +**A whole-plan read is the fallback, not the route.** Plans in the field run tens of thousands of +characters and three readers open one on every phase; the §6.2 section plus the two lines is what a coder +needs, and a brief that says *read the plan* spends the rest on §1, §5 and the phases that are already +`done`. A plan written before phases carried these lines has neither: then say to load per that README's +conditional table, and to read §3 for the decisions that bind this phase. Describe **what** needs to happen, never **how** to code it. Scope each delegated task to specific files. @@ -188,7 +219,9 @@ Dispatch per [`context/executors.md`](../../../context/executors.md), which hold The last is the fallback and the weakest — the session that wrote the code judging whether the code is good — so **say which one you ran**, every time. The first is the cheapest real independence available: a reader that never saw the implementation being written, only what it produced. Brief it with the phase's -section — its `Files:`, its review expectations and its `Standards:` line — and the diff. Where the runtime +§6.2 section — its `Files:`, its review expectations, its `Standards:` line and its `Decisions:` line — +and the diff. **The section, not the plan**, for the reason step 7 gives; a reviewer judging whether the +change honoured a decision needs the decisions the phase cites, not all of §3. Where the runtime has no such mechanism, fall back to reviewing the diff yourself against the plan's review expectations and the standards, and say that is what happened. @@ -245,6 +278,12 @@ The row is part of the same change as the work — never a separate step afterwa - **Some landed** → stays `in progress`, Note rewritten to name exactly what remains. - **A gate hit its cap, or something external blocks it** → `blocked`, with the blocker in the Note. +**The Note is one line**, per §6.1 of the plan itself: what a later session needs in order to understand +or resume the Status beside it, and nothing else. **Gate 1's output, the test counts and the measurements +do not go in it.** They go in the commit message that carries the row, under +[`context/git.md`](../../../context/git.md)'s answer, or in step 13's report. The ledger is read in a hurry +by a session that has just arrived, and a Note that has become a phase log is one it cannot skim. + **Never mark `done` on a coder's self-report** — the gate output is the evidence. A blocking item that was not fixed means the gate did not pass, and a gate that did not pass means the phase is not `done` — there is no second check to run, because `done` already says both gates passed. @@ -286,6 +325,9 @@ side-effect of the last phase finishing. Without the flag, this invocation is over at step 13. With it, **go back to step 3 and run the next phase**, and keep going until the stop list below says otherwise. +**The unit is step 3's round.** This flag decides whether there is a next one, never how wide one is — +that is [`parallel.md`](parallel.md)'s, and it is decided again each time step 3 runs. + Phase to phase is not a tier boundary — this command already owns "activation, and the phases within a plan" — so the flag crosses nothing. What it removes is the pause where a user reads a phase's report before the next one builds on it, and everything below exists to replace that pause with something diff --git a/.claude/skills/feature-implement/parallel.md b/.claude/skills/feature-implement/parallel.md new file mode 100644 index 0000000..b282203 --- /dev/null +++ b/.claude/skills/feature-implement/parallel.md @@ -0,0 +1,238 @@ +# /feature-implement — a round of phases, side by side + +Read this where step 3 formed a round of **more than one phase**, and where step 4 found a row whose Note +names a tree a previous round left. Every step in [`SKILL.md`](SKILL.md) holds; this says what a round +adds. A round of one phase with nothing left behind is an ordinary run and reads none of this. + +**There is no flag.** How wide a round is is this command's own decision, made fresh each round from the +ledger, the phases' **Files:** lines and the cap in +[`context/executors.md`](../../../context/executors.md). `--all` is unchanged: it decides whether there is +a *next* round, never how wide one is. + +## What forms a round + +Start from the phase step 3 picked — the lowest-numbered phase that is not `done` and whose `Depends on` +are all `done`. Then walk the remaining runnable phases in number order and add each one that **shares no +path with any phase already in the round**, up to the cap. + +- **Runnable** is step 3's test and nothing weaker: not `done`, every `Depends on` `done`. +- **A round holds `not started` phases only.** A phase reading `in progress` or `blocked` has half-work and + a Note of its own in the feature's tree — step 3 resumes it and step 4 reads its row — and that state + cannot be branched into a tree beside it. **A resumed phase runs alone**, exactly as it does today. +- **Shares no path** is read off the phases' **Files:** lines, and **a path that contains another is an + overlap**: a directory or a glob on one line and a file under it on another are the same place, however + differently the two phases spelled it. **The plan's own path is the one exclusion** — only this command + writes the ledger, so two phases naming it are not two writers. **Prose counts** otherwise — a `.md` + file, a README, anything under `context/`. Two phases that each add a section to one file both append, in + two trees, from one base: that is a conflict every time, and in the files that answer what this project's + commands are. +- **The cap** is the one in *Coder* in [`context/executors.md`](../../../context/executors.md) — **3 where + that file says nothing, and 1 turns this off.** It counts coders, which is all there is to count: the + gates below are serial. + +**Say the round before any coder starts.** Which phases are in it, and for every runnable phase left out, +which test left it out — an overlap, **naming the path**, or the cap. + +**A `Files:` line is a prediction.** Phases in the field grow one while they run, which is why the same +test runs again, against the real diffs, before anything lands. + +## What makes it fall back to one phase at a time + +**Nothing here refuses.** Each line below means this run does what it has always done — one phase, in the +feature's own working tree, through steps 6 to 13 — and **says so once, before the first phase**, naming +the file and the answer that decided it. + +| The line | Why a round cannot run under it | +|---|---| +| [`git.md`](../../../context/git.md) *Who commits* — **the user commits** | every mechanism below is a commit, and a tree carrying three phases cannot be cut back into three. Step 14's argument, one level down | +| [`executors.md`](../../../context/executors.md) *Coder* — not configured, or in-host | one coder, by construction. That is what ships, so a fresh install runs one at a time | +| the host cannot run two coders at once, whatever that answer says | **say which of the two it was**, the recorded answer or the runtime; they are fixed in different places | +| the host will not let a coder write in the tree this command can make, or confines it to one of the host's own | a coder that has to be asked about every file it writes is not running unattended, and one sealed into a tree the host chose cannot be pointed at this one. **Name the limit**, take the runtime's tree where it passes the three tests below, and otherwise run one at a time | +| [`verify.md`](../../../context/verify.md) has no **Prepare a fresh tree** answer | a fresh checkout has no installed dependencies, so a coder in one cannot write a failing assertion and run it. Name the file and `/onboard` | +| `git check-ignore -q .phase-trees/` exits non-zero, run at the top of the working tree | the trees would be in Gate 1's way — a linter would lint the other phases' half-written work and a test runner would run their copies of the suite. **Keep the trailing slash**, or the test answers for a file that is not there. Name `/onboard`, which writes the entry and proves it against this project's commands | +| the cap is **1** | the project said one at a time. Not a gap — an answer | +| `git worktree add` fails, for any reason | report the error and run the phase. A tree that cannot be made is not a phase that cannot be run | + +## The scratch tree + +One per phase in the round, made by this command with plain git, at the feature branch's HEAD, **detached**, +in one fixed directory inside the working tree this run is standing in: + +```bash +git worktree add --detach ".phase-trees/-phase-" HEAD +``` + +- **No branch is created**, and **the invocation [`executors.md`](../../../context/executors.md) names is + not used.** That one makes a *workspace* — env files, a naming convention, a directory somebody opens, + something it is handed to. This is a checkout nobody looks at, and the two are not interchangeable. +- **Inside this working tree**, because a host that scopes a coder's writes to the session's own working + directory can write here and the repository's admin directory is not that: under + [`git.md`](../../../context/git.md)'s worktree answer it is not even inside this directory, and a host + may hold it to a stricter rule than the files beside it. +- **And ignored, which is what keeps Gate 1 honest.** The leading dot is not enough — a linter or a test + runner that walks the tree itself finds these checkouts, lints the other phases' half-written work and + runs their copies of the suite, and some will *write* to them. What stops it is the entry in this + project's `.gitignore`, which is where most tools take their ignore list from, and `/onboard` **proves** + that against this project's own Gate 1 commands, because honouring that file is a per-tool setting. The + entry is also what keeps a tree out of `git status` and out of `git add -A`. +- **The path is fixed** — it is the only thing identifying these trees as this command's — and is + **recorded absolute**, `$(git rev-parse --show-toplevel)` and the path above, because a Note is read from + wherever the next session is standing. Each feature's trees sit in that feature's own tree, so the sweep + below cannot reach another's. +- **Record the sha `HEAD` resolved to.** It is this tree's base, `..HEAD` is the phase's whole work, + and the feature branch moves as the round lands — so the base cannot be read back afterwards. +- **Make it runnable.** Run what *Prepare a fresh tree* in + [`context/verify.md`](../../../context/verify.md) says, in the tree, before the coder starts. That is + paid per phase per round — the one cost that grows with the width being bought, so it is named in the + report rather than hidden. **If it fails in a tree, the round does not run** — a command there may bind a + port the tree beside it holds. No coder has started yet: remove the trees, name the command and its + output, and run one at a time. +- **Then the tree has to be clean: `git -C status --porcelain` prints nothing.** A prepare step that + rewrites a lockfile or writes a generated file nobody ignores has dirtied the tree **before the phase + started**, and from then on the phase's own state is unreadable: landing reads a clean phase as a coder + that stopped short, and the overlap check sees a path every phase in the round touched. Same outcome as a + failure — remove the trees, **name the files it changed**, and run one at a time. An installed dependency + directory is not this: `--porcelain` does not list ignored files. + +**Where the runtime can give a coder its own temporary tree of this repository — one whose commits are +objects in this repository — take that and make nothing.** A copy or a fresh clone is not that: its commits +cannot be cherry-picked from here, so it falls back like any line above. **Three things have to hold**, and +the first is a command rather than a claim: `git -C rev-parse HEAD` is **the sha this round +recorded**, since a runtime that cuts its trees from the default branch has handed the coder a tree with +none of this feature's work in it — a different base, not an isolated one; **something removes it when the +phase lands**, the runtime or this command by the path it recorded; and it is **outside this working tree +or dotted inside it**, for the Gate 1 reason above. Any of the three failing is a fallback like the rest. + +**It is not this command's tree, so it is reported rather than swept.** It has a branch: the detached test +below does not cover it, removing it leaves the branch behind because **nothing here deletes a ref**, and +`/feature-status` reads a branch on a worktree as a feature in flight for as long as the tree exists. +**Name its path, its branch and its tip.** + +**The brief is step 7's, plus four sentences and no more:** work in this directory; the plan here is a +stale copy and is not to be edited; commit your work in this tree when you are done, because that commit is +what the caller takes; **other coders are running beside you and this tree is a fresh checkout, so a test +that needs a server, a fixed port, or a secret the tree does not hold — nothing kept out of git came across +into it — is Gate 1's: leave it, and say in your output which assertions you did not run.** +[`roles/coder.md`](../../../context/roles/coder.md) is unchanged — one prompt for +every way a coder is dispatched, and committing is a property of this dispatch rather than of the role. + +## The gates, and the ledger + +**Both gates run in the feature's own tree, one phase at a time, in phase order** — never in a scratch +tree. Phase *k* is gated on a tree holding this round's phases up to *k*, and the tree holding up to *k*−1 +has just passed, so a failure at *k* is *k*'s, exactly as in a sequential run. The loopback cap is +unchanged: two loops per gate, per phase. Nothing runs the verification stack in two trees at once. + +**Only this command writes the ledger, and only in the feature's own tree.** Coders never touch the plan, +even where a phase's **Files:** line names it. + +- **The round's rows open in one write**, before any tree is made: one `in progress` and one Note each, + naming the round and the absolute path of the tree the phase will be worked in. Step 6's rule, for every + phase in it. +- **A scratch tree's copy of the plan is stale, and that is harmless.** It is branched before the opening + write is committed, and nothing in the tree reads it. +- **A closing row names the tip its work was at**, beside the path — the path is what a later sweep + destroys, and the tip is what survives one. `git merge-base ` recovers the base if the + recorded one is lost. +- **An open question this phase settles is still answered in the plan's §9**, per step 7 — but not by the + coder, which never touches the plan. **The coder reports what its phase settled and this command writes + the answer**, in the commit that carries the row. + +## Landing — the whole tree, as a range, in phase order + +Per phase, in phase order: + +1. **Check the coder finished.** `git -C status --porcelain` is empty, or **nothing from that phase + lands** — it is a coder that stopped short, so its row stays `in progress` naming what remains, per step + 11, and its tree is kept. Uncommitted work in a scratch tree is not a candidate to land. +2. **Check the real diff.** `git diff --name-only ..` — the base recorded when the tree was + made, the tip its HEAD — read from the feature's own tree with no checkout, against the paths this round + has already landed. A shared path is the next section. +3. `git cherry-pick -n ..` — **the whole tree, however many commits are in it**, staged and not + committed, so the row can ride the work. +4. **Gate 1**, then **Gate 2** on the staged diff, per steps 8 and 9. +5. **Close the row**, per step 11. +6. **One commit**, carrying the phase's change and its row — [`git.md`](../../../context/git.md)'s *one + commit per phase*, unchanged. +7. **A phase that closed `blocked` ends the landing here**, with every later tree kept — *Mixed results*. + +**A kept tree is landed by the run that resumes its phase** — these same steps, from the base and tip its +Note recorded, before any coder is briefed. Re-running a coder would throw away a phase's finished work to +arrive back where it already is. + +**The range, not the tip.** How many commits a coder makes in its own tree is its own business, and +`..` is the phase either way. Landing the tip alone would drop work silently. + +**Cherry-pick reads the scratch tree without touching it**, which is what lets a tree be kept exactly as +its coder left it. A merge would add a commit per phase that no granularity answer names; a rebase would +rewrite the only durable record of what Gate 2 reviewed. + +**A gate failure loops back in the feature's own tree** — never in the scratch tree, and **nothing is +reset.** The cherry-pick is already staged here, which is the state step 10 re-briefs a coder in on any +ordinary phase, so re-brief it here against this tree. The scratch tree is missing the phases that landed +earlier in the round, so a failure the combination caused — a shared budget two phases each fit under +alone — cannot be seen from there, let alone fixed there. + +## When two phases touched the same path + +The round was formed on a prediction; the diffs are the fact. Where a later phase's diff shares a path with +one this round has already landed: + +- **The earlier phase stands.** It is already a commit. +- **The later phase's work is thrown away.** Nothing is cherry-picked from it, and the two versions are + never merged — nothing here has the standing to decide which of two coders' versions of a file wins. + Name the discarded tip in the report; its range is readable until git collects it. +- **Its Note stops naming its tree.** A Note that names one means *land that tree* — step 4 — and this is + the one tree whose work must not be landed. It is kept, and it is named in the report instead. +- **Run that phase again, one at a time, on the combined tree**, from step 7, with its row still + `in progress` and its Note saying so. It can happen at most once per phase per round: the re-run is + alone, so there is nothing left for it to overlap with. +- **Say it in the report** — the path, the phase that kept it, and the phase that ran again. + +**The check is before the cherry-pick, not after.** A range that conflicts stops part-applied with the +sequencer open, and getting out of it means choosing between two coders' versions of a file — the one +decision nothing here has the standing to make. Checking the diffs one command earlier means that state +never exists. + +## Mixed results + +- **A phase that ends `blocked` ends this round's landing.** What landed before it stands — those are + commits, and withholding them buys nothing. **Every later tree is kept**, with its path and tip in the + report, and the next run lands it: those rows still read `in progress` from the opening write, so step 3 + resumes each one alone. Landing is in phase order, each phase on top of the last, so carrying on past a + blocked phase would cherry-pick *k*+1 onto *k*'s half-work and gate it there — the failures from then on + belong to a phase that did not cause them, and the loopback would brief a coder to fix somebody else's. +- **A blocked phase's Note names the tree its work is in, and the tip it is at.** `git worktree list` shows + the tree and nothing else says which phase it belongs to. One line, by the same rule as every other Note. +- **The next round does not start**, and **each phase is reported as it lands** — step 14's stop list and + its per-phase report, neither of them widened by a round. + +## The trees this command removes, and the ones it never touches + +[`context/workflow.md`](../../../context/workflow.md) says nothing removes a worktree. **This is the one +exception, and it is this narrow:** + +- **Only a tree this command made, at the path above, detached and with no branch** — and **never with + `--force`.** `git worktree list --porcelain` prints `branch` for every worktree the recorded invocation + has ever made and `detached` for these. A tree with a branch on it is never touched. +- **A tree goes when its phase's row reads `done`** — `git worktree remove `, never `--force` — and + is **otherwise kept, with its path and its tip in the report.** One rule, covering a blocked phase, a + discarded one and a coder that stopped short alike. A record whose directory has already gone is removed + the same way; nothing here prunes. +- **Without `--force`, removal cannot destroy work a coder failed to commit.** Git refuses a tree holding + modified or untracked files and succeeds where the only leftovers are ignored — which is what an + installed dependency directory is. A refusal is reported, not forced. +- **A later run sweeps that directory first, and removes only a tree whose row reads `done`.** **Read the + ledger before the sweep.** A clean tree is not an abandoned one: a coder that did its job committed, so + removal succeeds without `--force` and the commit it leaves behind has no branch and nothing else + pointing at it. That is exactly the tree an `in progress` or `blocked` row is keeping for the next run, + and the Note would be left naming a path that no longer exists. **A tree whose path names another plan is + left alone** and named in the report — this run has not read that plan's ledger and is not entitled to. + +## The report + +Step 13's report, plus three things: + +- **Which phases ran side by side**, and which ran alone. +- **Every fallback, with the file and the answer that caused it** — once, not per phase. +- **Every tree still on disk, by path**, and the sha of anything thrown away. diff --git a/.claude/skills/feature-implement/tracker.md b/.claude/skills/feature-implement/tracker.md index 513ee37..a41f853 100644 --- a/.claude/skills/feature-implement/tracker.md +++ b/.claude/skills/feature-implement/tracker.md @@ -52,9 +52,11 @@ be editing at the same time — refining the plan while you flip a status — an loses their edit with no trace. This hazard does not exist under the working-tree answer, where the plan sits in a tree only you are working in. -**The body has a size limit, and the row outranks everything else in it.** -[`context/tracking.md`](../../../context/tracking.md) says what it is. `/feature-plan` leaves room for the -rows this command writes, so reaching the limit here means the body grew after the plan landed. If a write +**The body has a size limit, and the row outranks everything else in it.** *How much a plan may hold* in +[`context/tracking.md`](../../../context/tracking.md) says what it is. `/feature-plan` held a reserve back +for the rows this command writes, and step 11's one-line Note is what that reserve was sized against — so +reaching the limit here means either the body grew after the plan landed or a Note stopped being one +line. If a write would overflow, **shorten your own Note until the row fits and say the body is at its limit** — the Status column is the phase's only home and a row that never lands is state that is simply gone. Never drop a row, never delete someone else's text to make space, and never move a row or a note out of the body. @@ -108,6 +110,22 @@ that is never written is worse than one whose evidence is still owed, and this i **Never edit a row to get past a refusal**, exactly as no phase is marked `done` to get past one. +### A round of phases, in steps 6 and 11 + +[`parallel.md`](parallel.md) holds the round itself. Three things change under this answer: + +- **The round's rows open in one body write.** Fewer windows against a person editing the body, which is + this answer's one hazard the tree answer lacks — and the opening write is the one that can be batched, + because no phase has landed yet. +- **They close one write per phase, as it lands.** The tree answer's row travels inside the commit; here it + does not, so a run that dies two phases into a round would leave two commits recorded nowhere but git, + and the sha is the row's only evidence. One extra window is worth less than that. +- **The sha in a closing row is the commit on the feature branch**, never the one in the phase's scratch + tree: that one has no branch and stops existing when the tree is removed. A row naming a sha nothing can + resolve is worse than a row naming none. +- **The heartbeat names every phase in the round**, at the boundary it opens and the one it closes. An + assignee is a lock with no expiry, and a run that dies mid-round leaves three open rows instead of one. + **A blocking defect is unchanged too**, because it was never a separate object: it is fixed, or it is the `blocked` status in the row this section already describes, or it is an issue. Under this answer the third of those is an issue in the same tracker as everything else, which is the one place the file answer had to diff --git a/.claude/skills/feature-plan/SKILL.md b/.claude/skills/feature-plan/SKILL.md index 6ebc75e..50e6abf 100644 --- a/.claude/skills/feature-plan/SKILL.md +++ b/.claude/skills/feature-plan/SKILL.md @@ -81,9 +81,10 @@ upper-cased. - **If the entry's **Doc** names a draft in `context/drafts/`**, `git mv` it to that path and build the plan on top of its content. Use `git mv`, not `mv`. Do not create a second file and do not leave the draft behind — a draft and a plan for the same feature is two documents disagreeing about one thing. -- **Otherwise** copy `context/plan-template.md` to that path. Copy it verbatim; it is a bare skeleton with - nothing to strip. [`context/plan-template.notes.md`](../../../context/plan-template.notes.md) says what - goes in each section. +- **Otherwise** copy [`context/plan-template.md`](../../../context/plan-template.md) to that path. Copy + it verbatim; it is a bare skeleton with nothing to strip. + [`context/plan-template.notes.md`](../../../context/plan-template.notes.md) says what goes in each + section. **The draft is the most valuable input you have** — material the user gathered deliberately, often from somewhere you cannot reach. Carry its specifics forward; do not summarise them away, and do not silently @@ -108,6 +109,10 @@ planner's own default shape: - **A `Standards:` line on every phase** — the files from that table the phase will be reviewed against, the always row included and the surface rows below added. The plan is the one traversal of the table: whoever implements the phase and whoever reviews it read that line, and neither opens the README again. +- **A `Decisions:` line on every phase** — the §3 decisions that govern it, by number and in a clause + each. Same principle one section over: whoever implements a phase is briefed with that phase's own + section, so a constraint living only in §3 either rides a whole-plan read or gets re-decided + downstream. A phase genuinely governed by none says `—`. - **The surface question, asked out loud** — per the standing rule in [`context/workflow.md`](../../../context/workflow.md): does this feature put something in front of a person, sit in a hot path, or cross a trust boundary? Each *yes* names the standards rows the phases will @@ -126,7 +131,39 @@ planner's own default shape: **Files:** line naming every path it touches. That line is what makes reconciliation a check rather than a judgement call. -### 5. Write the document +### 5. Measure against the budget, before you write + +[`context/tracking.md`](../../../context/tracking.md)'s *How much a plan may hold* gives a **ceiling** and +a **reserve**. Measure the drafted plan's own size, add the reserve for each phase in it, and compare the +total to the ceiling. + +**Measure the file, not your impression of it.** Nothing can count the characters of a draft it is holding +in context. Write the draft to a path outside `context/` and run `wc -m` on it — the plan is written from +there in step 6, or thrown away — and **say the number in the report.** + +**Do this before the write, not after one fails** — under the tracker answer the body is a single write and +a failed one leaves the research with nowhere to go, and in the tree a plan that is already on disk is a +plan somebody will keep rather than split. + +**If that section is not there** — an install from before it shipped — **name the gap, do not invent a +number.** Say the budget is not recorded, name `/onboard`, and write the plan. Inventing a ceiling +mid-run would write policy nobody chose, and refusing to plan over a missing answer would be worse: the +same rule this workflow applies to a path [`context/release.md`](../../../context/release.md) does not +cover. `check` carries its own fallback and says when it used it, so an oversized plan is still reported in +that install. + +**It is not *does this fit*. It is *does it fit with room for the rows still to be written*.** The ledger +is edited for the feature's whole life: every row opens to `in progress` and then closes with a sha and +its Note. That is what the reserve is, and a plan that only just fits has already failed. + +**Under the budget** → carry on to step 6. Say the measurement in the report either way; a check nobody +can see the result of is one nobody trusts. + +**Over the budget** → the feature is more than one feature, and the answer is to split it. Read +[`split.md`](split.md) beside this file — it holds the workarounds that are refused, how the split is +proposed, and what happens to each chunk. Do not improvise one, and **write no plan until it is settled.** + +### 6. Write the document Fill in the template's shape. Then: @@ -143,10 +180,14 @@ Fill in the template's shape. Then: is `context/verify.md`'s answer; **if that file names nothing that drives a browser, say so in §9 rather than inventing one** — a standard named in a phase's review expectations and nowhere checkable is a rule nobody runs, and `/onboard` is what fills the gap. -- Fill in **§9 Open questions** honestly. An honest gap is worth more than an invented decision. +- Fill in **§9 Open questions** honestly. An honest gap is worth more than an invented decision. **Say + which of them need a person** — a reading, a judgement, a pass on a device + [`context/verify.md`](../../../context/verify.md) names no driver for — because `/feature-close` goes + through this section and each one of those has to be answered, filed or waived before the feature + retires. - Every phase is `not started`. -### 6. Update the roadmap entry +### 7. Update the roadmap entry Repoint the entry's **Doc** field at the new `plans/` path. If you `git mv`d a draft, that same edit is what fixes the now-dead `drafts/` link, so do it together. @@ -159,7 +200,7 @@ state for a planned-but-not-started feature. plan, skip the activation, and name the feature that holds it.** The plan is valuable and harmless on its own; discarding it over a marker would undo the point of the split. -### 7. The plan and its entry travel together +### 8. The plan and its entry travel together They are one change: the entry's **Doc** field points at the document, and `check` reports a dead link if the entry exists where the plan does not. Never leave one behind. @@ -174,7 +215,7 @@ This is the one place the ordering is not obvious: everywhere else in this workf it describes land together, and here the document has to land *first*, in a different tree from the one that will use it. -### 8. Report and stop +### 9. Report and stop State the document path, the phase count, the documentation surfaces §7 commits to updating, and the open questions. Then say plainly that **what you produced @@ -187,6 +228,9 @@ once they are satisfied. - **Never implement anything.** Not "just the first phase", not "a quick scaffold". - **Never mark a phase `done`**, and never mark a phase anything other than `not started`. - **Never write outside `context/`.** No source files, no config. -- **Never shrink a plan to fit where it is kept.** A plan too large for its home says the feature is too - large; splitting it is the answer, and trimming the research to fit hides the finding. +- **Never shrink a plan to fit its budget.** A plan over the line in + [`context/tracking.md`](../../../context/tracking.md) says the feature is too large; splitting it is the + answer, and trimming the research to fit hides the finding. Step 5 is where that is measured, and it is + measured under both tracking answers — a plan document having no hard ceiling is not the same as having + no budget. - Do not fold the draft's content into `context/roadmap.md`. Tier 1 stays high-level. diff --git a/.claude/skills/feature-plan/split.md b/.claude/skills/feature-plan/split.md new file mode 100644 index 0000000..f8481b3 --- /dev/null +++ b/.claude/skills/feature-plan/split.md @@ -0,0 +1,62 @@ +# /feature-plan — a plan over its budget + +Read this from step 5 of [`SKILL.md`](SKILL.md), and only when the measurement there came out over the +budget in [`context/tracking.md`](../../../context/tracking.md). Nothing else needs it, which is why it is +not in the body. + +## Over the budget, the feature is more than one feature + +A plan that reaches the ceiling holds the design, the phases and the risks of **more than one piece of +work** — which is already a defect by *Feature or task?* in +[`context/workflow.md`](../../../context/workflow.md), since a feature is work you would want one +`history.md` row for. Under the working-tree answer nothing else in the repository will ever say so. + +**The only honest response is to split the feature.** The budget is not the thing to argue with: it was +telling you something true about the scope, and every way of satisfying it other than the split costs more +than the split does. + +## Three ways out are refused + +Naming them matters because each looks reasonable at the moment it is reached. + +| Workaround | What it costs | +|---|---| +| **Trimming the plan until it fits** | Discards exactly the research the plan was written to hold. And a trimmed plan reads identically to one that was small enough, so nothing downstream can tell them apart | +| **Continuing the plan into a second document** | The plan stops having a single home, and a reader cannot tell which half is current | +| **Linking out to a gist or a paste** | The same, plus a second object to keep in step | + +## Propose, then ask + +**The phase list is already the seam.** Phases are commit-sized units with real `Depends on` values, so a +cut across a dependency boundary is one the research has already justified. The plan's own structure is the +split proposal; you are not inventing a boundary, you are reading one. + +Offer the split as a numbered list — each chunk with the phases it carries and what it waits on — and +**write nothing until the user answers.** Splitting one feature into three is a scope decision, and this +command asks before it commits to far less than that. + +## Once they agree + +- **The entry being planned keeps the first chunk**, and its plan is written as normal. Do not retire it + and open replacements: it is the entry the user chose, and under the tracker answer it is also a thread + with a reporter and subscribers on it. +- **Every other chunk becomes a new Tier-1 backlog entry** — `pending`, with the one or two lines of why + and a size. **No plan and no ledger**: each gets its own `/feature-plan` run when its turn comes, and a + ledger written now would be a plan nobody wrote. +- **Record the order between the chunks** the way [`context/tracking.md`](../../../context/tracking.md) + says. The cut has just established which chunk waits on which, and that is the strongest dependency this + workflow ever knows about. +- **No parent entry, and no entry whose phases are separate objects.** The ledger lives in the plan; a + hierarchy laid over the chunks puts the phase ordering into a second home. + +Then plan the first chunk only, and **report the split first** — what was cut where, what was opened, and +which chunk this plan covers. That is the most consequential thing the run did. + +## If the user declines + +Say plainly that the plan cannot be written as one feature, and **stop**. Do not write a shortened version +as a compromise: a plan trimmed to fit reads exactly like a plan that was small enough, and nothing +downstream can tell the two apart. + +The research is not wasted. It is in the transcript, and the entry is still there to plan once it has been +cut down to one feature. diff --git a/.claude/skills/feature-plan/tracker.md b/.claude/skills/feature-plan/tracker.md index 2d0ff68..d311663 100644 --- a/.claude/skills/feature-plan/tracker.md +++ b/.claude/skills/feature-plan/tracker.md @@ -19,69 +19,42 @@ research, write, report — and only where the plan lands changes. | `git mv` a draft into `plans/` | nothing moves — **the plan replaces the body of the same issue** | | copy `plan-template.md` to a new path | write the template's sections into that issue's body | | the phase ledger, Status column and all | **unchanged** — the same table, written into the body | -| step 6, repoint **Doc** | nothing — a plan is the observation that the body holds a ledger, plus the planned label, which is a rendering of it and is read by nothing | +| step 7, repoint **Doc** | nothing — a plan is the observation that the body holds a ledger, plus the planned label, which is a rendering of it and is read by nothing | | step 2's *already planned* check | the body already holds a phase ledger | -| nothing — a plan document has no size limit | **the body does**, and a plan that overflows it is split rather than trimmed | +| step 5's budget, which nothing in the tree enforces | **the body enforces it** — the same number, and a write that overruns it simply fails | **The ledger does not change shape.** `#`, `Phase`, `Depends on`, `Status`, `Note` — the table a plan document carries, written into the body with every row `not started`. There is no second object to create, nothing to reconcile it against, and **no window in which a plan is half-written: the body is one write.** -### A plan that will not fit is a plan for too much - -That one write has a ceiling. [`context/tracking.md`](../../../context/tracking.md) says how large a body -may be; **measure the plan against it before writing, not after a write fails.** - -The plan must fit with **room to spare**, because the same body is edited for the feature's whole life: -each phase row moves to `in progress` and then to `done`, gaining a commit sha and a note as it closes. A -plan that only just fits has already failed — the last phase would not land. - -**Overflow is a scope signal, and the only honest response is to split the feature.** A filled plan is a -few thousand characters. One that reaches the ceiling is the design, the phases and the risks of more than -one piece of work written into a single document, and the substrate is the first thing that has said so. - -**Three ways out are refused, and naming them matters because each looks reasonable at the moment it is -reached:** - -- **Trimming the plan until it fits.** That discards exactly the research the plan was written to hold, to - satisfy a limit that was telling you something true. -- **Continuing the plan into comments.** The plan would then have no single home, and a reader could not - tell which half is current — the thing putting the ledger back in the body settled. -- **Linking out to a document or a paste.** Same failure, plus a second place to keep in step. - -#### How to split - -**Propose, then ask.** The phase list is already the seam — phases are commit-sized units with real -`Depends on` values, so a cut across a dependency boundary is a cut the research has already justified. -Offer the split as a numbered list, each chunk with the phases it carries and what it depends on, and -**write nothing until the user answers.** Splitting a feature in two is a scope decision, and this command -asks before it commits far less than that. - -Once they agree: - -- **The issue being planned keeps the first chunk**, and its plan is written into the body as normal. Keep - the id: the thread, the reporter and everyone subscribed are the same reason adoption does not open a - second issue about one thing. -- **Every other chunk becomes a new backlog issue** — carrying the backlog label, `pending`, with the one - or two lines of why, a `Size:` and a `Priority:`. No plan and no ledger: they are Tier-1 entries, and each - gets its own `/feature-plan` run when its turn comes. -- **The order between the chunks is a relationship, not a sentence.** A cut along a dependency boundary has - just established which chunk waits on which, so record it the way - [`context/tracking.md`](../../../context/tracking.md) says — one write per edge, visible from both ends. - This is the strongest dependency the workflow ever knows about, and writing it into a body instead would - leave the backlog's next reader to notice it by eye. -- **Cross-link anything the relationship does not carry**, so the split is visible from any one of them. - Two chunks that share a subject without either waiting on the other get a mention in each body and **no - relationship** — recording one there would hide runnable work behind nothing. -- **No parent issue, and no issue whose phases are separate objects.** The ledger lives in a body; a - hierarchy laid over that is a second home for the same ordering. - -Then plan the first chunk only, and **report the split first** — what was cut where, which issues were -opened, and which one this plan covers. That is the most consequential thing the run did. - -**If the user declines the split**, say plainly that the plan cannot be written into this substrate as one -feature, and stop. Do not write a shortened version as a compromise: a plan trimmed to fit reads exactly -like a plan that was small enough, and nothing downstream can tell the two apart. +### A plan that will not fit — the same check, with a harder consequence + +Step 5 measures the plan against *How much a plan may hold* in +[`context/tracking.md`](../../../context/tracking.md) and sends an over-budget plan to +[`split.md`](split.md). **Both hold here unchanged**, and the number is the same number. What differs is +what the ceiling is and what the chunks are made of. + +**The ceiling is the substrate's, not a budget.** The body holds exactly that many characters and the +write is refused at one more, so overflow is not a report — it is a failed write with the research done +and nowhere to put it. **The body is one write**, so the measurement is the only thing standing between a +finished draft and a lost one. That is also why `/tracking-migrate` refuses an oversized plan rather than +carrying it: the split happens in the tree first. + +**`split.md`'s chunks are issues here.** Each new one carries the backlog label, `pending`, the one or two +lines of why, a `Size:` and a `Priority:`. **The issue being planned keeps its id** — the thread, the +reporter and everyone subscribed are the same reason adoption does not open a second issue about one +thing. + +**The order between chunks is a relationship, not a sentence.** One write per edge, visible from both +ends, as [`context/tracking.md`](../../../context/tracking.md) says. Writing it into a body instead would +leave the backlog's next reader to notice it by eye. + +**Cross-link anything the relationship does not carry**, so the split is visible from any one of them. Two +chunks that share a subject without either waiting on the other get a mention in each body and **no +relationship** — recording one there would hide runnable work behind nothing. + +**The planned label goes on the chunk that was planned and on nothing else.** The others are Tier-1 +entries with no plan. ### What it waits on — read before the ranking, set after the research @@ -160,12 +133,12 @@ it and say so once — it is metadata, not a gate. **`--activate` assigns the issue** rather than editing a marker, subject to the same one-active-feature rule and the same outcome when the slot is held: write the plan, skip the activation, name the holder. -**Step 7's ordering problem disappears, and that is worth knowing.** Under the working-tree answer the plan +**Step 8's ordering problem disappears, and that is worth knowing.** Under the working-tree answer the plan has to be committed and pushed *before* a worktree exists, because a tree carries only what its source ref holds. An issue is in no ref: it is visible from every tree the moment it exists. Plan, then create the tree, in whatever order suits — nothing here has to land first. -**Step 5's `**Status:**` rule applies to the issue body verbatim, and only to the feature.** The body holds +**Step 6's `**Status:**` rule applies to the issue body verbatim, and only to the feature.** The body holds the plan and never a line claiming where the *feature* stands; that fact is the assignee. The ledger's Status column answers a different question at a different scope, and it belongs in the body exactly as it belongs in a plan document. diff --git a/.claude/skills/feature-status/SKILL.md b/.claude/skills/feature-status/SKILL.md index 1517716..fc28e52 100644 --- a/.claude/skills/feature-status/SKILL.md +++ b/.claude/skills/feature-status/SKILL.md @@ -41,6 +41,9 @@ branch merges. So enumerate them. 1. `git worktree list` — every working tree, its path and its branch. **That is the state.** Nothing records which features are in flight; a worktree existing is the claim, and removing it withdraws the claim. Do not write a file to track this, and do not read one if you find it. + **Skip the ones with no branch.** A detached tree under `.phase-trees/` is a phase's scratch checkout, + made by `/feature-implement`, and a tree with no branch is nobody's claim. One left behind belongs to a + phase that did not land — name its path in the report, read nothing out of it, and remove nothing. 2. For each tree other than this one, read `/context/roadmap.md` for its `active` entry and that entry's ledger at `/context/plans/-PLAN.md`. Two numbers per tree — phases `done` out of phases total — and its lowest not-`done` phase. diff --git a/.claude/skills/onboard/SKILL.md b/.claude/skills/onboard/SKILL.md index 576b297..543f735 100644 --- a/.claude/skills/onboard/SKILL.md +++ b/.claude/skills/onboard/SKILL.md @@ -296,6 +296,23 @@ Three outcomes, and only the first writes the answer alone: **Never write the tracker answer and delete the tree files in the same run.** Removal is what makes the migration irreversible, and this command has no way to show a remote write as a diff first. +### How much a plan may hold — leave it alone, and say it is there + +That section of [`context/tracking.md`](../../../context/tracking.md) ships with both its numbers filled +in and **holds under either answer above**, so there is nothing to ask. Two things are still yours: + +- **Say the budget exists and what it is**, in one line, for the same reason this step reports at all. It + is the one answer in this file a user is most likely to want to change and least likely to discover. +- **If the section is absent** — an install from before it shipped, which `update` reports and cannot fix, + since this file is the project's — **write it from + [`context/tracking.notes.md`](../../../context/tracking.notes.md) with the shipped numbers** and say you + did. Until it is there `/feature-plan` names the gap and plans anyway, so this is the thing that closes + it. + +**Do not change the numbers unless the user asks.** The ceiling is the issue body's hard limit under the +tracker answer and the working-tree answer takes the same number on purpose, so raising it here is a +choice to write plans a tracker would not accept. If they ask, say that before editing. + ### Under the tracker answer Write the tracker answer's paragraph from @@ -388,7 +405,9 @@ in [`context/verify.notes.md`](../../../context/verify.notes.md). request, because Gate 1 runs per phase and would flag every docs-only change. 2. **Show the candidates and ask** which belong in Lint, Typecheck, Build and Test, and whether anything is missing. Ask about prerequisites too — a package manager version, an install step, a service that must - be up. + be up. **Whatever has to be *run* to make a clean checkout pass goes in *Prepare a fresh tree***, not + into the preamble: `/feature-implement` runs that section when it prepares a tree for a phase, and + without it runs one phase at a time. 3. **Ask what else this project runs to prove a change is good.** The four headings are the ones every project has, not the whole of what one checks — an end-to-end run, an accessibility suite, a size budget, a coverage floor, a visual snapshot, a dependency audit. **Ask specifically about the end-to-end @@ -405,6 +424,15 @@ in [`context/verify.notes.md`](../../../context/verify.notes.md). when an advisory is published against a lockfile nobody touched, and a phase gate cannot tell that from a defect the phase introduced. Ask before sorting one there — a project that holds the check clean as a standing invariant has decided the opposite, and for that project a red one is this change's problem. + **Then offer one candidate of your own, and only this one: a check that no test in the suite is + skipped, focused or expected to fail.** A runner reports a skipped test as a pass and a focused one + silences every test beside it, so the Test section can exit 0 while the suite asserts almost nothing, + and nothing else you write here would notice. It is cheap and the change is what makes it fail, so it + belongs above *Not run by Gate 1* — inside Test where this project's runner or its lint rules can + assert it, otherwise under a heading of its own. **Offer it; do not assume it.** A project that marks + work in progress with a skip has chosen something this check forbids, and declining is an answer. If it + is taken, the command is whatever this project already has or a short one written for it, and like every + other it is run in step 4 before it is written down. 4. **Run every candidate bound for a gate section.** Actually run it, from the repo root. One sorted under *Not run by Gate 1* is not run here — this command has no Docker, no deploy target and no reason to spend minutes driving a browser — and step 6 says what is recorded in its place. @@ -418,6 +446,36 @@ in [`context/verify.notes.md`](../../../context/verify.notes.md). a check this project has and this gate does not run; without the name it reads as a check nobody runs, which is a different and much worse fact. +7. **Verify *Prepare a fresh tree* in a tree that is actually fresh.** Step 4 does not run it — it is below + the Gate 1 line, and it is an answer about a checkout nobody has lived in, which this one is not. Clone + the repository into a temporary directory instead: `git clone --no-hardlinks . `. **That is not a + worktree**, and it is not the invocation [`context/executors.md`](../../../context/executors.md) names; + nothing here makes one of those. Run the candidates there, in order, and then **check the tree is still + clean** — `git -C status --porcelain` must print nothing. **A dirty tree is a wrong answer, not + something to work around:** name what it changed and fix the answer — the install form that reads the + lockfile instead of rewriting it, or a `.gitignore` entry for a file a step generates. Remove the + directory afterwards. Where this repository has no commit yet there is nothing to clone, so say the + section was written unverified and name what you would have run. + +8. **Then put the phase trees out of Gate 1's way, and prove they are.** `/feature-implement` runs a round + of phases in detached checkouts under `.phase-trees/` **inside this working tree**, so every command + above has to pass over that directory — or phase *k*'s gate lints the other phases' half-written work, + runs their copies of the suite, and in at least one real configuration *writes* to them. + - **Write `.phase-trees/` into this project's `.gitignore`** at the repository root. Not + `.git/info/exclude`: that is per-clone rather than shared, and some hosts refuse to write inside the + git directory at all. The entry is where most tools take their ignore list from, and it is also what + keeps these checkouts out of `git status` and out of `git add -A`. + - **Then prove it, because honouring that file is a per-tool setting and not every tool does.** Plant + two files under `.phase-trees/probe/`: one the lint command must reject, and one test that must fail. + Run every command bound for a gate section. **None of them may see either file.** + - **Where one does, fix that tool's own configuration** — whichever it has: a setting that reads the + ignore file, a file list, a test exclude — then run it again. **Say which tool and which setting**; + that edit is this project's, not this workflow's. + - **Where it cannot be fixed, set the cap to 1** in *Coder* in + [`context/executors.md`](../../../context/executors.md), with the reason beside it. That is the + recorded way to say one phase at a time, and it is an answer rather than a gap. + - **Remove the planted files**, and report what the proof found either way. + Explain what you are doing: this turns `verify.md` from someone's guess into something verified at install time, which is the one moment it is cheap to catch. @@ -584,8 +642,10 @@ leaving it implied. the unit. - **What a release ships, and on what event** — fill in what bumps a version, what tags, what cuts a release, what publishes and what deploys, one line each, and write "nothing yet" where that is the truth. - **Nothing in this workflow does any of the five**, and this section is what stops the answer being read as - "releases happen automatically". + **Nothing in this workflow does any of the five on its own initiative** — `/release` runs the *Bump* wire + in the turn somebody types that command, and nothing at all runs the other four — and this section is what + stops the answer being read as "releases happen automatically". **Say who runs the Bump wire while writing + it**: a wire nobody is recorded as running is the one most likely to be read as firing by itself. Then, from the sweep's fourth line, **the event and what it ships per path**. There is **one event, not one per artifact kind**: the merge of the pull request where the notes were consumed and the versions diff --git a/.claude/skills/orchestrate/SKILL.md b/.claude/skills/orchestrate/SKILL.md index e697ff0..c1df5d4 100644 --- a/.claude/skills/orchestrate/SKILL.md +++ b/.claude/skills/orchestrate/SKILL.md @@ -1,6 +1,6 @@ --- name: orchestrate -description: "Run one ad-hoc, commit-sized change through the same verification and review gates the feature loop uses, without a roadmap entry or a phase ledger. Explicit invocation only — run this when the user types /orchestrate. Do NOT match on 'build X', 'implement X', 'orchestrate the work', or any request that belongs to a planned feature." +description: "Run one ad-hoc, commit-sized change through the same verification and review gates the feature loop uses, without a roadmap entry or a phase ledger — or, with --pr, end that run at a pull request instead of in the working tree. Explicit invocation only — run this when the user types /orchestrate. Do NOT match on 'build X', 'implement X', 'orchestrate the work', or any request that belongs to a planned feature." disable-model-invocation: true model: sonnet effort: medium @@ -23,6 +23,8 @@ Read [`context/workflow.md`](../../../context/workflow.md) for the gate contract ``` /orchestrate "" /orchestrate # # the issue is the brief +/orchestrate --pr "" # end at a pull request, not in the working tree (step 7) +/orchestrate --pr # ``` **The second form is how a bug reaches this loop.** A defect too small to plan has no backlog entry and @@ -52,7 +54,8 @@ A refusal here is the workflow working. ## 2. Do the work -Read `context/stack.md` and load `context/standards/README.md` per its conditional table. +Read [`context/stack.md`](../../../context/stack.md) and load `context/standards/README.md` per its +conditional table. **Ask the surface question first**, per the standing rule in [`context/workflow.md`](../../../context/workflow.md): does this change put something in front of a person, @@ -107,9 +110,19 @@ found* in [`context/workflow.md`](../../../context/workflow.md). - `PASS` or `PASS WITH NOTES` → done. - `FAIL` → loop back. -**A non-blocking observation goes in this run's report and dies with the session** — unless it needs code -changes, in which case it is work and belongs in the backlog per -[`context/tracking.md`](../../../context/tracking.md). +**Every non-blocking observation gets one of the ends in *What happens to a defect the gate found* in +[`context/workflow.md`](../../../context/workflow.md), and kind decides which** — not size, and **not +whether it needs code changes**, which every true observation about code does. + +- **A real defect, and it is user-visible or a regression would land green** → a **bug**, wherever this + project already files bugs, carrying this project's own labels. **Never the backlog label** — see *A bug + is not a backlog entry* in that same file. The backlog is a list of features and this is not one. +- **Not a defect, but work you would want a `history.md` row for** → that is a backlog entry, and adding + one is `/roadmap`'s. **Name it and say what you would add**; do not append to the backlog from here. That + command applies the worth-adopting test and writes what the ranking reads, and a gate doing it by hand + produces neither. +- **Anything else** → this run's report, and it dies with the session. **There is no `context/notes.md` in + this command** — step 5 says why. ## 5. Loopback @@ -131,22 +144,25 @@ resume — the tree either carries the change or it does not. **This command writes no `context/notes.md`**, and that is the one place it departs from *What happens to a defect the gate found* in [`context/workflow.md`](../../../context/workflow.md). That file's cheap end is bounded by `/feature-close` deleting it, and an ad-hoc change has no close — so a note written here is the -one that would outlive every branch. **An observation that is work becomes an issue; every other one goes -in the report and dies with the session.** +one that would outlive every branch. **So step 4's cheap end has nowhere to live here: a bug is filed, a +feature is `/roadmap`'s to admit, and everything else goes in the report and dies with the session.** ## 6. Land it — read [`context/git.md`](../../../context/git.md) -**Do not commit unless that file says the agent does.** If it does not exist, the answer is *the user -commits*: say so once, and name `/onboard`. +**Do not commit unless that file says the agent does, or `--pr` was typed** — that flag is the same file's +other source of permission and step 7 is where it is spent. If the file does not exist, the answer is *the +user commits*: say so once, and name `/onboard`. -**Nothing here branches, worktrees or pushes**, whatever *Where work lands* and *Push and pull request* -say. Both of those answers are about a feature — one branch or tree per entry, one push at +**Without `--pr`, nothing here branches, worktrees or pushes**, whatever *Where work lands* and *Push and +pull request* say. Both of those answers are about a feature — one branch or tree per entry, one push at `/feature-close` — and an ad-hoc change has no entry and no feature to close. It lands on whatever branch is already checked out. **A worktree answer is not permission to move an ad-hoc change into a tree of its -own**; that is the commonest way this rule gets read backwards. +own**; that is the commonest way this rule gets read backwards, and **the flag does not change it** — step 7 +pushes a branch and creates no tree. - **The user commits** → leave the change **unstaged** in the working tree and hand it over. Staging it is - not a head start; it is half a commit. + not a head start; it is half a commit. **Under `--pr`, go to step 7 instead**: a pull request with nothing + committed to it is not a smaller version of this flag, it is nothing. - **The agent commits** → one commit, at the granularity that file names. **Run from `#`, the commit is what closes it.** Put `Closes #` in the commit message, the @@ -156,13 +172,74 @@ user commits, say the line rather than writing it**: it is their commit, and a c on their behalf. Never close the issue by hand as a separate act — a closed issue whose fix is sitting unstaged in somebody's tree is worse than an open one. -## 7. Report +## 7. `--pr` — the change leaves the machine + +Without the flag this invocation is over at step 6, and the change is committed or waiting on whatever +branch you were already on. With it, the run ends at a **pull request**: one commit, the branch pushed, and +the work opened for a person to read. + +**The flag is the permission, not a new `git.md` answer.** That file names two sources and only two — an +answer covering this command at this point, *or* the user asking in this session in plain words — and a +typed flag is the second one. So the flag does not need *Who commits* to say the agent does, and it does +not read *Push and pull request*: both of those answers are about a feature, and this command has none. +What it authorises is **this invocation**. Nothing it does becomes policy for the next one, and nothing +here is written into `git.md`. + +It buys exactly three things, in this order: **the commit, a push of the branch you are on, and the pull +request.** Anything past that is refused below, under *What the flag is not*. + +**The body is this run's report** — step 8's content, which is the only description of the change that +exists. Run from `#`, step 6 already put `Closes #` in the commit, so the body does not repeat +it: the trailer closes the issue on merge whichever text the forge reads. + +### It cannot invent a branch + +- **HEAD is not the default branch** → commit, push that branch, open the pull request. Nothing was created, + which is why this is the case the flag is built for. +- **HEAD is the default branch** → **stop before the commit**, unless *Branch and worktree* in + [`context/executors.md`](../../../context/executors.md) names a command — then run that one. Pushing to + the default branch is asked for by name each time under every `git.md` answer, and an empty *Branch and + worktree* means the workflow makes no branch, not that it should improvise `git checkout -b`. Say which + of the two stopped you, leave the change in the tree, and let the user make a branch and run it again. +- **That command makes a worktree rather than a branch** → stop and say so. Step 6 refuses to move an + ad-hoc change into a tree of its own, and a flag on this command is not the user lifting that. + +### Stop, and hand back + +The flag is permission to finish, not an instruction to land. **Stop, report, and name the line that +stopped you**, when: + +- **step 1 refuses the scope.** A flag is not a reason to admit work that is not commit-sized, that a + roadmap entry already covers, or that arrived carrying the backlog label. +- **Gate 1 does not come back clean, or Gate 2's verdict is `FAIL` on its last allowed loop.** Nothing is + committed and nothing is pushed: a pull request is where finished work goes to be read by a person, not + where unfinished work goes to be verified. Step 5's handback is unchanged — the change stays in the tree. +- **the push will not fast-forward**, or the repository has no remote to push to. + +**Step 2's release-note ask still happens.** The level is a judgment and `release.md` has it confirmed +before the note is written; this flag names where the run ends, not that nobody is reading. A run that +waits there is working as intended. + +### What the flag is not + +**Nothing merges it.** Not on green CI, not on an approving review, not after any wait. `git.md` reserves +review and merge in one line — *review and merge are yours* — and a pull request nobody reads is the one +thing this flag must not produce, since then the push bought nothing the working tree did not already have. +It also does not delete a branch, enable the forge's own auto-merge, or stay alive watching a check run. + +**It lifts no refusal and widens nothing.** It does not skip a gate, raise a loopback cap, admit a scope +step 1 refused, or turn an ad-hoc change into a feature. A stop list worked around once is not a stop list. + +## 8. Report What changed, whether it is committed or waiting in the tree, the Gate 1 output, the Gate 2 verdict, any loopbacks, any non-blocking observations the review raised — which die here — any bug filed for work that outlived the change, whether the commit closes the issue it ran from, and any release note written, with the paths that were checked and owed nothing. +**Under `--pr`**, also the branch that was pushed and the pull request that was opened — or the line in +step 7 that stopped short of one — and that nothing merged it. + ## Rules - **No ledger row is touched.** This command has no phase and does not belong to a feature. @@ -171,3 +248,4 @@ the paths that were checked and owed nothing. - **The release note is not deferred to a later command.** There is no `/feature-close` behind this one to write it, which is exactly why the change is the unit. - **Never skip Gate 1 to save time.** The gates are the entire reason this command exists. +- **Nothing merges, and nothing waits for CI.** `--pr` ends at a pull request a person has not read yet. diff --git a/.claude/skills/prototype/SKILL.md b/.claude/skills/prototype/SKILL.md index 1fab619..ae2dd11 100644 --- a/.claude/skills/prototype/SKILL.md +++ b/.claude/skills/prototype/SKILL.md @@ -42,8 +42,9 @@ Say which question the mockup exists to answer, in one line, before writing anyt **Borrow before inventing.** This repository exists already, so most of the look is decided — and a mockup built from values the project does not have is a picture of an app nobody is going to build. -Read `context/stack.md`, then find the real thing: the stylesheet or token file, the design system or -component library, and two or three existing screens close to what is being mocked. Copy the actual token +Read [`context/stack.md`](../../../context/stack.md), then find the real thing: the stylesheet or token +file, the design system or component library, and two or three existing screens close to what is being +mocked. Copy the actual token names, spacing scale, radii and type stack out of them. Invent only where there is genuinely nothing to borrow — a new surface, or a project with no styling of its diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..6d89c7d --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,254 @@ +--- +name: release +description: "Cut a release from the notes already accumulated on the default branch — make the branch the recorded command makes, run the script context/release.md's Bump wire names, and end at a pull request for a person to approve. Explicit invocation only — run this when the user types /release. Do NOT match on 'ship it', 'cut a release', 'release this', 'publish it', or a release being obviously due." +disable-model-invocation: true +model: sonnet +effort: medium +--- + +# /release + +Consumes the notes that are waiting, moves the versions, and ends at a **pull request nobody has read +yet.** No roadmap entry, no ledger, **no tier boundary crossed** — and nothing released: the version moved +in a branch, and what ships it is the merge. + +It exists because [`context/release.md`](../../../context/release.md) has always named the ship event as +*the merge of the release pull request* — and nothing in this workflow created one. `/feature-close +--release` fuses the release into a feature's own merge, which is right for a repository that releases +about as often as it merges. This command is the separated shape: the notes accumulate on the default +branch as features land, and somebody decides when they go out. + +Read [`context/workflow.md`](../../../context/workflow.md) for the standing rules — above all +*Writing a note is the workflow's half; consuming notes is not*, which this command is the asked-for +exception to. + +## Usage + +``` +/release # the branch is release/, as YYYY-MM-DD +/release "" # the branch is release/ +``` + +**Typing the command is the permission.** [`context/workflow.md`](../../../context/workflow.md) forbids +running what bumps, tags, publishes or deploys, with one exception — *when the user asks for it in that +turn* — and **a typed command is what that asking looks like**, the same way `/orchestrate --pr` and +`/feature-close --release` are. So it is never inferred: not from "and ship it" earlier in the session, not +from a plan, not from the notes looking ready, and not from a release being plainly overdue. **A sentence +is not a command.** + +**The name is the act, not the version.** The version is not knowable until the script that consumes the +notes has run, and that script **cannot be run twice** — so a branch named for the version cannot exist +before the thing that would name it. The version goes in the commit message and the pull request title, +where it is known. A name that already exists **stops the run**: do not suffix it, because an existing +release branch is either one in progress or one somebody abandoned, and both want a person. + +## 1. Refuse, before anything else + +Nothing is created and nothing is run until all five pass. They are in this order because the first two +cost nothing and are the common case. + +1. **No *Bump* wire in [`context/release.md`](../../../context/release.md)** → nothing in this repository + is written down as consuming the notes, so there is nothing to run and inventing it would be guessing at + how somebody releases. Refuse and name `/onboard`. The notes are unharmed; they wait, which is what + they are for. +2. **That file's *What records a note* answer is *nothing*** → this project records no notes, so there are + none to consume. Refuse and name `/onboard`. +3. **No notes are pending** → there is nothing to release. A run that moves no version publishes nothing + and writes a changelog entry describing nothing. Say so and stop; do not reach for whatever the + mechanism offers to make the run proceed. +4. **No invocation under *Branch and worktree* in + [`context/executors.md`](../../../context/executors.md)** → the workflow makes no branch, and **that is + not a licence to improvise one.** Never run a bare `git checkout -b`, `git branch` or `git worktree + add` because that section is empty. The alternative to a branch here is moving a version on the default + branch and pushing it, which is asked for by name each time under every + [`context/git.md`](../../../context/git.md) answer and is not what typing this command asked for. + Refuse, name `/onboard`, and say the release can still be cut by hand. +5. **HEAD is not the default branch, or the working tree is not clean** → a release is cut from the ref the + notes accumulated on, and uncommitted work would be swept into the release commit. Say which of the two + stopped you and stop. + +**A feature in flight is not a reason to refuse.** Its notes are not on this ref yet, so this release does +not include it — and that is the property the whole command rests on: **the notes on this ref are exactly +the changes on this ref.** Say which features are in flight and that they are not in this release. + +A refusal here is the workflow working, not a problem to route around. + +## 2. Gate 1, before anything is consumed + +Read [`context/verify.md`](../../../context/verify.md) and run every section above *Not run by Gate 1*, in +order — Lint → Typecheck → Build → Test first, then anything that file adds after them. The gate contract +is in [`context/workflow.md`](../../../context/workflow.md); a missing section is skipped, never faked, and +exit 0 is the verdict regardless of what any summary text claims. + +**It runs here, before the branch exists and before the script runs, and that ordering is the whole +reason this section is second.** The script that consumes the notes cannot be run twice. A gate run +afterwards, on a default branch that turns out to be red, leaves a tree whose notes are gone and whose +release cannot be cut again — **the one unrecoverable state in this design.** Finding out first costs one +gate run. + +**Gate 1 not clean → stop.** Nothing created, nothing consumed, nothing to undo. Report what failed and +say the notes are untouched. + +**There is no Gate 2, and that is deliberate.** Gate 2 reviews code against this project's standards, and +this run writes none — the version move and the changelog are a script's output. The review this change +needs is a **person's, on the pull request**, which is the entire reason the command ends at one. Do not +dispatch a reviewer to read a generated changelog. + +## 3. Show what it will consume, and confirm — before anything is created + +**This is the last moment anything here is free to change, and it is the only confirmation this command +has.** + +List **every pending note.** Not this session's, not the ones you can account for — every one, including +notes other people wrote for work that merged while nobody was releasing. For each, say what it describes +and what level it carries. Then, from [`context/release.md`](../../../context/release.md): + +- **what version each package lands on**, per note level; +- **what the merge of this pull request will do to each path**, from that file's *what a release ships* + answer — published, deployed, or nothing — and **what it leaves behind**. Read it; never infer it from + the fact that a version exists. + +Then confirm, using your runtime's question mechanism if it has one. + +**Every level is now final, and say so while asking.** [`context/release.md`](../../../context/release.md) +has the level confirmed where a note is written rather than here, on the reasoning that a note is a tracked +file publishing nothing and a wrong level is cheap to correct right up to the release. **This command is +that release.** A level that looks wrong is corrected by editing the note before the script runs, and after +it there is nothing left to edit — the note is gone and the version has moved. + +**Where that file records no answer for a path this release moves**, name the path, say the answer is +missing, and name `/onboard`. Do not guess at what the merge does to it, and do not hold up the release +over a gap in a configuration file. + +**Name no release tool.** That file says what records a note here and what consumes them. + +## 4. Make the branch, then run the Bump wire — in that order + +**The branch exists before the notes are consumed.** A script that eats the notes in a tree with no branch +to carry them has put the only copy of every pending note into whatever was checked out, and guard 5 says +that is the default branch. + +1. **Create it with the invocation [`context/executors.md`](../../../context/executors.md) names under + *Branch and worktree*.** That invocation, or nothing — never the bare git command, which skips whatever + the recorded one does around it. Say which invocation you ran. + + **A separate working tree is welcome here**, and this is the one place in the workflow where that is + true. `/orchestrate --pr` refuses one because an ad-hoc change has no feature and a tree of its own was + not what any answer authorised; a release is not ad-hoc work, and a branch of its own is the point of + the command rather than a side effect of it. Run whichever the recorded invocation makes, say which, and + **where it makes a separate tree, run everything below inside it.** + +2. **Then run the script the *Bump* wire names, exactly once.** It consumes every note, moves the versions + and writes the changelogs. [`context/release.md`](../../../context/release.md) says what it is; naming a + tool here would be wrong in most repositories this command runs in. + +3. **If it fails part-way, stop.** Do not re-run it and do not reach for a repair: the notes may already be + gone, and a second run on a half-consumed set is how a release goes out describing half of itself. + Report what it printed, what state the tree is in, and that the branch is there to inspect. + +## 5. Read the diff + +The versions that moved and the changelog text are the whole of it, and **the changelog text is what the +release page will say** — that is what +[`context/release.md`](../../../context/release.md)'s *Release* wire does with it, and it is where a note +finally reaches the audience it was written for. Read it rather than trusting the script's summary, and say: + +- **every version that moved, from what to what**, and whether that matches what step 3 confirmed. A + disagreement is a finding, not a rounding error. +- **every package that moved which no note named.** A major can drag a dependent in with a patch where it + puts that dependent's range out of range — and **where the dependent is a path that deploys, its version + moved, so this merge deploys it.** Say so plainly; it is the consequence least likely to be noticed and + most likely to matter. +- **any changelog entry that reads wrong.** Report it. The text came from a note, the note is gone, and + rewriting a shipped changelog is the user's call on the pull request rather than something to tidy up + here. + +**Never hand-edit a version.** The Bump wire owns every version it moved, and a hand-typed one is a number +nothing agrees with. + +## 6. Commit, push, open the pull request + +Read [`context/git.md`](../../../context/git.md). **The typed command is the permission, not a new answer +in that file.** It names two sources and only two — an answer covering this command at this point, *or* the +user asking in this session in plain words — and a typed command is the second. So this command does not +need *Who commits* to say the agent commits, and it does not read *Push and pull request*: both of those +answers are written about a feature, and a release is not one. What it authorises is **this invocation**. +Nothing it does becomes policy for the next one, and **nothing here is written into +[`context/git.md`](../../../context/git.md).** + +**One commit**, carrying the version moves and the changelogs and nothing else — guard 5 made sure there +was nothing else. Name the version in its subject. + +**Push that branch and open the pull request.** Its title carries the version; its body is: + +- every version that moved, +- the changelog entry for each, or a link to it in the file — **never a summary written in its place.** + That text is the description of this release and it was written by whoever wrote each note; +- what the merge will ship per path, and what it will leave behind, from + [`context/release.md`](../../../context/release.md); +- which features' notes are in it. + +**The push is a plain push of this branch.** If it will not fast-forward, or there is no remote, **stop and +say so** — and say it precisely, because the state is unusual: the branch exists, the notes are consumed, +and the commit is sitting there for the user to push. Force-pushing and pushing to the default branch are +asked for by name each time, under every answer, and this command has not been given either. + +### Nothing merges it + +Not on green CI, not on an approving review, not after any wait. +[`context/git.md`](../../../context/git.md) reserves it in one line — *review and merge are yours* — and a +release pull request nobody reads is the one thing this command must not produce, since the whole of what +it buys over moving the version on the default branch is that a person looks first. It also does not delete +a branch, enable the forge's own auto-merge, or stay alive watching a check run. + +**It removes nothing either.** Where the invocation in step 4 made a separate working tree, that tree is +still there after this command ends, and removing it is the job of whatever +[`context/executors.md`](../../../context/executors.md) names — after the merge, and not this command's at +any point. Say in the report that it is there. + +### The note check will go red, and a note is not what fixes it + +Where [`context/release.md`](../../../context/release.md)'s *What records a note* answer names a check that +asks *are there pending notes*, it will fail on this pull request, every time: there are none, **because +they became the changelog.** The description exists, more permanently than before. + +**Do not write a note to silence it.** These mechanisms offer a placeholder note carrying no change, and it +is the obvious way past a check that has gone red on the one commit that owes nothing — which is exactly +why reaching for it is the signal that **the check is asking the wrong question.** That file says what to +do about it, and the condition that resolves it is the one its ship answer already uses: *this path's +version moved in this merge.* + +Report it and leave it. Where the answer records the exemption and the check is red anyway, say that too — +the answer claims something the check does not do, and closing that gap is editing a workflow, which is not +this command's to edit. + +## 7. Report + +- the branch, and the invocation that made it — and where that made a separate working tree, that it is + still there and what removes one. +- **every note consumed**, and whose work each described. +- **every version that moved, from what to what**, including any package no note named. +- the pull request, and **that nothing merged it.** +- **what the merge will ship, per path, and what it will leave behind** — read from + [`context/release.md`](../../../context/release.md), never inferred. +- any path that file has no answer for, named rather than guessed at, with `/onboard`. +- the note check, if it went red, and that no note was written to quiet it. + +**Say what this is waiting for.** The merge. **Never report this as released, published, deployed or +live** — a version moved in a branch, and that is all that has happened. For a path that deploys, the merge +is what puts the change in front of users, and saying so is how somebody knows what approving the pull +request does. + +## Rules + +- **The Bump wire runs exactly once, and never twice.** It consumes the notes as it goes. +- **Never cut a release on the default branch.** Pushing to it is asked for by name each time, under every + answer, and this command was not given that. +- **Never improvise the branch.** [`context/executors.md`](../../../context/executors.md) names the + invocation or there is no branch. +- **Never write a release note here.** This command consumes notes; it has no change of its own to + announce, and a release commit carrying a note is a release describing itself. +- **Never hand-edit a version or a published changelog.** +- **No ledger row is touched, and no roadmap entry is created, activated or retired.** A release is not a + feature. +- **Nothing merges, nothing waits for CI, and nothing is removed.** diff --git a/.claude/skills/tracking-migrate/SKILL.md b/.claude/skills/tracking-migrate/SKILL.md index fe302ce..1258a79 100644 --- a/.claude/skills/tracking-migrate/SKILL.md +++ b/.claude/skills/tracking-migrate/SKILL.md @@ -63,10 +63,13 @@ list exists to prevent. `/feature-status` is where that gets resolved. - **A plan in `plans/` with no roadmap entry, or an entry whose **Doc** points at nothing.** The entry and its document travel together, and a migration is the wrong moment to decide which of the two is right. -- **A plan too large for an issue body.** [`context/tracking.md`](../../../context/tracking.md) says how - large a body may be; measure every plan against it, leaving room for the ledger rows that will be written - into the body later. Name each feature that overflows and the margin by which it does, and stop. **A plan - that does not fit is a feature that is several features**, and splitting one in two is a scope decision — +- **A plan too large for an issue body.** *How much a plan may hold* in + [`context/tracking.md`](../../../context/tracking.md) gives the ceiling and the per-phase reserve; + measure every plan against both. **A repository that has been measuring never reaches this refusal** — + the number is the same under both answers, so a plan `/feature-plan` passed in the tree fits a body. + Reaching it means these plans predate the budget, or somebody raised it here. Name each feature that + overflows and the margin by which it does, and stop. **A plan that does not fit is a feature that is + several features**, and splitting one in two is a scope decision — the same reason the row above refuses. Split it in the tree first, into an entry and a plan apiece, then re-run. Never trim a plan to fit, and never spill one into comments or a linked paste: this command moves data, and a plan that arrives shorter than it left has not been moved. diff --git a/AGENTS.md b/AGENTS.md index d1a0f74..efe4c5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,8 @@ the standing rules, and every command cites it rather than restating it. | `/feature-implement` | activates a planned feature and runs its phases, through both gates | | `/feature-status` | read-only "where do things stand". **Never a prerequisite** for anything | | `/feature-close` | retires a finished or abandoned feature into `context/archive/` | -| `/orchestrate` | one ad-hoc, gated, commit-sized change — no roadmap entry, no ledger | +| `/orchestrate` | one ad-hoc, gated, commit-sized change — no roadmap entry, no ledger. `--pr` ends it at a pull request | +| `/release` | consumes the notes waiting on the default branch into a release pull request — the one command that bumps, and only when typed | | `/prototype` | a throwaway HTML/CSS mockup under `prototypes/` — no gates, no ledger, no application code | | `/onboard` | fills in this project's own stubs — `verify.md`, `executors.md`, `git.md`, `tracking.md`, `release.md`, `stack.md` — adopting what an existing `AGENTS.md` already said | | `/tracking-migrate` | moves existing entries, drafts and plans onto the substrate `tracking.md` names — after `/onboard` sets it, never instead | @@ -42,8 +43,9 @@ the standing rules, and every command cites it rather than restating it. that is not `done` and whose `Depends on` are all `done`; state which you picked before starting; update the row as part of the same change as the work. **If the ledger disagrees with the repo, stop and say so.** -**Never stage, commit, branch, create a worktree, push or open a pull request on your own initiative.** Do -it where [`context/git.md`](context/git.md) says so, or where the user asked in this session — nowhere else. +**Never stage, commit, branch, worktree, push, open a pull request, or run what bumps, tags, publishes or +deploys, on your own initiative.** Only where [`context/git.md`](context/git.md) or +[`context/workflow.md`](context/workflow.md) says so, or the user typed it. **A phase is `done` only when its scope landed and both gates passed** — never on a self-report. diff --git a/context/.state/manifest.json b/context/.state/manifest.json index 815efae..0d9233d 100644 --- a/context/.state/manifest.json +++ b/context/.state/manifest.json @@ -1,53 +1,59 @@ { "schemaVersion": 1, - "version": "0.22.0", + "version": "0.26.0", "adapters": [ "claude", "agents" ], "managedFiles": { - ".agents/skills/feature-close/SKILL.md": "fb42b9f6411715d8cfe4666c7b2c4d9d87cf432b807d5f5a952f76f29f48ee67", - ".agents/skills/feature-close/tracker.md": "101a89481a31b4c250ee659ebd56421f83a5c4047e24b924372844faea09d739", - ".agents/skills/feature-implement/SKILL.md": "ecd6395d0be00b78852ddc1a93ff308214b819796265f1407d3cc95f19389d2c", - ".agents/skills/feature-implement/tracker.md": "7985addd78d484f1ed1581851e45e712eada925677b38ce311e6318e49f3e8be", - ".agents/skills/feature-plan/SKILL.md": "d4720b82c864fd1a9430eaf0394267bf40e1201889240a57b5bb550937600c71", - ".agents/skills/feature-plan/tracker.md": "bff9705a34ccd46d331094be9287fff6768765befd0104e21590d8b3d9ad71cf", - ".agents/skills/feature-status/SKILL.md": "0e41341b7144e2de5a273b4d661979e32d2a1b906b026fc7db1629bd76d0e3c1", + ".agents/skills/feature-close/SKILL.md": "16984fcd2b308d8d8afbe911f2c1955f8ebbcde853f412db765cdd802d9ad47d", + ".agents/skills/feature-close/tracker.md": "6cf2698c8dc59d78e6be5ead277b31049a43e543c49c6faf34449bd7d9507dac", + ".agents/skills/feature-implement/SKILL.md": "c009db913d6e6380014cd8f86c6df5c7ba146743db261c3a1f88c6f660dafeb0", + ".agents/skills/feature-implement/parallel.md": "c41ba6135f8c6835c7b1e1e38a37c829c8b554d37fd19662a08f85399e567c15", + ".agents/skills/feature-implement/tracker.md": "e0342a5eed4dc62508d5dbe903cdb45e293c08e98b8e57106451db61efdefb29", + ".agents/skills/feature-plan/SKILL.md": "84d91f92945d89456ea98dd2e51bae43827696470d2db781912b9c547fa28292", + ".agents/skills/feature-plan/split.md": "0f868d3c0eab43a07b46757cb0c9b0d5b3d2a9a7a81847a52f0ada19fab88ea5", + ".agents/skills/feature-plan/tracker.md": "f3815ffe47cb7cba1c22ac6b639d662f643ab9f57e41041872f759c943fc72c7", + ".agents/skills/feature-status/SKILL.md": "f7bcd5e06b2a9530ad4f382963765d842278b33048464299f7556d5297b449bf", ".agents/skills/feature-status/tracker.md": "20c35c5dfd506654e2eac1ee2386dd269178cb93f37f3276b7c821bfb57893fd", - ".agents/skills/onboard/SKILL.md": "0af9d601c7796e58d78ff9ba85e8c94ad08f31c4424ac9e1f2c55cd3f40b4f56", - ".agents/skills/orchestrate/SKILL.md": "c19c998c5af8e1559ced9382e16cd325759d1a3f795271bfee424288966053d8", - ".agents/skills/prototype/SKILL.md": "01efae578e1a1559c0953218f0158cf5fd558d91d3118a1fe3212bdd3417496f", + ".agents/skills/onboard/SKILL.md": "e6d94ba977107c613c763b474fa808eb07f63b57408c3446d27af431d0f813f4", + ".agents/skills/orchestrate/SKILL.md": "dd610a4e8011a8af2e665cfeaa6eedaf5e04d162b15395af241005f2db666643", + ".agents/skills/prototype/SKILL.md": "4edab78b37a2d2ca95d31af276c8e733cd5b470a2b099e781e75433702018ced", + ".agents/skills/release/SKILL.md": "72171a6df12aa9a168f3580002c171f874cad232d4806d9f503fbdb16877850f", ".agents/skills/roadmap/SKILL.md": "3503c98dc427cbb0ee6b7888cd6c0b02fef91449f0a6451f62d66d696f71d08c", ".agents/skills/roadmap/tracker.md": "0dd38371894f16fb0c704b6bac8f6fa7575603a237a05aefd84240afdf8eb260", - ".agents/skills/tracking-migrate/SKILL.md": "175c65993d3c49f593632157530288fee95851c55699d11ba9f080d0ab440c4d", - ".claude/agents/planner.agent.md": "d2aa5986a65f701c21090707f61fdbff22598b738619a756f3f96c96fab4aae6", - ".claude/agents/reviewer.agent.md": "9e94dbf54f785f5008c8b32045930c29f62b407221971969ee88a456093a8dfe", - ".claude/skills/feature-close/SKILL.md": "80f1295d8a68e823a3d24d9f00f9ad282893ee9ab889524de4e1b3939ec040e4", - ".claude/skills/feature-close/tracker.md": "101a89481a31b4c250ee659ebd56421f83a5c4047e24b924372844faea09d739", - ".claude/skills/feature-implement/SKILL.md": "67517538687e3cb0eb4203d1ac58b3d2c0e7d8bc89e4c76c5867cd7c191ac8d0", - ".claude/skills/feature-implement/tracker.md": "7985addd78d484f1ed1581851e45e712eada925677b38ce311e6318e49f3e8be", - ".claude/skills/feature-plan/SKILL.md": "0ff64461c2b5d1825e4a0cbc10e4bd94ca97330c6315242fb5d1cec050f26692", - ".claude/skills/feature-plan/tracker.md": "bff9705a34ccd46d331094be9287fff6768765befd0104e21590d8b3d9ad71cf", - ".claude/skills/feature-status/SKILL.md": "af772dfde23a111cc509e663508f868974de52b8817bb2a8cf57d8f74b15cf0c", + ".agents/skills/tracking-migrate/SKILL.md": "00b81223da3b2161e0387ca6826f265acdf7742b1bfc05046b86f17eaa76c968", + ".claude/agents/planner.agent.md": "b5e66e667d47b4f7041886ca48ece09679c4c537159306cb1a24a6eb8f51ea92", + ".claude/agents/reviewer.agent.md": "db1b17472b6793378c55dfcb7b892ac07462007827eeadd664a76c65106a221b", + ".claude/skills/feature-close/SKILL.md": "2e8bb617cdbc765e4d2b200a8ac7ffac9fe4e885990432b945080e96a53e0bcf", + ".claude/skills/feature-close/tracker.md": "6cf2698c8dc59d78e6be5ead277b31049a43e543c49c6faf34449bd7d9507dac", + ".claude/skills/feature-implement/SKILL.md": "307eee003af1219ab5e609cb9f364853d141d0faefe1f2d28c3ab63b9aa0906d", + ".claude/skills/feature-implement/parallel.md": "c41ba6135f8c6835c7b1e1e38a37c829c8b554d37fd19662a08f85399e567c15", + ".claude/skills/feature-implement/tracker.md": "e0342a5eed4dc62508d5dbe903cdb45e293c08e98b8e57106451db61efdefb29", + ".claude/skills/feature-plan/SKILL.md": "9a12805b80b8d6ef01e22173c3c1f82e2b98f9e4759a0be40ddbc59de2a009a0", + ".claude/skills/feature-plan/split.md": "0f868d3c0eab43a07b46757cb0c9b0d5b3d2a9a7a81847a52f0ada19fab88ea5", + ".claude/skills/feature-plan/tracker.md": "f3815ffe47cb7cba1c22ac6b639d662f643ab9f57e41041872f759c943fc72c7", + ".claude/skills/feature-status/SKILL.md": "d16fc67173777dbe37887802d743d5713849cb41ec983d4cd7bab107aa9c4c75", ".claude/skills/feature-status/tracker.md": "20c35c5dfd506654e2eac1ee2386dd269178cb93f37f3276b7c821bfb57893fd", - ".claude/skills/onboard/SKILL.md": "83f74756c71dc582a1f45eb146935f5388b644ec44b9988dcf9991ca76d1c53e", - ".claude/skills/orchestrate/SKILL.md": "e186f1ae3984026f0c5039f6451938f6bbde2ed889fbda183eef1b0ffa07f628", - ".claude/skills/prototype/SKILL.md": "2af63c574a2a0582b3d5aa807f9a686ee2692cbc20ae2d839ab80a5a69a78226", + ".claude/skills/onboard/SKILL.md": "884092fca88b96fcdf39e1f7320a2f0d817578bead16fac06a6821fca97ace89", + ".claude/skills/orchestrate/SKILL.md": "b0213f3523848fe05613ffa0759043d3ba1c6b38cc45228fdbb4b59e55ccf14e", + ".claude/skills/prototype/SKILL.md": "23cacfc1e80537f94a9381110ee2693c9d4ee7a7632aa8f4f5de097b48463507", + ".claude/skills/release/SKILL.md": "ddfef5eefce9fad7f2700e6f1eec4d6b4869e5e0de56eaaa1b0a99643d1da505", ".claude/skills/roadmap/SKILL.md": "dbb4e60e67d7964062adeab3611bb43a1a21300ffcb10330452e17798ee30193", ".claude/skills/roadmap/tracker.md": "0dd38371894f16fb0c704b6bac8f6fa7575603a237a05aefd84240afdf8eb260", - ".claude/skills/tracking-migrate/SKILL.md": "afbc72de769f43331e9921ca0cd202d160913031fc23fbe5a4f7c2d79d38b256", - "AGENTS.md#ai-workflow": "dffd9b2a7bfb6f3d770009240f279ef8f840c178dfdc2b48f88c9aab2fedb118", + ".claude/skills/tracking-migrate/SKILL.md": "b6a0d3c892cff26b2ec8b085a47b1c93f0f73b627fe2e6702746c1e8338df230", + "AGENTS.md#ai-workflow": "543b1cbcf9c8350849dad3a4570955d3e86cb87016da00d2e8980b65e47d7ae3", "context/README.md": "c79966ddbfa05e3271efdb4fa3820aa39f1c4b0c004347ee578a5a7b8b15a57d", - "context/executors.notes.md": "8a8f52fd2f4a6fb945d31c7e7e87ae1bbc3b0e058916a1a9be45f4d697bf7c5d", + "context/executors.notes.md": "33e51ff30f675cbcd2a42093feac805c59f5de2fccaad30b1d4f4c0de1ae810a", "context/git.notes.md": "e6cf4c0f4f2d933f331a9dbd01744770a254ca7fcd9c91f43635fa00f7192760", - "context/plan-template.md": "d30a6e498d3e08b673ab9bd39c8a68ab99b85d6dee5de1cd03fbb3eaee12a595", - "context/plan-template.notes.md": "5ef4319943e9a4ec57713d16d4c0ed0d9c3b903e9848b3791ef860e1839479d1", - "context/release.notes.md": "1877496117eb6b7880709e53a5cca72f99069d0877c35065ff41847feab866a4", - "context/roles/coder.md": "2e22865de598693022abba1e8f4fd12af4ec20bd401eb219e8be0243d84ad55b", + "context/plan-template.md": "bfed214d1fe222454aed24c4cc2bdd72e8255ea5003185cd5d207b6b8a8aa3a6", + "context/plan-template.notes.md": "50cdae33c1a5ac555357b3e3dafbd3dbd6708605b764bbd73eeb32d9a136ac31", + "context/release.notes.md": "0f66130b216038d576c361e0b2e2efad49aa859685745d28a28ce47fde607468", + "context/roles/coder.md": "f47da64fcfff6a0be40c170c8df8496d9b889f6c3dfb53a16d6c790bfcffcc41", "context/stack.notes.md": "7493b5d68d6d6899a071abc80aaff6219587cf2dacd27a942a8eed579e3c87b5", - "context/tracking.notes.md": "207eb3dda2404f62e0d6d7d8ef9f30beb7d2fe26747226820331b35b11679caa", - "context/verify.notes.md": "f3bcfecef2a8c0417786e7ca48a46f345b0c5f5ee1d84615e8d6757bec0246de", - "context/workflow.md": "139a912a80d5dc27521616d1a3436f04be28e540aebcb625bb7eb104774d62a4", - "context/workflow.notes.md": "e0501170f362ae138a6272f08cb556f228faf1b405f612fad7d831d442d4252e" + "context/tracking.notes.md": "3f62f0900b5a3b69d9f1fc76d6fd67433f459be8393e97916a1f628e57c36111", + "context/verify.notes.md": "fbd31f3f8ded8c840569cc6d00a21965b575d867840a8d01627b6b0cd814798c", + "context/workflow.md": "c5226d45c7b0b2624627b426751739cf21d0ffa13c483daf937d9cd12fb4dfee", + "context/workflow.notes.md": "00dc16b5ac52f1e58aaa2e48eee7e80c10a905a811bf11dd4eed679381e20a1f" } } diff --git a/context/executors.notes.md b/context/executors.notes.md index 03bd6f0..b7887a1 100644 --- a/context/executors.notes.md +++ b/context/executors.notes.md @@ -24,6 +24,11 @@ Record alongside an external one whether it was actually observed reading this r when. `/onboard` tests it; the standing rule in the stub says why the answer changes how briefs are written. +**The concurrency number is separate from the three answers and survives changing them.** It is how many +coders this machine and this account can carry at once, not how one is dispatched — so a project that hits +a usage limit with three running lowers it, and `1` stops `/feature-implement` forming a round at all. +Nothing but that command reads it. + ### The three answers, copy-ready **Not configured — implement in-host.** diff --git a/context/plan-template.md b/context/plan-template.md index f90edd0..feb0e46 100644 --- a/context/plan-template.md +++ b/context/plan-template.md @@ -42,9 +42,21 @@ only when the scope landed and both gates passed, and whoever finishes a phase u the same change as the work — who commits that change is [`../git.md`](../git.md)'s answer, not this table's. +**The Note is one line: what a later session needs in order to understand or resume the Status beside it.** +What is underway while a row is `in progress`, what remains where some of the phase landed, what is +blocking a `blocked` row, the commit sha where one is `done`. Nothing else — measurements, test counts, +gate output and the account of how the phase went go in the commit message that carries the row, under +[`../git.md`](../git.md)'s answer, or in the run's own report. A Note that grows into a phase log makes the +ledger unreadable at exactly the moment it is being read in a hurry, and it is the one column in this +document with no length anyone can see coming. + **Exactly one table in this document has these columns.** Do not add a second phase table — a differently-shaped one nearby is a decoy that gets read by mistake. +**A plan has a size budget**, and *How much a plan may hold* in [`../tracking.md`](../tracking.md) says what +it is. A plan over it is not trimmed to fit — it is a feature that is more than one feature, and +`/feature-plan` proposes the split. + ### 6.2 The phases #### Phase 1 — @@ -53,6 +65,8 @@ differently-shaped one nearby is a decoy that gets read by mistake. **Standards:** +**Decisions:** + **Scope:** **Done when:** @@ -63,6 +77,8 @@ differently-shaped one nearby is a decoy that gets read by mistake. **Standards:** +**Decisions:** + **Scope:** **Done when:** @@ -83,6 +99,18 @@ reads as "not checked", and nothing downstream can tell those apart.> +**What decides each value this feature computes.** One row per value, so the expected value in a test has a +source that is not the implementation. + +| Value | What decides it | +|---|---| +| | | + + + ## 9. Open questions -- +- diff --git a/context/plan-template.notes.md b/context/plan-template.notes.md index ec1cbfc..3f81aaa 100644 --- a/context/plan-template.notes.md +++ b/context/plan-template.notes.md @@ -30,6 +30,14 @@ acceptable response; silence is not. `Depends on` value (`—` for none): the phase-selection rule *is* "lowest-numbered phase that is not `done` and whose `Depends on` are all `done`", so a missing column silently degrades it. +**The Note is one line, and it is the one column with no visible bound.** Everything else in the row is a +number, a word from a fixed set or a short list; the Note is prose in a table cell that nothing wraps, so +it grows until a reader cannot find the Status beside it. Keep it to what a later session needs in order +to understand or resume that Status — what is underway, what remains, what is blocking, the sha where the +phase is `done`. The measurements, the test counts, the gate output and the account of how the phase went +belong in the commit message that carries the row or in the run's report, both of which are read by +someone who went looking for them. + **§6.2 The phases** — one sub-section per ledger row. **The `Files:` line is required, not advisory.** It is what turns `/feature-status`'s reconciliation from a judgement call into a check: without it, "does the repo match the ledger" has no answer. A phase is a **commit-sized unit of work with a checkable outcome**, @@ -45,6 +53,12 @@ always row, the rows for the code the phase touches, and the rows the surface qu misses. A reviewer that finds the change drifted past the line opens the table; that is the fallback, not the route. +**The `Decisions:` line is the same move applied to §3.** A phase's constraints are in §3 and its work is +in §6.2, and a brief that carries only the second leaves the coder to either read the whole plan or +re-decide what §3 already settled. The line names the decisions that govern the phase, by number and in a +clause each, so the brief is the phase's own section and nothing more. Where a phase is genuinely governed +by no decision, say `—`; the empty line and the unfilled one read alike otherwise. + **§7 Documentation** — every place this project explains itself that the feature makes wrong, out of date or incomplete. The surfaces come from [`stack.md`](stack.md)'s Documentation index, and from a sweep of the repository when that index is empty — an index nobody filled in is not evidence that there are no docs. @@ -62,7 +76,20 @@ something a person operates, an **end-to-end pass in a real browser** for a user to compare for a hot path. Which driver runs it is [`verify.md`](verify.md)'s answer and never this section's; a feature whose project has no such driver says that in §9 rather than naming one. -**§9 Open questions** — what the plan could not settle. Do not paper over them. +**It is also where each value the feature computes gets a source**, one row each: the requirement, the +specification, the published reference or the outside source that says what the value should be. The coder +works test-first from that row — *An expected value has a source, and it is never the code under test* in +[`workflow.md`](workflow.md) — so a row left unwritten is an expectation somebody derives mid-phase, in the +one place it is cheapest to derive it from the code that was just written. Research is what finds these, +which is why they are planned rather than left to the phase. A feature that computes nothing of its own +says so; a value whose source the research could not find is a §9 open question, because the alternative is +a number the implementation gets to choose. + +**§9 Open questions** — what the plan could not settle. Do not paper over them. **Say which items need a +person**, because this section has two readers after you: `/feature-implement` surfaces it at activation, +and `/feature-close` goes through it before the plan is archived and gives every item that still needs a +person an end — answered, filed where this project files bugs, or waived by the user in that turn. An item +a phase settles is answered in place, by that phase. Nothing here is archived unread. ## Standing rules @@ -76,3 +103,7 @@ section's; a feature whose project has no such driver says that in §9 rather th first edit, and a §-number survives the move into `archive/`. - **Never mark a phase `done` in a plan that has not been executed.** Every phase in a new plan is `not started`. +- **A plan has a size budget**, in *How much a plan may hold* in [`tracking.md`](tracking.md). It is + measured before the plan is written, with room held back for the ledger rows still to come. A plan over + the line is split along phase boundaries, never trimmed to fit: trimming discards the research the plan + exists to hold, and a trimmed plan reads exactly like one that was small enough. diff --git a/context/release.notes.md b/context/release.notes.md index e938111..6e1ee21 100644 --- a/context/release.notes.md +++ b/context/release.notes.md @@ -10,9 +10,9 @@ false answer, and every command would dutifully write into it forever while noth ## What announces a change, and to whom -**Shipped as:** nothing here announces a change. It is the only answer that is true of every repository -before anyone has looked — including one that publishes, where it says the answer has not been set rather -than that there is nothing to announce. +**Shipped as:** nothing here announces a change. It is the only answer that meets the standard above — +including for a repository that publishes, where it says the answer has not been set rather than that +there is nothing to announce. Replace the shipped line with one row per path that announces something. The table is **per path**, not per repository: a repo can publish one thing, deploy another, and say nothing about a third. @@ -37,9 +37,8 @@ A single-package repository gets one row, and that is a real answer rather than ## What records a note -**Shipped as:** nothing. Keep this until something in the repository actually records notes — an answer -naming a mechanism that is not on disk is a false answer, and every command would dutifully write into it -forever while nothing consumed it. +**Shipped as:** nothing. Keep this until something in the repository actually records notes: a mechanism +that is not on disk is the false answer the standard above rules out. Replace the shipped line with what this project actually uses. Three common shapes: @@ -107,9 +106,9 @@ stay under either answer. ## What a release ships, and on what event -**Shipped as:** nothing here ships on a merge. Like the first answer in the stub it is true of every -repository before anyone has looked — including one that deploys, where it records that the event has not -been written down rather than that nothing reaches users. +**Shipped as:** nothing here ships on a merge. Like the first answer in the stub it meets the standard +above — including for a repository that deploys, where it records that the event has not been written down +rather than that nothing reaches users. Replace the shipped paragraph once something does. There is **one event, not one per artifact kind**: the merge of the release pull request — the one where the script that consumes the notes was run, so the notes @@ -122,7 +121,11 @@ for that one. Write down what the event is wired to, one line each: - **Bump** — the script that consumes the notes, and who runs it. It cannot be run twice, and where a - deploy watches versions it is the button that ships. + deploy watches versions it is the button that ships. **Write this line even if nothing else in the + section is filled in**: it is the one wire a command in this workflow reads. `/release` runs it, on a + branch of its own and after Gate 1, in the turn somebody types that command — and where this line is + empty that command refuses rather than guessing, which is the behaviour an empty wire buys. A person + typing it by hand is also a real answer; say which. - **Tag** — what creates the tag, and from what. **Do not assume the publish step owns this.** In the common shape one command both publishes and tags, so a repository that publishes gets tags without ever deciding to — and a repository that only deploys runs no such command and silently gets none. If nothing diff --git a/context/roles/coder.md b/context/roles/coder.md index 643883f..72262d0 100644 --- a/context/roles/coder.md +++ b/context/roles/coder.md @@ -9,12 +9,18 @@ verified. ## What you receive -- A **plan** — the scope, the files, the acceptance criteria, and the review checklist the reviewer will - use. +- **One phase of a plan, as its own section** — the scope, the files, the acceptance criteria, and the + review checklist the reviewer will use. **That section is the brief, not the whole plan.** A plan holds + the problem, the risks and the phases already finished; none of it is yours to implement, and opening it + to look for your work is how a phase grows scope it was not given. Where you need something the section + does not carry, say which section you had to open and why. - The phase's **`Standards:` line** — the files under `context/standards/` this work is reviewed against. **Read them yourself**; they are files in this repository, not something that has to be pasted into your brief. Where the brief has no such line, load per the conditional table in `context/standards/README.md` instead. +- The phase's **`Decisions:` line** — the plan's §3 decisions that govern this phase. **They are settled.** + Implement what they say; where one is wrong, say so in your notes for the reviewer rather than quietly + taking the other branch. Where the brief has no such line, read §3 of the plan. - On a loopback: your prior implementation and the validator's verbatim feedback. ## What happens to your output @@ -29,6 +35,47 @@ Two gates, in order: Before you declare a task done, walk through both: *will verification pass?* — fix anything you can already see will fail. *What will the reviewer flag?* — fix the obvious ones. +**Neither gate asks where an expected value came from**, which is the next section. + +## Decide the expected value before the code runs + +Per *An expected value has a source, and it is never the code under test* in +[`../workflow.md`](../workflow.md): where this phase has behaviour you can observe at a seam, **work +test-first, in thin slices.** A slice is one observable behaviour at that seam — the happy path first, then +one per edge case, error state and option the phase is responsible for. List them before you write +anything. + +For each slice, in order: + +1. **Decide what the seam should produce.** Take it from the requirement, the phase's acceptance criteria, + the plan's §8 Verification — where a value this feature computes has a row naming what decides it — or + the specification or published reference that governs it. Be able to say in one line why that is the + answer. +2. **Check it against something that is not the implementation.** A specification, a reference + independent of this codebase, a worked example, an arithmetic check done by hand. **Never the code under + test**, never a helper this change also touches, and never a reference implementation written for this + task — all three agree with the code by construction. +3. **Write the assertion and run it.** Confirm it fails, and that it fails for the reason you expect. An + assertion that was already passing is asserting something else. +4. **Make the smallest change that turns it green.** If green appears to need a different expected value, + stop and go back to step 1: change the expectation only where the derivation itself was wrong, and say + so in your notes for the reviewer. Picking whichever value passes is the failure this whole section + exists to prevent. +5. Move to the next slice, and let what this one taught you reshape it. + +**Write down what decided each value**, in the test's own name, in a comment beside the constant, or in +your notes for the reviewer. The reviewer treats an expected value with no traceable source as a finding +even when the suite is green, so a value you could not source is something to raise rather than to assert. +**A rule you implement that no test asserts is the same kind of finding** — if nothing would fail when the +rule stopped holding, name it and say where it could be pinned. + +**Say when none of this applies.** Documentation, configuration, and wiring that only passes a value +through have no behaviour of their own to observe, and a test written first against them tests the +framework. Say which of the two the task was; the unanswered question reads exactly like a skipped step. + +Test behaviour at a seam rather than internals — that rule is in the files your phase's `Standards:` line +names, and it is not restated here. + ## Loopback rules When you are re-invoked with validator feedback: diff --git a/context/tracking.notes.md b/context/tracking.notes.md index e19eeef..151a3cb 100644 --- a/context/tracking.notes.md +++ b/context/tracking.notes.md @@ -46,8 +46,25 @@ Under the tracker answer, fill in its three parameters and check its one optiona - **Types.** Where the project has issue types configured, name them; where it has none, delete the `**Types:**` line. The workflow sets a type and never reads one. -Everything else in that subsection — the primitive table, the planned label, priority, `blocked by`, the -body's ceiling — is live prose and stays as it is. +Everything else in that subsection — the primitive table, the planned label, priority, `blocked by`, and +what the body's ceiling costs when it is reached — is live prose and stays as it is. + +## How much a plan may hold + +Two numbers, and both ship with an answer already in them: a **ceiling** of 65,536 characters and a +**reserve** of 500 characters per phase row. They hold under both answers above, so this section stays +either way — which is why it is a `##` of its own, below the subsection deleted under the tree answer and +not inside it. + +**Leave them alone unless the project has a reason.** The ceiling is the issue body's hard limit under the +tracker answer, and the working-tree answer takes the same number on purpose — a plan that fits one +substrate fits the other, so adopting a tracker later never forces a split that the tree had already +passed. Raising it in the tree is the one edit that breaks that, so a project that raises it should know +it is choosing plans a tracker will not accept. + +The reserve is how much the ledger is expected to grow after the plan lands: one sha and a one-line Note +per row, written twice. It is the number to change if this project's phases are unusually many or its +Notes unusually long — and a Note long enough to matter is the thing to fix instead. ## What this file does not decide diff --git a/context/verify.notes.md b/context/verify.notes.md index 9373665..9c602fe 100644 --- a/context/verify.notes.md +++ b/context/verify.notes.md @@ -7,8 +7,8 @@ alone. Each heading below mirrors a section of the stub, in the stub's order. ## Before the first section Write the prerequisites, if any, as a short paragraph above `## Lint`: node version, package manager, -install step already run, services up. A command that only passes after `install` has run is a command -that fails on a clean checkout until somebody says so. +services up. Facts a person reads. The commands that *make* a clean checkout pass go in *Prepare a fresh +tree* below, where something can run them. ## Lint, Typecheck, Build, Test @@ -26,6 +26,15 @@ can still afford on every phase, and that fails because of the change rather tha — an accessibility suite, a size budget, a coverage floor. Gate 1 runs every section above *Not run by Gate 1*, in order, so a heading placed there is run like any of the four. +**One of them is offered by `/onboard`, and is worth adding by hand if that offer was declined or never +made: a check that no test in the suite is skipped, focused or expected to fail.** A runner reports a +skipped test as a pass and a focused one silences every test beside it, so *Test* above can exit 0 while +the suite asserts almost nothing — and no other section here can see it, which makes this the gap the four +headings leave. **Optional on purpose:** a project that uses a skip to mark work in progress has chosen +something this check forbids, and declining it is a real answer. Where it is wanted, it goes in *Test* if +this project's runner or its lint rules can assert it there, otherwise under a heading of its own; either +way the command belongs in [`verify.md`](verify.md)'s block and nowhere else. + ## Not run by Gate 1 Three kinds of command go here: ones that need Docker, a cloud account or a deploy target; ones Gate 1 @@ -37,6 +46,25 @@ section by mistake. **Shipped as:** empty. +## Prepare a fresh tree + +The commands that turn a clean checkout into one where the sections above pass — the dependency install +first, then anything else: a generated client, a built artifact a test imports, a local service. One fenced +`bash` block, from the repository root, in order. **Run them in a fresh checkout before writing them +down**; a command that only works in a tree somebody has been living in is what this section exists to +catch. + +**These commands have to leave the tree clean**, which rules two things in. **Use the install form that +reads the lockfile rather than rewriting it** — every package manager has one, usually a `--frozen-lockfile` +or `--immutable` flag or an install subcommand of its own, and it is the form CI already uses — and +**gitignore anything a step here generates**: a client, a built artifact a test imports, a schema. +`/feature-implement` checks `git status --porcelain` after running this section and will not run a round of +phases in a tree that came back dirty, so a rewritten lockfile costs the round; left unfixed it also shows +up in every phase's diff from then on. + +**Shipped as:** empty. An empty section means `/feature-implement` runs one phase at a time instead of a +round of them side by side, and says so. It is not a gate failure and nothing else changes. + ## Rules Nothing to fill in. Live prose in the stub, and Gate 1 reads it every time. diff --git a/context/workflow.md b/context/workflow.md index 5fb4a64..b9ef1fd 100644 --- a/context/workflow.md +++ b/context/workflow.md @@ -20,7 +20,7 @@ running something else. | Tier 1 → a plan | `/feature-plan` | | a plan → being worked, then phase by phase | `/feature-implement`, or `/feature-implement --all` | | Tier 2 → retired | `/feature-close` | -| no tier crossed | `/orchestrate` — one ad-hoc gated change; `/feature-status` — read-only; `/prototype` — a throwaway mockup | +| no tier crossed | `/orchestrate [--pr]` — one ad-hoc gated change; `/release` — cut the accumulated notes into a release pull request; `/feature-status` — read-only; `/prototype` — a throwaway mockup | **Every command finds its own starting point.** Nothing has to be looked up first, and `/feature-status` is never a prerequisite for anything. @@ -36,8 +36,9 @@ This file holds the rules. Why each rule is what it is, and what was tried inste | `/feature-plan` | Tier 1 → a plan document | `plans/-PLAN.md`; the `active` marker only with `--activate` | | `/feature-implement` | activation, and the phases within a plan | the plan's ledger, the code, and a release note where [`release.md`](release.md) says *per phase* | | `/feature-status` | nothing — read-only | — | -| `/feature-close` | Tier 2 → retired | `history.md`, `archive/`, the reference sweep, a release note where [`release.md`](release.md) says *once per feature*, and the push and pull request where [`git.md`](git.md) says so | -| `/orchestrate` | one ad-hoc gated change | the code, and a release note where [`release.md`](release.md) says one is owed | +| `/feature-close` | Tier 2 → retired | `history.md`, `archive/`, the reference sweep, an end for every open question that still needs a person, a release note where [`release.md`](release.md) says *once per feature*, and the push and pull request where [`git.md`](git.md) says so | +| `/orchestrate` | one ad-hoc gated change | the code, a release note where [`release.md`](release.md) says one is owed, and under `--pr` the branch it pushes and the pull request it opens | +| `/release` | the one act that consumes notes | nothing of its own — it runs what [`release.md`](release.md)'s *Bump* wire names, on a branch [`executors.md`](executors.md) made, and ends at a pull request | | `/prototype` | one throwaway HTML/CSS mockup — no gates, no application code | `prototypes//`, and nothing else | | `/onboard` | the project-owned stubs | `verify.md`, `executors.md`, `git.md`, `tracking.md`, `release.md`, `stack.md`, and the pruning of what they replace | | `/tracking-migrate` | moving existing state onto the substrate `tracking.md` names | issues, and the tree files they replace — never `history.md` or `archive/` | @@ -106,14 +107,20 @@ commit-sized unit and anything an existing roadmap entry already covers. [`git.md`](git.md) answers three more, each independent and each shipping as the most conservative option: **where work lands**, **whether the agent pushes and opens a pull request**, and at what **granularity** it -commits. A push happens once per feature, at `/feature-close` — never at the end of a phase — and nothing -here merges a pull request, deletes a branch, or removes a worktree under any answer. +commits. Under those answers a push happens once per feature, at `/feature-close` — never at the end of a +phase — and the only pushes outside them are the two asked for by name: `/orchestrate --pr`, and +`/release`. **Nothing merges a pull request, deletes a branch, or removes a worktree under any answer, any +flag or any command.** The one exception is not a worktree in that sense: `/feature-implement` makes a +**detached, branchless checkout in `.phase-trees/`** for a phase it runs beside another, and removes it +when its row reads `done`. Not by the recorded invocation, and no other command makes or removes one. **Those answers authorise the commands in this workflow, at the point each one names, and nothing else.** *The agent commits* is permission for `/feature-implement` to close a phase it just ran, not standing leave to commit whatever is in the tree; *a worktree per feature* is permission for a **planned feature's** first phase to make one, not for an ad-hoc request to be moved into a tree of its own. Outside those points, an -operation needs the user to ask for it in this session. +operation needs the user to ask for it in this session. **A typed flag or a typed command is that ask** — +`/orchestrate --pr` and `/release` are the two the workflow ships, and each authorises that invocation's +commit, push and pull request without changing a single answer in [`git.md`](git.md) for the next one. **Permission is not inferred.** Choosing between approaches does not authorise any of this, even where the option text mentions it — and *especially* where you wrote that option text yourself. Neither does "ship @@ -125,7 +132,9 @@ default branch, and rewriting published history. Each has to be asked for by nam **A worktree is created only by what [`executors.md`](executors.md) names.** If that file names no invocation, there is no fallback — a bare `git worktree add` is the failure this rule exists to stop, not -the default. The same goes for removing one. +the default. The same goes for removing one. **`/release` is the one command that may make one for +something that is not a feature**, because a branch of its own is what that command is for rather than a +side effect of it — and it still makes it only with the recorded invocation, and still removes nothing. ### Where this state lives is an answer, not an assumption @@ -156,14 +165,36 @@ a legitimate answer, and it names the surfaces that were checked. Saying nothing > **Before loading standards, ask what surfaces this change has: something a person operates > (accessibility), a hot path or a payload that grows (performance), a trust boundary — input, -> authentication, a secret, a public endpoint (security). Write the answer down. *None of these* is an -> answer; saying nothing is not.** +> authentication, a secret, a public endpoint, a URL the server will fetch (security). +> Write the answer down. *None of these* is an answer; saying nothing is not.** **A surface reaches the work the way a documentation row does.** The standards rows it loads become the review expectations on the phase that carries it, and whatever proves it goes in that plan's §8 Verification — for something a person operates, an **end-to-end pass in a real browser**. Which browser driver is [`verify.md`](verify.md)'s answer like every other command, and nothing here names one. +### An expected value has a source, and it is never the code under test + +> **Where a change has behaviour you can observe at a seam, work test-first in thin slices: decide what +> each expected value should be — from the requirement, the specification, or a source independent of the +> implementation — before the code that produces it runs, and write down what decided it. Never read an +> expectation back out of the code under test. Documentation, configuration and wiring that only passes a +> value through have no such seam; say which it is rather than leaving the question unasked.** + +**A green suite proves that the tests and the implementation agree with each other.** It says nothing about +whether either agrees with the requirement, so an assertion copied from what the code returned passes both +gates forever and pins nothing: Gate 1 runs the suite, and Gate 2 walks a checklist that was never asked +where the number came from. The plan is where the sources are written — a value this feature computes gets +a row in its §8 Verification naming what decides it, and a value whose source nobody could find is a §9 +open question rather than a number for the implementation to supply. + +**Two findings follow, and Gate 2 raises them whatever the suite says:** an expected value with no +traceable source, and a rule the code implements that no test asserts. Neither is visible to Gate 1 — the +first passes, and the second is behaviour nothing would fail if it stopped. + +Testing behaviour at a seam rather than internals is the standards tree's rule and not this one's; the +phase's `Standards:` line names the file that carries it. + ### What a change announces is an answer, not an assumption > **Read [`release.md`](release.md) before closing out any command that lands code. If it does not exist, or @@ -189,10 +220,18 @@ file does not cover is reported, never guessed at** — name the path and name ` A skill may **name** it, report that it is pending, and stop — the same way it names `/onboard` for a gap it will not fill itself. Asked for directly, in that turn, it is the user's call and theirs to give. -**`/feature-close --release` is what that asking looks like, and it is the only shape of it this workflow -ships.** A flag typed in the turn it takes effect, which lists every pending note it is about to consume — -other people's included — before it runs anything. **The gap between writing a note and consuming it is -where a wrong bump level is still free**, so a command that closes that gap says so while it asks. +**A command or a flag typed in the turn it takes effect is what that asking looks like, and those are the +only two shapes of it this workflow ships.** Both list every pending note they are about to consume — +other people's included — before running anything, and **neither is inferred from prose: a sentence is not +a command.** + +| Shape | The release is | Cut from | +|---|---|---| +| `/release` | its own pull request, and the merge of that is the ship event | the default branch, where notes accumulate as features land | +| `/feature-close --release` | fused into the feature's own merge | that feature's branch — for a repository that releases about as often as it merges | + +**The gap between writing a note and consuming it is where a wrong bump level is still free**, so whichever +of the two closes that gap says so while it asks. ### Never transcribe a credential @@ -223,7 +262,8 @@ To pick the next phase: take the **lowest-numbered phase that is not `done` and are all `done`.** State which one you picked before starting. If it is already `in progress`, read its Note and resume — do not restart it. -**One run is one phase, unless `--all` says otherwise.** That flag repeats the pick above, and stops +**One run is one round**, and a round is one phase or the independent phases `/feature-implement` runs side +by side under the cap in [`executors.md`](executors.md). **`--all` repeats the pick above**, and stops exactly where a single run would: a phase that ended `blocked` or part-landed, a gate at its loopback cap, a ledger that disagrees with the repo. When the last phase goes `done` it stops there and names `/feature-close` — **that boundary is still crossed by an explicit command.** @@ -332,3 +372,22 @@ has already written where the phase stands. **`/orchestrate` has no ledger**, so a capped gate there hands the work back: it stays in the working tree, the report says what failed, and anything still worth doing becomes an issue. **It writes no `notes.md`** — it has no close, so nothing would ever delete one. + +## What happens to a question the plan could not settle + +A plan's §9 Open questions has two readers after the planner: `/feature-implement` surfaces it at +activation, and `/feature-close` goes through it at the retirement. **In between it is the phases' to +answer** — whatever a phase settles is written into the item it settles, by that phase, the same way +whatever it makes untrue in the documentation is fixed by it. + +**What is still open at the retirement is an owner check** — an item no gate can close, because it needs a +person. **It is not a defect**, and it is not a backlog entry: nothing is known to be wrong, and nobody who +could say has looked. + +> **Every one of them gets an end before `/feature-close` writes anything** — answered, filed or waived, +> picked by the user item by item. A feature never retires with its open questions archived into a document +> no command opens again. + +**The ends, what each one writes, and where a filed check goes are `/feature-close`'s**, which is the only +command that reads §9 after activation. The destination for a filed one is the shelf *A bug is not a +backlog entry* already names, and the kind is not a bug. diff --git a/context/workflow.notes.md b/context/workflow.notes.md index 929a09c..57122dc 100644 --- a/context/workflow.notes.md +++ b/context/workflow.notes.md @@ -88,10 +88,36 @@ before they opened the table. The kinship is with the documentation rule, and so is the reason: **no gate fails** when a control cannot be reached by keyboard, any more than one fails when a README describes a flag that no longer exists. A linter -catches a fraction of the first and none of the rest. Neither gap was a missing standard — the vendored tree +catches a fraction of the first and none of the rest. Neither gap was a missing standard — the standards tree already said accessibility was part of done — so the fix is a question asked out loud, with *none of these* a legitimate answer and silence not one. +### An expected value has a source, and it is never the code under test + +The gates had no opinion about where an expectation came from. A test that asserts whatever the function +returned is green on the first run and green on every run after, so Gate 1 passes it, and Gate 2 was asked +whether the behaviour was covered and never where the number came from. Both gates can therefore be +satisfied by a suite that records what the implementation happened to do on the day it was written — and +the next change is free to move the behaviour and the expectation together, because nothing outside the +code ever said which one was right. + +This is the same shape as the two rules above it: **no gate fails.** A README describing a renamed flag, a +control no keyboard can reach, and an expectation copied out of the code are all invisible to a run that +exits 0, and none of the three was a missing standard. The standards tree already says to test behaviour at +a seam rather than internals; what it does not say is where the expected value comes from, and that is the +sentence this rule adds. + +Deciding the value first is what makes the order matter rather than the ceremony. A test written after a +passing implementation can still be a good test, but the cheapest way to write it is to run the code and +record the answer, and nobody reading it later can tell which of the two happened. Writing down what +decided the value is what makes that distinguishable — and it is why the planner carries the sources in §8 +rather than leaving each one to be rediscovered per phase. + +The two findings are split the way they are because they fail differently. A sourceless expectation is a +test that cannot fail; an unasserted rule is behaviour that can be removed without anything failing. One is +a check switched off, the other is a check that was never on, and a reviewer looking only for the first +leaves the second in place. + ### What a change announces is an answer, not an assumption *Documentation is part of the change* covers a README that a rename made wrong. It does not cover the @@ -249,3 +275,49 @@ change has no close — so a note written there is the one that would outlive ev change that cannot pass its gates is not a thing to file away; it is a thing to hand back, because nothing about it is half-finished in a way the next session could resume. The tree either carries the change or it does not. + +## What happens to a question the plan could not settle + +The field report is the shape of the thing. One repository retired six features `shipped` whose history +rows read *"the Icelandic and VoiceOver check await the owner"*, *"the owner's Icelandic wording and +VoiceOver reads await"*, *"the owner's wording, the halo and look and an iPhone check await"*. Every one of +those sentences is true, written on purpose, and read by nothing. `/feature-implement` surfaces the plan's +open questions at activation and no command opens them again, so they were archived into a file whose only +reader is somebody who already knew to look. + +**None of the three endings for a defect fitted it**, which is why it had none. It is not a bug — nothing +is known to be wrong. It is not a backlog entry — nobody wants a history row for *read the Icelandic*. And +it is not a note, because notes are deleted at the close, which is the moment the check becomes real. + +**The destination is the bug shelf and the kind is not a bug**, and keeping those two apart is the whole +design. The same shape turned up when a bug needed somewhere to go: the project's own tracker already was +the answer, and what was missing was a sentence saying so. The properties an owner check needs are the five +a bug already has there — durable, outside the backlog bit, holding an assignee, closed by a person, read +by nothing in the workflow — so a second shelf would buy a word and nothing else. But filing it *as* a +defect is a defect report nobody can reproduce, so it says what it is. + +**Why not a file.** Because of the property that makes `notes.md` right for the opposite case: a line in a +file costs nothing to write and **nothing to ignore**. For a note that is the feature; for a check somebody +has to perform it is the failure. The fact *this has not been done* has to be observable in a place a +person already looks, rather than stored where somebody has to maintain it. An open issue is observed. A +heading accumulating lines is maintained, and both stores this design has removed failed in exactly that +way. + +**Why the outcome column gains no value.** The temptation is a fourth word — `shipped`, with something +appended, so the row says the check is pending. `history.md` is append-only and never edited, so that word +would still be there a year after the owner looked, and nothing in the workflow would ever remove it. Under +the tracker answer the column is the close reason, which has two values and no room for a third. A status +in an append-only file is a status nothing can withdraw; an issue can be closed, which is why the row +points at one instead of claiming the fact itself. + +**Why it is not the triage ceremony this design has already refused once.** That sweep disposed of an +unbounded store of gate observations, and it bounded the tail while leaving the accumulation. This one +reads one plan's open questions — written once by `/feature-plan`, bounded by the plan, already read at +activation — and applies one bit to them: *does this need a person*. Most do not, and one a phase settled +reads as settled because that phase wrote the answer into it. The lifecycle does the rest: a plan is +archived and never read again, so nothing carries forward into the next feature. + +**Why the close rather than the phase that raised it.** The phases answer what they settle, and that is +where most items end. What is left is precisely what no phase could finish, and the retirement is the last +moment anyone is deciding anything about this feature with its code still in front of them — the same +argument that puts the promotion of a note, and the release note's bump level, where they are. From 70b0be94a6977e4b26bb6116c84931fb47c4bfc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baldur=20P=C3=A1ll=20H=C3=B3lmgeirsson?= Date: Sun, 18 Oct 2026 18:33:56 +0000 Subject: [PATCH 2/2] chore(context): re-onboard the stubs under ai-workflow 0.26.0 --- .gitignore | 4 +++ context/executors.md | 16 +++++++++++- context/git.md | 5 ++-- context/release.md | 44 +++++++++++++++++++++----------- context/stack.md | 3 ++- context/tracking.md | 61 ++++++++++++++++++++++++++++++++++++-------- context/verify.md | 60 +++++++++++++++++++++++++++++++++++++------ 7 files changed, 155 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index ab28249..18ba481 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ docs/worker/docs-context.ts # Wrangler local dev secrets (populated by worker:setup-dev-vars from .env.local) docs/worker/.dev.vars + +# /feature-implement runs a round of phases in detached checkouts here. Each carries its own biome.json, +# and Biome only skips a nested config through this file (vcs.useIgnoreFile), not through files.includes. +.phase-trees/ diff --git a/context/executors.md b/context/executors.md index 61822db..a6234b7 100644 --- a/context/executors.md +++ b/context/executors.md @@ -9,7 +9,8 @@ Configured by `/onboard` on 2026-09-05. The **Branch and worktree** section belo when [`git.md`](git.md) took the worktree-per-feature answer, under the title *Worktree operations*. `/onboard` renamed it on 2026-09-30 under `@baldurpan/create-ai-workflow` 0.22.0, whose `/feature-implement` looks the section up by that name, and added the removal command and the -no-improvising rule. +no-improvising rule. Re-run on 2026-10-18 under 0.26.0, which added the concurrency cap under *Coder*; no +answer changed. What each section takes, and the alternative answers written out, are in [`executors.notes.md`](executors.notes.md). @@ -27,6 +28,15 @@ record and no repository-read test to run — the brief is a file already in thi *Set on 2026-09-30 by the maintainer, replacing the in-host answer recorded on 2026-09-05.* +**At most 3 phases of one plan run at once.** `/feature-implement` forms a round out of phases that depend +on nothing unfinished and share no file, and dispatches a coder for each; this number is the ceiling on how +many. **1 turns that off** and every phase runs on its own. It is a fact about this machine and this +account, so it is kept whichever of the three answers above is this project's — lower it if a round runs +into a usage limit. + +*Confirmed at the shipped 3 by the maintainer on 2026-10-18, under 0.26.0. Nothing forced it lower: the +`.phase-trees/` probe recorded in [`verify.md`](verify.md) passed once that directory was gitignored.* + ## Reviewer **The bundled `reviewer` subagent, dispatched with the Agent tool.** @@ -148,6 +158,10 @@ nor `npx … @latest` can run, stop and say so. Removal is the same: a tree goes through `worktree remove`, not `git worktree remove`, so its Herdr space is closed with it. +**The scratch checkouts `/feature-implement` makes under `.phase-trees/` are not these.** They have no +branch, nobody looks at them, and that command makes and discards them itself — neither `worktree branch` +nor `worktree remove` is involved, and nothing in this section applies to them. + *Until 2026-09-30 this file offered `git worktree add` as a by-hand fallback. It was dropped by `/onboard` on that date, on the maintainer's answer, when 0.22.0's stub named it as the one command never to improvise.* diff --git a/context/git.md b/context/git.md index a59de77..5ab0a6f 100644 --- a/context/git.md +++ b/context/git.md @@ -72,8 +72,9 @@ between them. - **Env files do not travel.** A new worktree starts without the `.env` files the tool copies for a human; `copyEnvFilesFromRootPath` handles that at creation time. A tree made by any other means may need them copied before `pnpm docs:dev` or the Worker scripts will run. -- **`pnpm install` per tree.** `node_modules` is not shared between worktrees, and the prerequisites in - [`verify.md`](verify.md) apply in each one before Gate 1 means anything. +- **`pnpm install` per tree.** `node_modules` is not shared between worktrees, so run + [`verify.md`](verify.md)'s *Prepare a fresh tree* in each one before Gate 1 means anything — without it + every gate fails with `command not found`. ## What this file does not decide diff --git a/context/release.md b/context/release.md index 2a5daef..d851b85 100644 --- a/context/release.md +++ b/context/release.md @@ -22,7 +22,13 @@ in a repository where nothing records one, and a false answer here is the same d **Files:** do not exist. **A note is not a release, and a merge is not a deploy.** Nothing in this workflow bumps a version, tags, -publishes, releases or deploys. What does, in this repository, is the last answer in this file. +publishes, releases or deploys **on its own initiative** — only `/release`, or `/feature-close --release`, +in the turn somebody types it, and each of those runs what this file names rather than anything it knows. +What ships the change, in this repository, is the last answer in this file. + +**Re-checked by `/onboard` on 2026-10-18 under 0.26.0**, which added `/release`. The answers did not +change; the run history under *The wires* was brought up to date from the workflow runs, tags, releases and +the npm registry that day. What each section takes, and the alternative answers written out, are in [`release.notes.md`](release.notes.md). `/onboard` reads that file when it fills this one. @@ -116,8 +122,9 @@ before a handover. ## What a release ships, and on what event -**Nothing in this workflow bumps a version, creates a tag, publishes an artifact, or deploys anything.** -What does is below. **A feature's merge ships nothing to npm**: it lands a note on `main` and waits. The +**Nothing in this workflow bumps a version, creates a tag, publishes an artifact, or deploys anything on +its own initiative.** `/release` runs the *Bump* wire below when somebody types it; nothing in the workflow +runs any of the others. What does is below. **A feature's merge ships nothing to npm**: it lands a note on `main` and waits. The event is the merge of the release pull request, the one made with `pnpm changeset:prepare-release`, which moves the version. The one exception is the docs site, which deploys on any merge touching `docs/**`, bump or no bump. @@ -127,7 +134,12 @@ or no bump. - **Bump** — a release pull request made with `pnpm changeset:prepare-release` (`changeset version`, then `pnpm sync-version` to regenerate `docs/src/lib/site-meta.ts`, the one file `ci.yml` checks for drift). It must come from that command: it writes the version's `CHANGELOG.md` section, and a hand-edited version bump has none, so - `publish.yml` stops before publishing. + `publish.yml` stops before publishing. **Who runs it:** `/release`, in the turn a maintainer types that + command, on a branch of its own and after Gate 1 — or a maintainer running `pnpm + changeset:prepare-release` by hand. Both are real answers; nothing runs it on a merge or a schedule. + It cannot be run twice over the same notes, and since `publish.yml` watches the version, it is the button + that ships. v2.1.1 was a release pull request of its own (#101); v2.1.2's bump and `CHANGELOG.md` + section rode the fix pull request itself (#103), which `publish.yml` handled the same way. - **Tag, release, publish** — one workflow, `publish.yml`, on a push to `main` that touches `package.json` and moves its version (a `guard` job reads the previous version from `github.event.before`), or on a manual dispatch, which forces the guard open. The `publish` job, in the `release` environment with OIDC, @@ -137,16 +149,17 @@ or no bump. GitHub release from the CHANGELOG section (`--prerelease` for a prerelease); a Discord post. Each step skips itself if its result already exists (version on npm, tag, release), so a failed run is recovered with "Run workflow". It replaces `tag-on-version-change.yml` (deleted) and the old `release: published` - trigger, which could not fire once the release is created by a workflow's own token. **Not yet run**: the - first real publish under this shape is the maintainer's next release. `v1.2.8` was once tagged and never - released, so never published; that gap is closed because tag and release no longer come apart. + trigger, which could not fire once the release is created by a workflow's own token. **Runs, green**: it + published, tagged and released v2.1.0 (2026-10-01), v2.1.1 (2026-10-04) and v2.1.2 (2026-10-14), and npm + serves 2.1.2. `v1.2.8` was once tagged and never released, so never published; that gap is closed + because tag and release no longer come apart. - **Deploy (docs)** — `docs-deploy.yml`, on any push to `main` touching `docs/**`, `package.json`, the lockfile, the workspace file or the version-sync script. **Runs**, green. It is not gated on a version. -- **Deploy (Worker)** — `worker-deploy.yml`, on any push to `main` touching `docs/worker/**`. **Has never - succeeded**: all five runs, the latest on 2026-09-11, failed at *Deploy to Cloudflare* because the - `CLOUDFLARE_API_TOKEN` secret is empty in this repository. The file loads and its test and build steps - pass; only the credential is missing. So nothing recorded here deploys the Worker — whatever is live was - put there some other way. +- **Deploy (Worker)** — `worker-deploy.yml`, on any push to `main` touching `docs/worker/**`, or a manual + dispatch. **Has succeeded once**: `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` were set as + repository secrets on 2026-10-03, and a manual dispatch that day succeeded. Every earlier run, the last + push-triggered one on 2026-10-01, failed at *Deploy to Cloudflare* for want of that credential. No push + has touched `docs/worker/**` since, so the push trigger has not yet been seen to succeed. ### What each path gets on a merge @@ -154,7 +167,7 @@ or no bump. |---|---|---| | `@northguild/worktree` (root) | if its version moved: tested, built, published to npm, tagged, released | npm version + tag + GitHub release (all automatic) | | `docs` | deployed to GitHub Pages if the merge touched `docs/**` or `package.json` — version or not | nothing | -| `docs/worker` | `worker-deploy.yml` fires and fails | nothing | +| `docs/worker` | deployed to Cloudflare if the merge touched `docs/worker/**` — version or not | nothing | **The npm path is gated on its own version, correctly.** `publish.yml` keys on the root version moving, and a merge that does not bump it publishes, tags and releases nothing. @@ -166,8 +179,9 @@ docs can describe behaviour npm does not have yet, and a docs change merged with behaviour that is not published at all. A deployed site leaves no tag and no release, so nothing records which commit is live. -**The gaps, named and not generated:** a Worker deploy credential, and the docs deploy, which keys on no -version by decision (`docs` is ignored by Changesets) and so leaves no tag and no release. +**The gap, named and not generated:** the docs site and the Worker deploy on no version, by decision +(`docs` is ignored by Changesets), and so leave no tag and no release. The Worker's missing credential, +named here until 2026-10-18, is closed. ## The rules that hold either way diff --git a/context/stack.md b/context/stack.md index 951c502..6aee972 100644 --- a/context/stack.md +++ b/context/stack.md @@ -8,7 +8,8 @@ a small Cloudflare Worker proxying Gemini for its chat feature. One piece of history explains a lot of the current shape: the project moved from the `@burglekitt` org to `@northguild` in 35fc08f (2026-08-26). Package names, the biome plugin and the docs' shared library all changed scope in that commit, so anything installed or generated before it is stale in a way that produces -confusing errors rather than obvious ones — see the prerequisites in [`verify.md`](verify.md). +confusing errors rather than obvious ones — see the prerequisites and *Prepare a fresh tree* in +[`verify.md`](verify.md). | Concern | Target | |---|---| diff --git a/context/tracking.md b/context/tracking.md index af28399..ad6a18f 100644 --- a/context/tracking.md +++ b/context/tracking.md @@ -13,6 +13,9 @@ kept because plan bodies written under 0.8.0 still describe the old shape. features, the body's size ceiling, and the closing rules. The answer did not change. The `workflow:planned` label was created that day. +**Re-run by `/onboard` on 2026-10-18 under 0.26.0**, which split the plan budget out into *How much a plan +may hold*. The answer did not change; both labels were confirmed present, and no phase was `in progress`. + **This file holds an answer, not a procedure.** How a phase is claimed, when a heartbeat is written, and in what order a plan and its phases are created live in the skills, so a defect in one can be fixed by an update. What each section takes is in [`tracking.notes.md`](tracking.notes.md). @@ -50,7 +53,7 @@ them and every tree can reach it. | what a retired feature's outcome was | the closed issue — *completed* for shipped, *not planned* for dropped | | what kind of work it is | the issue's type — set by the workflow, read by nothing in it | | which features have to land before this one | the issue's **blocked by** relationships | -| how much a plan may hold | the issue body's size limit — **65,536 characters** | +| how much a plan may hold | *How much a plan may hold* below — here that ceiling is the body's own hard limit | | how a change closes one | `Closes #`, in the pull request body where there is one and in the commit message otherwise | ## The plan and its ledger both live in the issue body @@ -210,24 +213,60 @@ unblocked by something nobody is going to build, and only the close reason says **Best-effort, like the type.** It needs triage permission. Where the write is refused, say so once and carry on — the relationship is how the backlog is read, not a gate anything passes. -## The body has a ceiling, and it measures scope +## The body has a ceiling, and here it is enforced -**An issue body holds 65,536 characters.** That is the one hard limit in this substrate, and this is the -only place the number belongs. +**The ceiling in *How much a plan may hold* below is this substrate's own limit**, not a budget this +project picked — an issue body holds exactly that many characters and the API refuses the write at +65,537. Everything that section says about measuring first, holding the reserve back and splitting rather +than trimming applies here unchanged; what is different is the consequence of ignoring it. -**A plan that does not fit is a feature that is several features**, not a formatting problem. The plan -also needs room left over: every phase row gains a status, a commit sha and a note over the feature's life, -in the same body, so a plan that only just fits has already failed. +**The body is one write, so a failed one is the worst possible moment to learn the plan was too big** — +the research is done and there is nowhere to put it. `/feature-plan` measures before writing. +`/tracking-migrate` refuses a plan that will not fit rather than guessing at a split, names it with the +margin, and leaves the split to happen in the tree first. `/feature-implement` shortens its own Note and +writes the row anyway: the Status column is the phase's only home, and a row that never landed is state +that is simply gone, while a note can be re-derived from the commit it names. For scale, the four archived plans under `archive/` run from 33KB (`CLEANUP-DATA-LOSS-PLAN.md`) to 63KB (`AGENT-MODE-PLAN.md`), measured 2026-09-30 in bytes. The largest would sit at this ceiling as an issue body before its first phase row moved, and the smallest would already have spent half of it. That is the measure this section exists to apply, not a reason to relax it. -**Three workarounds are refused:** trimming the plan until it fits, moving sections into comments, and -linking out to a gist or a file. **The answer is to split the feature into separate issues**, each with its -own plan and ledger — `/feature-plan` proposes the split along phase boundaries and asks, and -`/tracking-migrate` refuses rather than guessing at one. +## How much a plan may hold + +**Plan size ceiling:** 65,536 characters +**Ledger reserve:** 500 characters per phase row + +A plan is measured against the ceiling **before it is written**, with the reserve held back — the plan's +own size plus the reserve times its phase count has to come in under the line. Both numbers are this +project's to change; everything that reads them reads them here. *Written by `/onboard` on 2026-10-18, +under 0.26.0, with the shipped numbers.* Raising the ceiling here would mean plans the issue body cannot +hold, so under this answer it is not really a dial. + +**The reserve is what makes this a check and not a formality.** The ledger is written into for the +feature's whole life: every row opens to `in progress` and then closes, gaining a commit sha and its Note. +A plan that only just fits has already failed, because by the last phase it would not — so the question is +never *does this plan fit*, it is *does it fit with room for the rows still to be written*. + +**Which is why a row that has closed holds no reserve.** It has already been written, and its characters +are in the measurement; counting it again would charge the plan twice and report it for making progress. +At planning time every row is still to be written, so the two readings agree — the difference only shows +once phases start landing, and then the thing still growing is the Notes, not the plan. + +**Overflow is a scope signal and the answer is to split the feature**, which is `/feature-plan`'s to +propose along phase boundaries and the user's to accept. A plan that reaches this line holds the design, +the phases and the risks of more than one piece of work. +**Three ways out are refused** — trimming the plan until it fits, continuing it into comments or +a second document, and linking out to a gist or a paste. The first throws away the research the plan +exists to hold, to satisfy a number that was telling the truth; the other two give one plan two homes. + +**The number is the same under both answers, and that is deliberate.** Under this project's tracker answer +it is the issue body's hard limit, so a plan that overruns it cannot be written at all. In the working tree +nothing enforces it, which is not an advantage: a plan document over the same line is over the same line. +Holding one number also means a plan is portable. + +`check` reports a plan over the line as a **note** and nothing more. It never proposes the split, and a +note does not fail the check: an oversized plan is a scope finding, not structural breakage. ## What the workflow will not touch diff --git a/context/verify.md b/context/verify.md index 8ab9499..24c92d4 100644 --- a/context/verify.md +++ b/context/verify.md @@ -8,18 +8,21 @@ in `context/`, that copy is the one that is wrong. Keep it in step with CI. If a command here fails while CI is green, this file is the one that is wrong. -**Every command below was run from the repo root on 2026-09-30 and exited 0** (re-verified by `/onboard` after a fresh `pnpm install --frozen-lockfile`; first verified 2026-09-06). Anything that did not is -recorded as such rather than written into a section. +**Every command below was run from the repo root on 2026-10-18 and exited 0** (re-verified by `/onboard` +under `@baldurpan/create-ai-workflow` 0.26.0, in this worktree and in a fresh clone; first verified +2026-09-06). Anything that did not is recorded as such rather than written into a section. ## Prerequisites - **Node 24** (`.github/workflows/ci.yml` pins `node-version: 24`) and **pnpm 10.32.1** (the `packageManager` field in `package.json`). -- **`pnpm install --frozen-lockfile` already run.** A stale `node_modules` is the first thing to suspect - when lint or the docs tests fail on a clean checkout: this repo moved from the `@burglekitt` org to - `@northguild` in 35fc08f, and an install predating that leaves `@northguild/gmt-biome` and - `@northguild/gmt` absent while the lockfile already names them. The symptoms are a biome - `Cannot read file` plugin error and `Failed to resolve import "@northguild/gmt"` in the docs suite. +- **The tree is prepared** — see *Prepare a fresh tree* below. A worktree made by `worktree branch` starts + with no `node_modules`, and every section here then fails with `command not found`. A *stale* + `node_modules` is the next thing to suspect when lint or the docs tests fail: this repo moved from the + `@burglekitt` org to `@northguild` in 35fc08f, and an install predating that leaves + `@northguild/gmt-biome` and `@northguild/gmt` absent while the lockfile already names them. The symptoms + are a biome `Cannot read file` plugin error and `Failed to resolve import "@northguild/gmt"` in the docs + suite. ## Lint @@ -40,6 +43,12 @@ filename exists in the tree. The standards bundle ships a Biome starter template [`stack.md`](stack.md)), so `update` no longer writes to it. Keep it that way — never let a file named `biome.json` or `biome.jsonc` back into the tree. +**The one exclusion that does work is `.gitignore`**, through `vcs.useIgnoreFile`. Every phase tree under +`.phase-trees/` is a full checkout carrying its own `biome.json`, and `/onboard` showed on 2026-10-18 that +without the `.phase-trees/` entry in `.gitignore` this command dies with *Found a nested root +configuration*; with it, a planted lint error, type error and failing tests there were invisible to every +section of this file. Keep that entry. + ## Typecheck ```bash @@ -66,10 +75,28 @@ interactive shell. If a console assertion ever starts failing only on someone's first. `pnpm test` covers `src/` and `scripts/`; `pnpm docs:test` covers the `docs` workspace including the Cloudflare Worker. -CI runs both. As of 2026-10-02 that is 1000 and 68 tests respectively — a snapshot for recognising a suite +CI runs both. As of 2026-10-18 that is 1006 and 68 tests respectively — a snapshot for recognising a suite that did not run, not a figure to assert against. It goes stale on any commit that adds a test, so correct it in passing rather than treating a mismatch as a failure. +## Test markers + +```bash +! git grep --untracked -nE '(^|[^A-Za-z0-9_.])(it|test|describe|suite)(\.[A-Za-z]+)*\.(only|skip|todo|fails|skipIf|runIf)([^A-Za-z0-9_]|$)' -- '*.test.ts' '*.test.tsx' '*.test.mjs' +``` + +Fails when any test file — tracked or new — holds a focused, skipped, todo or expected-to-fail test, +including chained forms such as `it.concurrent.skip` and conditional ones such as `describe.skipIf(…)`. +Vitest reports a skipped test as a pass and a focused one silences every test beside it, so *Test* above +can exit 0 while asserting almost nothing. **Biome does not cover this:** `noFocusedTests` reports `it.only` +but `pnpm check` still exits 0, and nothing in the recommended set sees `.skip`, `.todo` or `.fails`. + +`--untracked` is what makes it see a test a phase has just written, and it still honours `.gitignore`, so +`node_modules` and `.phase-trees/` stay out. The portable character classes are deliberate: `git grep -E` +on macOS does not honour `\b`, and a version of this command using it matched nothing at all. A property +access such as `expect(x.only)` inside a test does not trip it. Adopted by `/onboard` on 2026-10-18; a +test that genuinely has to be skipped is a conversation, not an edit to this pattern. + ## Not run by Gate 1 - **`pnpm sync-version`** — generation, not verification. It *writes* `docs/src/lib/site-meta.ts` @@ -90,6 +117,23 @@ it in passing rather than treating a mismatch as a failure. - **`pnpm --filter docs worker:deploy`** and any `wrangler deploy` — need `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`. Never run against production from a workstation; `worker-deploy.yml` owns it. +## Prepare a fresh tree + +```bash +pnpm install --frozen-lockfile +``` + +What makes a clean checkout of this repository runnable — and here it is the whole of it. **Gate 1 does +not run it**, which is why it sits below that line: an ordinary phase works in a tree that is already +prepared. `/feature-implement` runs it in each fresh checkout it makes for a round of phases. + +Verified by `/onboard` on 2026-10-18 in a `git clone --no-hardlinks` of this repository: after this one +command every gate section above exited 0, and `git status --porcelain` printed nothing. Nothing else is +needed — `pnpm build` is not a prerequisite of the tests, and the Worker's generated `docs-context.ts` is +aliased to a committed stub in `docs/vitest.config.ts`. `--frozen-lockfile` is the form that reads the +lockfile instead of rewriting it, the same one CI uses; plain `pnpm install` could dirty the tree and cost +the round. + ## Rules - **A missing entry is skipped, never faked.** An empty section above means there is no such step. Gate 1