@boopkit/cap-workflows 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/capability.json +20 -0
- package/package.json +18 -0
- package/skills/boop-architecture-audit-workflow/SKILL.md +199 -0
- package/skills/boop-design-handoff-workflow/SKILL.md +93 -0
- package/skills/boop-feature-dev-workflow/SKILL.md +404 -0
- package/skills/boop-issue-sweep-workflow/SKILL.md +176 -0
- package/skills/boop-knowledge-backfill/SKILL.md +19 -0
- package/skills/boop-light-dev-workflow/SKILL.md +104 -0
- package/skills/boop-refactor-dev-workflow/SKILL.md +111 -0
- package/skills/boop-test-audit-workflow/SKILL.md +152 -0
- package/skills/boop-website-dev-workflow/SKILL.md +195 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: boop-issue-sweep-workflow
|
|
3
|
+
description: "When the bd queue has accumulated a noisy backlog of lingering P1/P2 items — paper-cuts, small refactors, dead code, cosmetic bugs, taxonomy drift — and you want to clear them autonomously in a sequence that supports the week's goals, follow this process: inventory → cluster by theme → pick a cluster with the operator → run autonomously in one sweep worktree → hand off for a single UAT pass → log. The work lands in a worktree for one operator review rather than on main; it arrives with no decisions left in it."
|
|
4
|
+
metadata:
|
|
5
|
+
category: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Issue sweep workflow — clear lingering backlog noise without spending the operator's judgment.
|
|
9
|
+
|
|
10
|
+
The bd queue accumulates a long tail of lingering items: small refactors that didn't get done at the time, cosmetic bugs that nobody had cycles for, dead code that got noticed but not removed, taxonomy drift that someone filed and then moved on from. Each one is small. The cost is not the work; the cost is **carrying them in mind** — every `bd ready` listing them, every plan revisited with them open, every "should I do this now or later" decision. The mental load is the load.
|
|
11
|
+
|
|
12
|
+
Issue-sweep clears that load. The workflow's deliverable is **one sweep worktree carrying N verified fixes, ready for a single operator UAT pass, with no decisions left in it** — not main with the issues already merged and closed.
|
|
13
|
+
|
|
14
|
+
The load being cleared is **decision** load, not review load. The operator still looks at the result once, but they never have to answer a question to get there: N issues that each carried a "should I, and how?" collapse into one "does this look right?" That is a real reduction, and it is the only one the workflow promises. Anything that survives the run and grows the queue instead — a branch full of open questions, a findings doc to read, a follow-up issue to triage, a list of next steps to weigh — relocates noise from the bd list into the operator's review queue. That is not a reduction; it is a transfer.
|
|
15
|
+
|
|
16
|
+
## When to use this vs the other workflows
|
|
17
|
+
|
|
18
|
+
| Situation | Workflow |
|
|
19
|
+
|---|---|
|
|
20
|
+
| A backlog of lingering P1/P2 paper-cuts, small refactors, dead code, cosmetic fixes that the operator wants cleared in the background | **this** |
|
|
21
|
+
| A concrete improvement to one existing surface, framed as a goal, whose design the operator wants to weigh in on | `boop-light-dev-workflow` |
|
|
22
|
+
| A new feature with backend work driven by a spec | `boop-feature-dev-workflow` |
|
|
23
|
+
| A page UX delivery campaign with a finding budget | `boop-ux-app-ui-dev-workflow` |
|
|
24
|
+
|
|
25
|
+
The contrast with light-dev is no longer *where the work lands* — both end in a worktree the operator reviews. It is *what the operator has to bring to that review*. Light-dev hands back a change whose design the operator is expected to weigh in on. Issue-sweep hands back a batch of fixes that were only eligible because no weighing was needed, so the review is a look rather than a decision. If an issue requires operator judgment to ship — a copy choice, a design tradeoff, a fix path that isn't already named — it is not eligible for issue-sweep this pass, regardless of priority. The operator's judgment is exactly what the workflow exists to avoid spending.
|
|
26
|
+
|
|
27
|
+
## Authority granted on launch
|
|
28
|
+
|
|
29
|
+
When the operator invokes issue-sweep, they grant the agent authority — scoped to that run — to:
|
|
30
|
+
|
|
31
|
+
- Create the sweep branch and worktree, plus a separate worktree for any item that routes as its own light-dev run.
|
|
32
|
+
- Claim the issues it takes on, so they leave `bd ready` and stop presenting as open decisions.
|
|
33
|
+
- Commit, run tests, run the visual-check for UI changes.
|
|
34
|
+
- Close issues it finds were **already shipped** — those need no review, and the queue should shrink immediately.
|
|
35
|
+
- Delete branches and worktrees for items it abandons.
|
|
36
|
+
|
|
37
|
+
Two things the sweep may **not** do: merge to main, and close an issue as fixed before the operator has taken the worktree through UAT. Everything else stays on the worktree; issues it works stay **claimed and open** until sign-off. The audit trail is `git log`, the claimed/closed state in bd, and the append-only log file (below). This is a deliberate but bounded exception to the conservative-profile default in `CLAUDE.md`, scoped to the sweep run — a per-invocation grant, not a standing default. If the operator did not explicitly launch a sweep, none of this authority applies.
|
|
38
|
+
|
|
39
|
+
## Stages
|
|
40
|
+
|
|
41
|
+
### 1. Inventory
|
|
42
|
+
|
|
43
|
+
**First, check that no sweep is already outstanding.** One sweep worktree exists at a time. If a previous sweep is still sitting unmerged, stop and say so rather than starting a second — an unmerged worktree the operator hasn't taken is the same mental load as an open issue, only harder to see, and two of them are worse than the backlog they replaced. The operator decides whether to take the outstanding one through UAT, rebase it onto current main, or abandon it.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git worktree list
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then read the open queue at P1 and P2 across all types:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bd list --status=open --priority=1
|
|
53
|
+
bd list --status=open --priority=2
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If both tiers are empty, fall back to P3. Do not filter by `--type=bug`; the workflow covers anything the operator carries in their queue — refactors, dead code, docs drift, paper-cut bugs, taxonomy decisions whose answer is already known, scaffolds that need finishing.
|
|
57
|
+
|
|
58
|
+
**Then dedupe, before clustering.** The same problem gets filed repeatedly across sessions that could not see each other's work, and the duplicates rarely share wording — they share a *subject*: the same test, the same template, the same symbol, the same surface. Read the inventory for that subject rather than for matching titles.
|
|
59
|
+
|
|
60
|
+
Where several issues turn out to be the same thing, pick one as the survivor and note the others as its duplicates. They then travel together: one investigation, one fix, and all of them close together on merge. This is the cheapest win the workflow has — it closes N issues for the cost of one, and more to the point it prevents N separate investigations of the same problem.
|
|
61
|
+
|
|
62
|
+
### 2. Cluster by theme
|
|
63
|
+
|
|
64
|
+
Group the inventory by surface or system theme, not by priority. Theme is the cheaper sort because issues that share a surface often share investigation cost. A first pass on a backlog might yield clusters like *one surface's paper-cuts*, *a status-taxonomy refactor*, *a CSS cleanup*, *an upstream-package batch* — and a handful of standalone items that don't cluster with anything.
|
|
65
|
+
|
|
66
|
+
A cluster of one is fine. It means the item routes as a single light-dev-shaped run inside the sweep, not that it does not belong.
|
|
67
|
+
|
|
68
|
+
### 3. Pick a cluster with the operator
|
|
69
|
+
|
|
70
|
+
Propose one or two candidate clusters to the operator with a one-sentence rationale per candidate: how many items it carries, what surface it touches, whether it lines up with a named outcome in the project's active weekly next-steps doc, if one exists. The operator picks. **The picker is the operator, not the agent** — even though the rest of the workflow is autonomous, the selection step is where the operator's strategic judgment enters.
|
|
71
|
+
|
|
72
|
+
The project's active weekly next-steps doc (if one exists) is one input, not a hard filter. A cluster that doesn't serve a named outcome may still be the right pick (correctness, developer-experience, mental-noise reduction). Goal alignment is a tiebreaker between otherwise comparable clusters.
|
|
73
|
+
|
|
74
|
+
### 4. Decide the worktree shape
|
|
75
|
+
|
|
76
|
+
The default is **one sweep worktree carrying the whole cluster**. Independent fixes that merely share a surface still belong in it: they are independent to *implement*, but they get reviewed together, gated together, and handed off together, so splitting them buys nothing while costing a worktree apiece.
|
|
77
|
+
|
|
78
|
+
Split an item out into its own light-dev run and its own worktree only when it is genuinely larger than sweep-shaped — it needs its own design pass, its own review, or a UAT story that would be lost inside a batch. That is an escape hatch, not the common case. If more than an item or two in a cluster needs it, the cluster was mis-picked.
|
|
79
|
+
|
|
80
|
+
Create the worktree from a **freshly fetched main**, and copy any local env files into it rather than symlinking — a symlink into a sibling worktree dangles when that worktree is removed.
|
|
81
|
+
|
|
82
|
+
### 5. Run autonomously
|
|
83
|
+
|
|
84
|
+
For each issue in the cluster, work through the light-dev beats (`goal → investigate → design → critique → implement → verify`), with the additions below, specific to issue-sweep.
|
|
85
|
+
|
|
86
|
+
**The already-done check, before investigation.** Issues sometimes ship without being closed, or get partially addressed by unrelated work that touched the same code path. Before claiming an issue, look at the actual surface, the actual code, and the recent commits that touched it. Three outcomes:
|
|
87
|
+
|
|
88
|
+
- **Already shipped.** The work is fully done. Close the issue with a one-line note pointing at the commit(s) that did it (`bd close <id> --reason="shipped in <commit>"`), record it under *Closed as already-done* in the log, and move on. This is the one close that does not wait for UAT, because there is nothing to review.
|
|
89
|
+
- **Partially done.** Some of the work has shipped, some hasn't. If the remainder is itself autonomously fixable, finish it in the sweep worktree like any other item — it closes at merge, not now. If the remainder hides a judgment call, bail (same shape as the eligibility bail below) — leave a one-line note on the issue describing what's done and what's left.
|
|
90
|
+
- **Not started.** Proceed to the eligibility check.
|
|
91
|
+
|
|
92
|
+
This is cheap to run — one read of the relevant file or one `git log -- <path>` — and it prevents the worst paper-cut version of the workflow: an agent doing work that was already done, or re-litigating a decision the codebase already made.
|
|
93
|
+
|
|
94
|
+
**The eligibility check at investigate-time.** During investigation, ask explicitly: *does this fix require me to pick between alternatives?* If yes — multiple reasonable approaches exist, the spec is ambiguous, the data model has a question hiding in it, the copy choice is not obvious — bail.
|
|
95
|
+
|
|
96
|
+
The decision filter is the first gate, not the only one. Bail on any of the following too, regardless of how obvious the fix looks — each reaches past what a single unattended pass and a local gate can safely cover:
|
|
97
|
+
|
|
98
|
+
- **Changes the database schema.** Migrations are coordinated; a sweep is the wrong vehicle even for an obviously-correct column addition.
|
|
99
|
+
- **Has external side effects.** Anything that sends a message, calls a paid or third-party provider, touches real customer data, or deletes something not plainly dead.
|
|
100
|
+
- **Changes dependencies, secrets, or environment config.** These fail in ways a local gate won't catch.
|
|
101
|
+
- **Has unbounded blast radius.** See the stop-loss below.
|
|
102
|
+
|
|
103
|
+
**Bail behavior:**
|
|
104
|
+
|
|
105
|
+
- Add a one-line note to the bd issue via `bd update <id> --notes "issue-sweep skipped <YYYY-MM-DD>: needs decision on <X>"`. **Do not use `bd close` to record a bail, not even with `--reason="..."`.** A bail leaves the issue OPEN and visible in `bd ready`; closing it — even with a thoughtful close-reason — drops the issue out of the queue, which is the noise transfer the workflow exists to prevent. The bail note belongs in the issue's notes field; the issue's open status keeps it in the operator's review path.
|
|
106
|
+
- Leave the issue in its current status. Do not file a follow-up issue, do not write a findings doc, do not update memory.
|
|
107
|
+
- Move to the next item in the cluster.
|
|
108
|
+
|
|
109
|
+
The skipped issue stays in the operator's queue exactly where it was. The operator periodically reaps the skipped ones and either decides them, promotes to feature-dev, or hands them back to issue-sweep with more guidance.
|
|
110
|
+
|
|
111
|
+
**The stop-loss.** The eligibility check catches *this needs a decision*. It does not catch *this is simply bigger than it looked*. If a fix spreads past roughly three files, or its investigation keeps opening new ground, stop and bail (same shape as the eligibility bail) — it is a light-dev candidate that was mis-filed as a paper-cut. Sunk cost is the failure mode here; the twenty minutes already spent is not a reason to spend an hour more.
|
|
112
|
+
|
|
113
|
+
**The "did I just decide something" critique.** In the critique beat from light-dev, in addition to the stranger test and the omission check, run an explicit check on the implementation itself: *did I pick between alternatives anywhere in the fix?* If yes, the issue was not actually eligible and the autonomy filter missed it. Revert the work, unclaim the issue so it returns to `bd ready`, apply the bail behavior above, and skip. False negatives (bailing on something that was actually safe) cost nothing; false positives (auto-deciding something operator-altitude) cost trust.
|
|
114
|
+
|
|
115
|
+
**The verification gate before handoff.** A fix stays on the worktree only when:
|
|
116
|
+
|
|
117
|
+
- Tests pass (`npm test` for the targeted area, full suite before handoff).
|
|
118
|
+
- Build is green where relevant.
|
|
119
|
+
- For UI changes, the visual-check from `boop-web-web-ui-design` is run: build the CSS bundle, capture the affected page at desktop and mobile widths, read the captures. A green test suite is not evidence that the surface looks right; only the render is.
|
|
120
|
+
|
|
121
|
+
If verification fails, revert that fix and bail on its issue (same shape as the eligibility bail). Never hand back a red worktree, and never hold the whole batch hostage to one bad fix.
|
|
122
|
+
|
|
123
|
+
### 6. Hand off for UAT
|
|
124
|
+
|
|
125
|
+
Leave the sweep branch and worktree in place. Tell the operator what is in it: the branch name, the worktree path, the command to run the app against it, and the per-issue checklist from the log file below. The issues stay **claimed and open** until the operator has taken the worktree through UAT — claimed keeps them out of `bd ready`, so they stop presenting as decisions, and open keeps them honest, because nothing has shipped yet.
|
|
126
|
+
|
|
127
|
+
Verify the whole batch on the worktree before you call it ready: full suite green, the visual-check done for every UI change, each fix confirmed against its issue. Handoff is a single checkpoint — one look for the operator, no open questions attached.
|
|
128
|
+
|
|
129
|
+
After sign-off, the closeout is the usual one: merge the branch to main, close the shipped issues (`bd close <id1> <id2> ...`), then remove the worktree and delete the local branch. If the operator rejects one fix, revert its change, unclaim its issue with a one-line note, and merge the rest.
|
|
130
|
+
|
|
131
|
+
### 7. Log
|
|
132
|
+
|
|
133
|
+
Append a block to a sweep log file (e.g. `labs/issue-sweep/log.md`). Write it *during* the run rather than after, so it is ready at handoff: it doubles as the checklist the operator does UAT from and the record of what the sweep did.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
## <YYYY-MM-DD> — <cluster name>
|
|
137
|
+
|
|
138
|
+
Branch: <sweep branch>
|
|
139
|
+
Worktree: <path>
|
|
140
|
+
|
|
141
|
+
Shipped (UAT checklist):
|
|
142
|
+
- <id> — one-line outcome — look at: <page or surface, or "tests only">
|
|
143
|
+
- <id> — one-line outcome — look at: <…>
|
|
144
|
+
|
|
145
|
+
Closed as already-done:
|
|
146
|
+
- <id> — shipped in <commit>
|
|
147
|
+
|
|
148
|
+
Bailed:
|
|
149
|
+
- <id> — one-line reason (the decision needed, or the limit hit)
|
|
150
|
+
|
|
151
|
+
Learned:
|
|
152
|
+
- <pattern> → updated <skill-path> (only if a skill edit was warranted)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
No headers beyond the date+cluster line. No formatting ceremony. No calls to action. The "look at" note is what earns the file its keep at handoff: it is what turns N reviews into one pass. The audit trail is `git log` and the bd queue; the log is the human-readable summary the operator skims. If nothing was learned that warranted a skill edit, omit the Learned section.
|
|
156
|
+
|
|
157
|
+
If the sweep discovers a pattern that warrants a skill update, the skill update lives in the skill — not in the bd queue and not in the log body. The log just notes the pointer.
|
|
158
|
+
|
|
159
|
+
## Anti-patterns
|
|
160
|
+
|
|
161
|
+
- **Handing back a decision.** The worktree is a single batched checkpoint, not a queue of open questions. A findings doc to read, a follow-up issue to triage, a "which of these did you want?" — each means the eligibility filter failed, because the operator is now spending exactly the judgment the sweep existed to save. The win is clearing decision load: the operator does one look and never answers a question.
|
|
162
|
+
- **Merging to main yourself.** The sweep's authority ends at a verified worktree, and issues stay claimed-and-open until the operator has taken it through UAT. The one exception is an issue that turns out to have shipped before the sweep started: close that immediately, since there is nothing to review.
|
|
163
|
+
- **Starting a second sweep while one is unmerged.** An unmerged worktree the operator has not taken is the mental load the workflow was supposed to remove, wearing a disguise.
|
|
164
|
+
- **Investigating the same problem twice.** Duplicate filings are common and rarely share wording. Dedupe by subject at inventory, or pay the investigation cost once per filing.
|
|
165
|
+
- **Auto-deciding a judgment call to keep the cluster moving.** The autonomy filter is restrictive on purpose. Skipping an eligible-looking issue because it turned out to hide a design call costs nothing; shipping a design decision the operator hasn't made costs trust.
|
|
166
|
+
- **Filing follow-up bd issues during a sweep.** A sweep that closes three issues and files two is a wash, not a reduction. If a bail surfaces something worth tracking, the bail note on the original issue is the tracking. Skill edits go in the skill.
|
|
167
|
+
- **Filtering by type at inventory.** Most P1/P2 items aren't tagged `[bug]`. Refactors, dead code, taxonomy decisions whose answer is known, scaffolds that need finishing — all are eligible if they're autonomously fixable.
|
|
168
|
+
- **Letting goal alignment become a hard filter.** A P2 paper-cut that doesn't serve this week's named outcome is still a paper-cut. Alignment is a tiebreaker, not a gate.
|
|
169
|
+
- **Skipping the visual-check for UI work because tests pass.** The render is the proof. A green test suite proves the structure compiles; only the capture proves the surface still looks right after the fix.
|
|
170
|
+
- **Asking the operator mid-run.** The operator picked the cluster at stage 3; the rest of the run is autonomous. If a question arises that the operator would have to answer, that issue is not eligible — bail and move on, do not interrupt.
|
|
171
|
+
|
|
172
|
+
## Related
|
|
173
|
+
|
|
174
|
+
- [`boop-light-dev-workflow`](../boop-light-dev-workflow/SKILL.md) — the per-issue beat structure that issue-sweep uses inside its run stage. Each cluster item is a light-dev-shaped pass, with the eligibility filter and the shared hand-back worktree added on top. Both workflows hand back a worktree; the difference is whether the operator has to bring judgment to it.
|
|
175
|
+
- [`boop-web-web-ui-design`](../../../cap-web/skills/boop-web-web-ui-design/SKILL.md) — the source of the visual-check rule for UI changes.
|
|
176
|
+
- [`boop-writing-technical-prose`](../../../cap-product-design/skills/boop-writing-technical-prose/SKILL.md) — the translation test still applies to anything the agent writes (commit messages, log entries, bail notes).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: boop-knowledge-backfill
|
|
3
|
+
description: "When you need to integrate new knowledge from a set of specs or completed work that was not documented in reference at the time it shipped. This often happens when a feature is developed under a tight deadline and the team prioritizes shipping over documentation, or when work was done before the reference system was fully established."
|
|
4
|
+
metadata:
|
|
5
|
+
category: docs
|
|
6
|
+
---
|
|
7
|
+
# Knowledge Backfill
|
|
8
|
+
|
|
9
|
+
**a. Integrate into reference:**
|
|
10
|
+
- Follow the [reference integration skill](../../../cap-product-design/skills/boop-product-design-reference-integration/SKILL.md) to blend the new feature's knowledge into existing `reference/` pages.
|
|
11
|
+
- This is NOT "create a new page for the feature" — it's merging knowledge into the existing concept pages (and creating new concept pages only when a genuinely new concept has no home).
|
|
12
|
+
- Update cross-links between affected reference pages.
|
|
13
|
+
- Remove or update any reference content that is now outdated.
|
|
14
|
+
|
|
15
|
+
**b. Archive the spec:**
|
|
16
|
+
- Move the completed spec from `specs/` to `specs/history/` (create the directory if it doesn't exist).
|
|
17
|
+
- If any aspects of the spec were NOT implemented (deferred, descoped, or punted), extract those into a new revised spec in `specs/` that captures just the remaining work. Reference the archived spec for full context.
|
|
18
|
+
- The archived spec should be the full original — do not edit or trim it.
|
|
19
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: boop-light-dev-workflow
|
|
3
|
+
description: "When you are extending an existing system (front-end, back-end, or full-stack) toward a goal — wiring something new from data and patterns that already exist, not building greenfield — follow this grounded flow: goal → investigate the real system → design → critique → decompose into one scoped epic + child issues → run them → close out. Loop-drivable via boop loop --work --flow; lighter than feature-dev-workflow."
|
|
4
|
+
metadata:
|
|
5
|
+
category: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Light dev workflow — goal-grounded extension of an existing system.
|
|
9
|
+
|
|
10
|
+
When you are extending an existing system toward a goal — the entities, patterns, conditions, events, and call sites you need already exist, and you are wiring something new from them — follow this process. It is the lighter sibling of `boop-feature-dev-workflow`, and the difference is not just size.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
**Two beliefs frame everything below.**
|
|
15
|
+
|
|
16
|
+
**1. This extends what exists; it does not create something new.** `boop-feature-dev-workflow` is for greenfield — a new subsystem, a new data model, a new API, driven from a spec. This workflow is for the far more common case: the system already does something close, and you are wiring a new behavior out of parts that are already there (an existing close-condition, an existing dependency edge, an existing view-model, an existing event kind). So the work is *find and reuse*, not *invent*. The richest design decisions are "which existing seam do I extend" — and you can only answer that by looking at the real system, not at a summary of it.
|
|
17
|
+
|
|
18
|
+
**2. Going from a spec to issues is lossy, and the loss is avoidable.** `boop-feature-dev-workflow` splits its front half into separate sessions — understand, then plan, then decompose — each a fresh agent with zero prior context, reconstructing intent from the previous step's written artifact. Every one of those handoffs re-interprets a summary, and the spec was already a lossy summary of intent to begin with. This workflow collapses that chain into **one grounded session**: the same agent that investigates the real system also writes the child issues, so the decomposition is grounded in reality instead of transcribed from a spec. The spec or issue is an *input to grounding*, not a thing to faithfully re-serialize.
|
|
19
|
+
|
|
20
|
+
**The cost of collapsing the front half:** you lose the multi-session redundancy that sometimes catches a front-half miss. Two beats pay that back — the **critique** (step 4) is the in-session skeptic that must run before the epic is committed, and the **review/punch-list** in the back half (step 6) is where independent perspective lives. Neither is optional here; they are the relocated safety net, not ceremony.
|
|
21
|
+
|
|
22
|
+
Run the beats in order. Steps 2 and 4 are the load-bearing ones — skip them and you get a plausible epic grounded in nothing.
|
|
23
|
+
|
|
24
|
+
## The flow
|
|
25
|
+
|
|
26
|
+
### 1. Goal
|
|
27
|
+
State the job to be done in one sentence, in the user's words, from the input (a goal-framed spec or issue). If the input slipped into "add X, Y, Z," translate it back to *why* — the behavior the user wants to exist — and design against that. The goal anchors every later decision, including which existing seam to extend.
|
|
28
|
+
|
|
29
|
+
### 2. Investigate the real system (load-bearing)
|
|
30
|
+
Before designing anything, go look at the real thing first-hand. What "the real thing" is depends on the context — see [Front-end, back-end, full-stack](#front-end-back-end-full-stack-what-investigate-means) below. Across all three, the discipline is identical:
|
|
31
|
+
|
|
32
|
+
- **Find the existing parts you will extend**, not a place to add a parallel mechanism. The entity, the condition, the event, the dependency, the view-model, the call site — locate the real ones and read them.
|
|
33
|
+
- **Design from the real code and real data, never from the spec's summary or a remembered API.** The source almost always carries more than the summary named; the most goal-relevant part is often the one the summary forgot.
|
|
34
|
+
- **Name the pattern you will reuse.** If the system already closes a task on a condition, or already expresses a cross-node dependency, your new wiring should extend that pattern, not fork it.
|
|
35
|
+
|
|
36
|
+
Write down the inventory before moving on: the existing parts, the pattern you will extend, and the exact gap the new wiring fills.
|
|
37
|
+
|
|
38
|
+
### 3. Design
|
|
39
|
+
From the real materials, decide the **smallest wiring that serves the goal by reusing what exists**. Prefer extending an existing seam over standing up a parallel one. For UI surfaces, apply [`boop-ux-purpose-judgement`](../../../cap-ux/skills/boop-ux-purpose-judgement/SKILL.md) (does it carry what the job needs, in the right shape?) then [`boop-ux-visual-judgement`](../../../cap-ux/skills/boop-ux-visual-judgement/SKILL.md) (does it scan?). For back-end work, the equivalent question is *does this compose with the existing model or fork it* — a design that duplicates an existing mechanism is the back-end version of a database-dump. Favor the option that generalizes over a one-off.
|
|
40
|
+
|
|
41
|
+
### 4. Critique — before you decompose (the compensating control)
|
|
42
|
+
This is the only independent check before the epic is committed, so it carries the weight the missing multi-session redundancy would have. Run the design against the goal as a skeptic:
|
|
43
|
+
|
|
44
|
+
- **The end-to-end test.** Does this wiring actually achieve the goal on the *real* path, start to finish — not just in the one spot you were looking at?
|
|
45
|
+
- **The every-case check (critical for extend-existing).** What did *investigate* surface that this design does not handle? When you extend an existing condition or state, does the new wiring cover **every** case that condition already reaches, or only the happy one? (An extension that handles one terminal when the existing thing reaches three leaves the others stranded.)
|
|
46
|
+
- **The reuse check.** Are you reinventing a pattern that already exists? Extend, don't fork.
|
|
47
|
+
- **The scope check.** Is this the smallest wiring that serves the goal, or is it drifting into a redesign?
|
|
48
|
+
|
|
49
|
+
If the design fails, redesign now — it is far cheaper here than after an epic's worth of issues have been built against it.
|
|
50
|
+
|
|
51
|
+
### 5. Decompose into one scoped epic + child issues
|
|
52
|
+
Now swap the "implement" tail of a one-pass flow for a decomposition. In the **same session**, while grounded, emit **one bd epic** (an epic container is claim-safe) with child issues scoped to the actual wiring:
|
|
53
|
+
|
|
54
|
+
- **Ground each child in the real code** — name the files, call sites, conditions, and events from *investigate*. A child issue written by the agent that read the real system is a far less lossy input to its execution session than a spec would be.
|
|
55
|
+
- **Keep it proportional.** The epic is the real work plus its safety tasks — not a regrown four-session front half or a reflexive 13-issue tree. But it does need to *be* an epic: this workflow's floor is a change that genuinely decomposes into several grounded children. If the work is a one-line edit that needs no decomposition, you are in the wrong workflow — file a plain bd issue or just make the change; do not spin up this flow for it.
|
|
56
|
+
- **Size children to a session, not to a step.** A child issue is the unit a session picks up, and every session boundary is a cold start: the next agent re-reads the surface, re-derives the pattern, and re-runs the gates before it changes a character. So never split work that one session could carry. Group related small changes — a set of copy fixes on one template, several style corrections in one stylesheet — into a single child, and keep grouping until that child would take a session past roughly **50%** context. A one-line text change must never be its own child issue; that is a full session spent on a cold start to earn one line of diff.
|
|
57
|
+
- **Include a review task and a punch-list** as children — the relocated safety net. For engine/back-end extensions especially, the review must verify the new wiring is in the **live path** (not tests-only) and handles **every** existing case it touches.
|
|
58
|
+
- Use [`boop-project-mgmt-beads-planning`](../../../cap-project-mgmt/skills/boop-project-mgmt-beads-planning/SKILL.md) and [`boop-project-mgmt-engineering-planning`](../../../cap-project-mgmt/skills/boop-project-mgmt-engineering-planning/SKILL.md) to shape the epic.
|
|
59
|
+
- **Do not create the feature branch yourself.** This decompose step runs in the *launch directory* (the planning phase runs before any worktree exists), so a `git checkout -b work/<epic-id>` here parks the launch dir on the feature branch — which then collides with the executor's `git worktree add` ("already checked out") and, worse, strands a concurrent session's next commit on your feature branch. The executor owns the branch: `boop loop` creates the worktree and its `work/<epic-id>` branch when it picks up the first child. Just create the bd tree and stop.
|
|
60
|
+
|
|
61
|
+
The loop then picks up each child as its own execution session. That boundary is the context-budget seam, and it is fine *when the child is large enough to need one* — the children are grounded, so a fresh session can execute them. It is pure waste when the child is smaller than the cold start that precedes it, which is what the sizing rule above exists to prevent. It creates the worktree and feature branch itself; the decompose session above must not.
|
|
62
|
+
|
|
63
|
+
### 6. Run the epic — implement, review, punch-list
|
|
64
|
+
The loop implements each child ([`boop-coding-development`](../../../cap-coding/skills/boop-coding-development/SKILL.md), [`boop-testing-tdd-light`](../../../cap-testing/skills/boop-testing-tdd-light/SKILL.md)), then the review and punch-list run against the whole branch ([`boop-coding-review-changes`](../../../cap-coding/skills/boop-coding-review-changes/SKILL.md)). Do not treat these as leftover process — they are where the independent perspective the single front session lacked actually happens. Verify each child against its goal, not merely that it closed.
|
|
65
|
+
|
|
66
|
+
### 7. Verify and close out
|
|
67
|
+
- **Run the real path, not just the suite.** For UI, capture the render and re-critique against it. For back-end, drive the real end-to-end flow against a throwaway environment — a green suite proves structure compiles, not that the goal is served.
|
|
68
|
+
- Run the targeted tests for what you touched, and the full suite before closeout.
|
|
69
|
+
- Integrate knowledge into `reference/` ([`boop-product-design-reference-integration`](../../../cap-product-design/skills/boop-product-design-reference-integration/SKILL.md)) and archive anything the change completed.
|
|
70
|
+
- Leave the branch for review. **Do not merge to main** — the human owns the merge.
|
|
71
|
+
|
|
72
|
+
## Front-end, back-end, full-stack: what "investigate" means
|
|
73
|
+
|
|
74
|
+
Same principle (ground in the real thing, first-hand), different instruments:
|
|
75
|
+
|
|
76
|
+
| Context | Look at the real… | With |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **Front-end / surface** | record behind the view, controller/view-model, template + partials, rendered HTML, route, what each affordance fires | the project's data query tool for a real record; read the render, never just the view-model (it shows only today's impoverished subset) |
|
|
79
|
+
| **Back-end / data / engine** | code path end-to-end, the entities/conditions/events/dependencies you will extend, real records, the existing pattern to reuse | the project's data query tool for real state; trace the real event/data flow; **run** the real path, don't reason about it from memory |
|
|
80
|
+
| **Full-stack** | both of the above **and the seam between them** — the request/response, the signal, the shared type | do both investigations, then trace what crosses the boundary |
|
|
81
|
+
|
|
82
|
+
The trap is symmetric across all three: designing from the summary (the issue, the current view-model, a remembered API shape) instead of the real system. Go look.
|
|
83
|
+
|
|
84
|
+
## When to use this vs the alternatives
|
|
85
|
+
|
|
86
|
+
| Situation | Workflow |
|
|
87
|
+
|---|---|
|
|
88
|
+
| Extending an existing system toward a goal — FE, BE, or full-stack — wiring something new from existing parts | **this** |
|
|
89
|
+
| Greenfield: a new subsystem, data model, or API driven from a spec | [`boop-feature-dev-workflow`](../boop-feature-dev-workflow/SKILL.md) |
|
|
90
|
+
| An open-ended "make this page better / find what's wrong" eval that should grow the judgement skill | [`boop-ux-eval-and-refine-workflow`](../../../cap-ux/skills/boop-ux-eval-and-refine-workflow/SKILL.md) |
|
|
91
|
+
| A page UX delivery campaign with a finding budget and per-finding decomposition | [`boop-ux-app-ui-dev-workflow`](../../../cap-ux/skills/boop-ux-app-ui-dev-workflow/SKILL.md) |
|
|
92
|
+
| A noisy backlog of small lingering items to clear in a themed sweep | [`boop-issue-sweep-workflow`](../boop-issue-sweep-workflow/SKILL.md) |
|
|
93
|
+
|
|
94
|
+
**Bounded on both ends.** *Above:* if *investigate* or *design* reveals the work is actually greenfield — a genuinely new subsystem, a real data-model design, no existing pattern to extend — it has left this workflow's domain; say so and escalate to `boop-feature-dev-workflow` rather than forcing a redesign through a single grounded session. *Below:* if the change is a one-liner or a lone edit that does not decompose into several grounded issues, it is beneath this workflow — a plain bd issue or a direct fix is the right tool, not an epic.
|
|
95
|
+
|
|
96
|
+
## Anti-patterns
|
|
97
|
+
|
|
98
|
+
- **Designing from the spec/issue summary, the current view-model, or a remembered API — instead of the real system.** The summary is lossy and the view-model is the impoverished status quo; both hide the part that matters. *Go look.*
|
|
99
|
+
- **Forking a parallel mechanism when an existing seam already does the job.** Extend-existing means extending — reinventing an existing pattern is the failure this workflow exists to prevent.
|
|
100
|
+
- **Handling only the happy case of an existing condition you extend.** If the thing you are wiring into already reaches several states/terminals, the extension must cover all of them, or it strands the ones it forgot.
|
|
101
|
+
- **Skipping the critique.** It is the single front-half check; a one-session flow has no other redundancy before the epic is committed.
|
|
102
|
+
- **Ceremony inflation.** Regrowing feature-dev's separate understand/plan/decompose sessions, or a reflexive large issue tree, throws away the entire reason to use this. The epic is scoped to the wiring.
|
|
103
|
+
- **Verifying against the diff or the suite instead of the real path/render.** Tests prove structure; only the real path proves the goal is served.
|
|
104
|
+
- **Wiring the new behavior in tests only, never in the live code path.**
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: boop-refactor-dev-workflow
|
|
3
|
+
description: "When you need to change a contract, cardinality, or shape that existing code already depends on — usually to make a feature buildable — follow this workflow instead of the feature workflow."
|
|
4
|
+
metadata:
|
|
5
|
+
category: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Workflow for refactoring to enable a feature
|
|
9
|
+
|
|
10
|
+
When you need to change something existing code already depends on — a relationship's cardinality, a function's contract, a field's meaning — follow this workflow. It is a **peer of** [boop-feature-dev-workflow](../boop-feature-dev-workflow/SKILL.md), not a variant of it, and using the feature workflow for this job is a known way to fail.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
**A refactor is defined by what already calls the thing you are changing. So the completeness guarantee has to come from the compiler, not from anyone's diligence — and that means the build must be allowed to go red.**
|
|
15
|
+
|
|
16
|
+
This is the opposite instinct from feature work, and the difference is worth stating plainly:
|
|
17
|
+
|
|
18
|
+
| | Feature development | Refactor |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| The work is | capability that does not exist yet | every existing caller of a contract you are changing |
|
|
21
|
+
| Progress looks like | vertical slices, each demonstrable | concept groups, none user-visible |
|
|
22
|
+
| A green build means | the slice works | **possibly that you have not deleted enough** |
|
|
23
|
+
| Completeness comes from | the Done-when and a demo | the compiler, a mechanical scan, and the suite |
|
|
24
|
+
|
|
25
|
+
The central failure mode: **keeping the build green throughout a refactor is only possible by leaving a fallback** — a scalar, an optional-handle-with-default, a first-of-set pick — that silently absorbs the callers you have not converted yet. Every "additive, expand-contract, drop the old thing last" plan re-commits that sin, which is why it keeps producing refactors that look finished and leave an undetected tail.
|
|
26
|
+
|
|
27
|
+
## When this workflow applies
|
|
28
|
+
|
|
29
|
+
- Changing a relationship's cardinality (one-to-one becoming one-to-many).
|
|
30
|
+
- Making an optional parameter required, or deleting a fallback path.
|
|
31
|
+
- Replacing a resolver, a read, or a data shape that many call sites use.
|
|
32
|
+
- Any change where the honest answer to "how many places does this touch?" is "whatever the compiler says."
|
|
33
|
+
|
|
34
|
+
**When it does not apply:** adding new capability (use the feature workflow), or a change contained to one file with a handful of callers you can see at once. This workflow's overhead only pays when the call sites are too many to hold in your head — which is exactly when diligence-based approaches fail.
|
|
35
|
+
|
|
36
|
+
## The workflow
|
|
37
|
+
|
|
38
|
+
### 1. Establish the target vocabulary first, grounded in the code
|
|
39
|
+
|
|
40
|
+
Before changing anything, audit the consumers and produce a **concept-to-primitive map**: for each thing being replaced, what replaces it, and which callers route to which successor. This is a reviewable contract, and it is worth an operator's sign-off before any code changes.
|
|
41
|
+
|
|
42
|
+
Do not mechanically apply a transformation. **Verify each thing's true shape against the code** — a mechanical "every scalar becomes a set" will convert fields that are genuinely singular, which is its own defect. Three verdicts cover most cases:
|
|
43
|
+
|
|
44
|
+
- **Genuinely one-to-many** — hold and return the **set**.
|
|
45
|
+
- **Genuinely singular, but reached by the wrong handle** — require the specific handle; never resolve it from the container.
|
|
46
|
+
- **A distinct concept in disguise** — give it its own name. One field serving two concepts is common, and splitting it is often the real finding.
|
|
47
|
+
|
|
48
|
+
Ground the vocabulary in the domain, not in the code's current names. If the code calls two different things by one name, the map is where that gets caught.
|
|
49
|
+
|
|
50
|
+
### 2. Decompose by concept group, not by call site
|
|
51
|
+
|
|
52
|
+
One task per concept, ordered so that later concepts build on earlier ones. Enumerate-and-patch — a task per call site, each threading a handle through — always leaves a tail, because the list of sites is exactly the thing you cannot trust yourself to have enumerated.
|
|
53
|
+
|
|
54
|
+
### 3. Delete the escape hatch first, and commit the red build
|
|
55
|
+
|
|
56
|
+
For each concept group: **delete the old resolver, scalar, or fallback before writing the replacement's callers.** The build goes red. Commit that as its own step — the red is the *proof* the hatch is gone, and the compiler's error list is now the honest, complete work-list.
|
|
57
|
+
|
|
58
|
+
If the build is still green after the deletion, a surviving hatch is absorbing the callers. You did not delete enough.
|
|
59
|
+
|
|
60
|
+
### 4. Follow the compiler to green
|
|
61
|
+
|
|
62
|
+
Fix each site by giving it the handle it needs, reading the set, or — only at a genuine display boundary — an explicit **commented reduction** (`set[0]` with a comment saying it is a render choice). Never inside a reusable resolver, where a write path can reach it.
|
|
63
|
+
|
|
64
|
+
**A fallback is never correct.** Not a defaulted handle, not `?? oldThing`, not a first-of-set inside a resolver. If a call site genuinely cannot supply the handle, that is a **finding to surface**, not a fallback to invent. Green is the *end state*, never a state maintained throughout.
|
|
65
|
+
|
|
66
|
+
### 5. Scan for what the types cannot see
|
|
67
|
+
|
|
68
|
+
A read hidden behind an inline cast (`as { field?: ... }`) re-declares the deleted field locally and compiles fine. After reaching green, grep for the cast pattern and for the deleted name in strings and comments. Also check loosely-typed call sites (`any`-typed blobs, raw queries, string-built SQL) — the compiler has no opinion there.
|
|
69
|
+
|
|
70
|
+
### 6. Run the full suite — it finds more than the compiler can
|
|
71
|
+
|
|
72
|
+
Two whole classes of breakage are invisible to a type-check of the source:
|
|
73
|
+
|
|
74
|
+
- **Fixtures** using the deleted shape, when tests are not in the source's type-check scope. Enumerate them with a widened check and migrate them forward.
|
|
75
|
+
- **Writer-side and runtime gaps.** The compiler cannot see a *writer* that fails to populate the new field, or a value that now lands somewhere a reader does not look. Only the running suite does.
|
|
76
|
+
|
|
77
|
+
So the loop is always: **go red → follow the compiler → scan for casts → run the full suite → fix the writer gaps → repeat until green.** A green type-check is a checkpoint, not the finish line.
|
|
78
|
+
|
|
79
|
+
### 7. Prove it structurally, then hand off
|
|
80
|
+
|
|
81
|
+
Finish with evidence that does not depend on anyone's care:
|
|
82
|
+
|
|
83
|
+
- The **mechanical scan** for every deleted pattern returns zero outside explicitly-commented display reductions.
|
|
84
|
+
- A **real-path test at N greater than one** drives the actual surface to distinct outcomes — not a unit test of the new primitive, but the whole path, because concurrency and wiring defects only appear there.
|
|
85
|
+
- The **full suite** is green.
|
|
86
|
+
- Fold what changed into the reference docs, and leave the merge decision to the operator.
|
|
87
|
+
|
|
88
|
+
## Principles that carry across the whole workflow
|
|
89
|
+
|
|
90
|
+
**Make the mistake unexpressible rather than remembering not to make it.** When you catch yourself planning to "scan for this afterwards" or "be careful not to," ask whether the thing can be deleted so it cannot be written. Auditing is diligence; deletion is structure. This applies past the source: test harnesses and helpers deserve the same treatment, since a helper that picks an arbitrary item makes every test built on it silently unreliable.
|
|
91
|
+
|
|
92
|
+
**Verify the guarantee mechanism itself works.** "The compiler enforces this" is a claim worth testing. A type-check config that does not actually compile the files you think it does has been silently enforcing nothing.
|
|
93
|
+
|
|
94
|
+
**Prefer three over two when proving a set.** Two is the weakest multi case: a first-of-set bug survives it whenever the arbitrary pick happens to match what the assertion reads. Use three, and assert against the middle one.
|
|
95
|
+
|
|
96
|
+
**Assert by name and by count.** Naming catches something missing; counting catches something doubled. A set-equality assertion reads identically whether an item was written once or twice.
|
|
97
|
+
|
|
98
|
+
## Running this unattended
|
|
99
|
+
|
|
100
|
+
If the work is executed by autonomous agents, put **the method itself into each task's notes** — not just the task's scope. An agent picking up "make this field required" without the go-red discipline will keep the build green, which is the failure this workflow exists to prevent. Carrying the method on every task is what makes an unattended run trustworthy, and it is cheap: the same block on each issue, with a task-specific section.
|
|
101
|
+
|
|
102
|
+
## Relationship to the feature workflow
|
|
103
|
+
|
|
104
|
+
A refactor of this kind usually exists to **make a feature buildable**. It ships no user-visible behaviour, so it has no demo and no vertical slices, and judging it by those produces the wrong plan. Run it to completion first; the feature that follows then builds on a foundation where the old mistake is no longer expressible.
|
|
105
|
+
|
|
106
|
+
## Related
|
|
107
|
+
|
|
108
|
+
- [boop-feature-dev-workflow](../boop-feature-dev-workflow/SKILL.md) — the peer workflow, for building capability.
|
|
109
|
+
- [boop-product-design-engineering-design](../../../cap-product-design/skills/boop-product-design-engineering-design/SKILL.md) — the concept map in step 1 is a design artifact; the current-state audit guidance applies.
|
|
110
|
+
- [boop-testing-testing](../../../cap-testing/skills/boop-testing-testing/SKILL.md) — for the real-path proof in step 7.
|
|
111
|
+
- [boop-coding-review-changes](../../../cap-coding/skills/boop-coding-review-changes/SKILL.md) — the scan in step 5 belongs in review too.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: boop-test-audit-workflow
|
|
3
|
+
description: "Use when a set of tests is not testing the purpose of the code: mocks that verify data the test itself fed in, tests that exercise the platform rather than our code, or a stubbed seam where the integration is the actual risk. Also use when tests are so coupled to the shape of the implementation that they check a label, an exact string, or a call sequence rather than a behaviour. This workflow removes tests that cannot go red for a real reason, rewrites the ones that check a real goal at the wrong level, merges duplicates, and does not lose coverage. It explicitly does not target good tests merely because a coming change will make them need updating. The sibling of light-dev-workflow for reworking tests rather than building features."
|
|
4
|
+
metadata:
|
|
5
|
+
category: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Test audit workflow — cut the tests that cannot go red, and re-level the rest.
|
|
9
|
+
|
|
10
|
+
When you need to make a set of tests earn its keep — because the tests around a surface do not actually exercise the purpose of the code, or because they are written so tightly against the current implementation that a behaviour-preserving change breaks them — follow this process. It removes tests that have no real failure mode, rewrites the ones that protect a real goal at the wrong level, and merges duplicates, without losing coverage. It is the sibling of [boop-light-dev-workflow](../boop-light-dev-workflow/SKILL.md) for the opposite direction of work: that workflow builds something new, this one removes and rewrites, and removing needs a safeguard that building does not.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
Three beliefs shape every decision below.
|
|
15
|
+
|
|
16
|
+
**First: a test earns its place by being able to go red when the code stops doing its job.** That is the property to audit for. It is not the same as being stable, being fast, or being green — a test that can never go red for a real reason is perfectly stable and completely worthless, and it costs a real amount to carry, read, and forward-update. The number of tests is not the thing to optimise either: an agent told to "cut the number of tests" will happily delete the one test that catches a real bug. Removal is a side effect of the audit, not its target.
|
|
17
|
+
|
|
18
|
+
**Second: needing to be updated is not a defect.** A test that checks a real contract at a real boundary, and that has to change when that contract genuinely changes, is doing its job. Updating it is the price of the coverage, not waste. Do not put a test on the trim list because a coming change will make it need editing, and do not treat a wave of tests that a refactor just turned red as the target list — a useful test going red is the system working. The tests to remove are the ones that would have stayed green through the same refactor *and* through the bug it might have introduced.
|
|
19
|
+
|
|
20
|
+
**Third: removing a test is destructive, so classify before you cut.** A build workflow only adds, so a mistake shows up as a failing test. This workflow removes, and a deleted invariant test fails silently, because nothing tells you the bug it used to catch now slips through. So the plan for what to keep, rewrite, remove, and merge has to be written down and reviewed before anything is deleted, and every invariant you set out to protect has to still have a test checking it at the end.
|
|
21
|
+
|
|
22
|
+
## The two questions (the judgement this workflow turns on)
|
|
23
|
+
|
|
24
|
+
Ask both of every test in scope, in this order. Do not ask whether the test "would still be true" after a rewrite — that phrasing is satisfied vacuously by the worst tests in the suite, since a test that asserts nothing real is true no matter what the code does. Ask instead for a location, because a location cannot be bluffed:
|
|
25
|
+
|
|
26
|
+
> **Where would a bug have to live for this test to go red?**
|
|
27
|
+
|
|
28
|
+
**Question 1 — could it go red for a real reason?** Name the wrong version of the production code that turns this test red. If the only way to make it fail is to edit the test's own setup, or to break somebody else's library, then the test is not exercising our code and it is dead weight. Answering this well means pointing at a file and a behaviour, not gesturing at a concept.
|
|
29
|
+
|
|
30
|
+
**Question 2 — could it go red for a fake one?** Name the *correct* production code that turns this test red. If a restructure that preserves the behaviour would break it, the test is over-specified and states its check in terms of the mechanism. That is grounds for a rewrite and never for a removal on its own.
|
|
31
|
+
|
|
32
|
+
A test can fail both, fail one, or pass both. Failing question 1 is the disqualifier: a test with no real failure mode goes on the trim list however clean or stable it looks. Failing only question 2 means re-level it, not delete it.
|
|
33
|
+
|
|
34
|
+
### The patterns that fail question 1
|
|
35
|
+
|
|
36
|
+
These are the recurring shapes. They are worth naming because they are *generated by an incentive*, not by carelessness, which is why they keep coming back. When the proximate goal is a green test, stubbing the collaborator is the fastest route to green, and the result is green forever — and nothing in that feedback loop distinguishes "green because the code works" from "green because nothing real ran." Expect to find these, including in tests you wrote yourself.
|
|
37
|
+
|
|
38
|
+
- **Mock theater.** The assertion checks data the test itself fed in through a mock. The arrange step already contains the answer the assert step looks for, so what is under test is the wiring of the mock rather than the code.
|
|
39
|
+
- **Testing the platform.** Asserting that the ORM saves a row, the filesystem writes a file, the framework routes a request, or the parser parses. That is someone else's code and they already test it.
|
|
40
|
+
- **The stubbed seam.** The collaborator whose integration is the actual risk has been replaced by a stub, so the one thing likely to break is the one thing not exercised. Note the distinction that matters here: mocking a genuinely external boundary — a model provider, an email or SMS gateway, a queue — is legitimate and is not this pattern. Mocking one of our own services to avoid standing up the real one is.
|
|
41
|
+
- **The tautological assertion.** The test asserts a literal that the test or the template already fixes: exact user-facing copy, a label, a string echoed back unchanged.
|
|
42
|
+
|
|
43
|
+
### What the two questions cannot see
|
|
44
|
+
|
|
45
|
+
Both questions take an existing test as input, so they judge the tests in front of you and say nothing about the shape of the population. Hold these blind spots in mind, and treat step 2's invariant list as the place they get caught:
|
|
46
|
+
|
|
47
|
+
- **A behaviour with no test at all scores perfectly.** Nothing fails question 1 if nothing is there. This is why the invariant list is built from the spec and the code's purpose rather than from the current tests.
|
|
48
|
+
- **An uncovered seam is invisible.** The stubbed-seam pattern catches a seam that is *falsely* covered, because a stub is there to notice. A seam nobody ever wrote a test for leaves no trace. A suite can be all-unit plus all-e2e, with the integration where things actually break untested, and every test in it passes both questions.
|
|
49
|
+
- **A symbol search cannot see coverage that arrives through an outer boundary.** When you check whether a seam is covered, the obvious move is to grep for its name — the function, the class, the module path — and read whatever comes back. That finds only the tests that *import* the thing. A test that drives the same code through HTTP, a queue, a CLI, or a rendered page names the route and never the symbol, so it leaves no textual trace, and zero hits reads as zero coverage. This produces the most damaging verdict this workflow can issue: a confident "nothing anywhere covers this" that is false, and that the next reader will act on. Before declaring a seam uncovered, search for the behaviour as well as the name — the URL or route, the queue topic, the command, the fixture — and sweep the directories where end-to-end tests live even when the step-1 scope table did not list them. A negative result is a claim about the whole suite, so it needs a search wide enough to justify one.
|
|
50
|
+
- **Redundancy across levels survives.** A test can go red for a real reason and still be the fifth thing that catches the same bug.
|
|
51
|
+
- **Total cost is invisible.** A large suite of individually good tests can still be too slow to run.
|
|
52
|
+
- **Flakiness falls between the questions.** Question 2 asks what correct code turns a test red, not what nondeterminism does. Shared fixtures, ordering dependence, and parallel workers against one database are real defects neither question names.
|
|
53
|
+
|
|
54
|
+
When the scope has any of these, add a per-surface pass alongside the per-test one: for each invariant, list which levels test it, and check that the level where it would actually break is among them. That pass is the only one that can see an absence.
|
|
55
|
+
|
|
56
|
+
### What is not a target, however tempting
|
|
57
|
+
|
|
58
|
+
- A test that checks a real contract at a real boundary and will need updating when that contract changes.
|
|
59
|
+
- A boundary mock of a genuinely external service.
|
|
60
|
+
- A narrow test that pins a real edge case, even if it looks trivial next to its neighbours.
|
|
61
|
+
- A test that a refactor just turned red. Red is not evidence of uselessness.
|
|
62
|
+
|
|
63
|
+
## The flow
|
|
64
|
+
|
|
65
|
+
### 1. State the goal and scope
|
|
66
|
+
Say which set of tests you are auditing and why. Keep the scope tight: audit the tests covering the surface or change at hand, not the whole repository. A scope that is too wide turns a cheap cleanup into an open-ended rewrite. Name the test files or suites in scope, and for what is out of scope state the rule that draws the line rather than listing files.
|
|
67
|
+
|
|
68
|
+
A cheap mechanical pass sharpens the scope before any reading: inventory what the tests in scope mock, and split it into genuinely external boundaries and our own modules. The second list is where the stubbed-seam pattern concentrates. Treat it as a reading list, not a verdict — some internal mocks exist for good reasons, such as avoiding the boot of a heavy subsystem.
|
|
69
|
+
|
|
70
|
+
### 2. List the invariants to protect (do this before touching any test)
|
|
71
|
+
Before changing a single test, write down the goals, invariants, behaviours, and contracts this set of tests has to keep checking. This list is what every later step is measured against, and at the end you confirm each item still has a test.
|
|
72
|
+
|
|
73
|
+
An invariant is something that has to stay true for the user or the system no matter how the code is written:
|
|
74
|
+
|
|
75
|
+
- **Good — states an outcome that must hold:** "an item the operator selects ends up in the selected state"; "no notification goes out without the operator's approval"; "submitting the form twice creates one record, not two."
|
|
76
|
+
- **Not an invariant — restates the test's own setup or the current shape:** "the button reads 'Select {name}'"; "the service calls `repo.save()` exactly once"; "the mocked accounts service returned the account the test told it to return."
|
|
77
|
+
|
|
78
|
+
One case needs care. A status code, a wire format, or an error shape is a real contract when it is the system's promise to a caller, and a test that checks it is checking purpose. It is an echo only when the test uses it as a stand-in for behaviour it never actually verifies. Decide by asking question 1, not by the category of the thing being asserted.
|
|
79
|
+
|
|
80
|
+
Take the invariants from the spec, the reference docs, and what the code is meant to do — not from the current tests, since the tests are the thing you are auditing and may already be checking the wrong thing.
|
|
81
|
+
|
|
82
|
+
### 3. Classify each test into a written trim list, reviewed before anything is deleted
|
|
83
|
+
For each test in scope, ask the two questions, name the invariant or behaviour it protects, and sort it into one of four groups:
|
|
84
|
+
|
|
85
|
+
- **Keep** — it can go red for a real reason, and only for real reasons.
|
|
86
|
+
- **Rewrite** — it protects a real invariant but fails question 1 or question 2. If it fails question 2 it is stated against the mechanism, and re-leveling is an edit. If it fails question 1 the rewrite is a promotion: standing up the real collaborator, the real database, or the real pair, so that the seam is genuinely exercised. Budget for that; it costs more than an edit.
|
|
87
|
+
- **Remove** — it fails question 1, and the invariant behind it, if any, is already covered by a keep-or-rewrite test elsewhere.
|
|
88
|
+
- **Merge** — it duplicates another test; fold the two together.
|
|
89
|
+
|
|
90
|
+
Write this out as a trim list and have it reviewed before deleting anything. Then check it against the invariant list from step 2: every invariant has to be covered by at least one keep-or-rewrite test. If an invariant has no other test covering it, you cannot remove its only test — rewrite it instead, even when the existing test is pure mock theater, since the invariant is real even though the test is not.
|
|
91
|
+
|
|
92
|
+
### 4. Critique the trim list before running it
|
|
93
|
+
Read the trim list back as a skeptic, against the invariant list:
|
|
94
|
+
|
|
95
|
+
- **Coverage.** Does every invariant from step 2 still have a test after the trims? A missing one stops the plan.
|
|
96
|
+
- **Failure mode.** For each "keep", can you still name the wrong production code that turns it red? If the answer got vague on a second reading, it belongs in rewrite. Watch for the answer that names a refactor nobody has performed — "the only thing this guards is a future rewrite to X." That is a hypothesis, and it is cheap to check: make the change locally and see what goes red. The claim it has to survive is not that the test goes red, but that it goes red *alone*. A guard that fails alongside forty ordinary tests is not guarding anything.
|
|
97
|
+
- **Over-cutting.** Is anything on the remove list there only because it will need updating, or only because a refactor broke it? Those are keeps.
|
|
98
|
+
- **Echo.** Is anything marked remove or merge actually the only test covering a real case or edge? If so it is a rewrite, not a remove.
|
|
99
|
+
- **Shape.** For each invariant, which levels test it, and is the level where it would actually break among them? The two questions cannot see an absence, so this is where a missing seam test gets caught.
|
|
100
|
+
- **Scope.** Is the plan wandering past the tests you scoped in step 1 into a general rewrite?
|
|
101
|
+
|
|
102
|
+
Fix the plan here. It is far cheaper than fixing it after an epic of issues has been written against it.
|
|
103
|
+
|
|
104
|
+
### 5. Break the work into one epic and a few coarse child issues
|
|
105
|
+
In the same session, while the classification is fresh, create one bd epic (an epic cannot be claimed as work itself, so it is a safe container) with child issues scoped to the trim list.
|
|
106
|
+
|
|
107
|
+
**Keep the child count low, and group coarsely — at the suite level or higher.** Every child issue is a session for whoever runs it, and a fresh session reloads its context from scratch. Many small children multiply that cost and lose context between them. Prefer a few coarse issues, split by area or by suite rather than by file or by kind of change. Keep a removal together with the rewrite that covers its invariant, so one session cannot delete an old test before its replacement exists. Add one issue that confirms coverage and one that reviews the result. Ground each issue in the real test files it touches, and use [boop-project-mgmt-beads-planning](../../../cap-project-mgmt/skills/boop-project-mgmt-beads-planning/SKILL.md) to shape the tree.
|
|
108
|
+
|
|
109
|
+
Then create the bd tree and stop. Do not run the work yourself, and do not create the feature branch: the loop creates the worktree and its `work/<epic-id>` branch when it takes the first child. Creating the branch here collides with that and can strand commits.
|
|
110
|
+
|
|
111
|
+
### 6. How the children are executed (the loop runs them; these are the rules each one carries)
|
|
112
|
+
Hand the finished tree to `boop loop --prefix <epic-id> --run`. Prefer `--reuse-session` for this workflow: every child has to re-read the invariant list and the trim list to make a correct keep-or-cut call, and carrying one session across the cluster is exactly the context a fresh-session-per-issue run would reload each time.
|
|
113
|
+
|
|
114
|
+
Write these rules into the child issues so each executing session follows them:
|
|
115
|
+
|
|
116
|
+
- **The suite passes before and after.** This is a change to the tests, not to behaviour, so the tests in scope stay green throughout, and the only tests that stop existing are the ones the trim list marked for removal.
|
|
117
|
+
- **Run tests per file or per small group, not the whole suite on each pass.** Re-running a large suite on every attempt is the biggest avoidable cost of this kind of work.
|
|
118
|
+
- **Change only tests.** Fixtures, factories, and test harness code count as tests and may change, which is what makes a promotion to a real-collaborator test possible. Production behaviour does not. If a rewrite uncovers a real bug — and a promotion often will, since it exercises a seam nothing exercised before — write a separate issue for it rather than fixing it in place.
|
|
119
|
+
|
|
120
|
+
The review child then reads the whole branch and checks each change against the invariant it was meant to protect, not merely that the issue closed.
|
|
121
|
+
|
|
122
|
+
### 7. Confirm coverage and close out
|
|
123
|
+
This is the work of the coverage and review children, and the loop's own closeout:
|
|
124
|
+
|
|
125
|
+
- **Coverage.** Confirm that every invariant from step 2 still has a test, by name — not just that the suite is green, since a suite can pass while an invariant quietly goes uncovered.
|
|
126
|
+
- The tests in scope pass.
|
|
127
|
+
- **A short before-and-after note** records what was rewritten, removed, and merged, and names the patterns found. This keeps the reduction legible and makes the level you re-set the standard that new tests on this surface follow.
|
|
128
|
+
- Fold any lasting standard into the reference docs. Leave the branch for review, and do not merge to main.
|
|
129
|
+
|
|
130
|
+
## When to use this instead of a related workflow
|
|
131
|
+
|
|
132
|
+
| Situation | Workflow |
|
|
133
|
+
|---|---|
|
|
134
|
+
| Remove tests that do not exercise the purpose of the code, and re-level the ones written against the mechanism, without losing coverage | **this** |
|
|
135
|
+
| Extend an existing system toward a goal, wiring new behaviour from parts that already exist | [boop-light-dev-workflow](../boop-light-dev-workflow/SKILL.md) |
|
|
136
|
+
| Build a genuinely new subsystem, data model, or API from a spec | [boop-feature-dev-workflow](../boop-feature-dev-workflow/SKILL.md) |
|
|
137
|
+
| Clear a noisy backlog of small lingering items in a themed sweep | [boop-issue-sweep-workflow](../boop-issue-sweep-workflow/SKILL.md) |
|
|
138
|
+
|
|
139
|
+
If the set of tests is small enough that the audit is a handful of edits with no real decomposition, skip the epic and do the rewrite under a plain bd issue. This workflow is for a surface big enough that classifying first, then executing, earns its keep.
|
|
140
|
+
|
|
141
|
+
## Mistakes to avoid
|
|
142
|
+
|
|
143
|
+
- **Cutting a good test because it will need updating.** Needing an edit when a contract changes is the cost of coverage. Only an absent failure mode justifies removal.
|
|
144
|
+
- **Treating the tests a refactor just broke as the trim list.** That set is selected for sensitivity, which is the property you want to keep.
|
|
145
|
+
- **Optimising the number of tests.** Deleting to hit a target is how the one test that catches a real bug gets deleted.
|
|
146
|
+
- **Removing an invariant's only test.** Every invariant on the step-2 list keeps a test, with no exceptions made to reach a target.
|
|
147
|
+
- **Deleting before the trim list is reviewed.** The written classification, read before anything is removed, is the only safeguard this workflow has.
|
|
148
|
+
- **Rewriting a test into vagueness.** Raising the level does not mean checking less. A test that now asserts nothing useful has become the dead weight it replaced.
|
|
149
|
+
- **Replacing mock theater with more mock theater.** A promotion that swaps one stub for another has not exercised the seam. If the rewrite did not make a real call, it is not done.
|
|
150
|
+
- **Changing production code while rewriting a test.** A real bug the audit uncovers is a separate issue.
|
|
151
|
+
- **Re-running the whole suite on every pass.** Work per file or per small group, and save the full suite for closeout.
|
|
152
|
+
- **Splitting into many small child issues.** Each is a session with real setup cost; group coarsely, at the suite level or higher.
|