task-pipeline-skill 1.85.2 → 1.86.3

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +2 -1
  4. package/SKILL-CARD.md +1 -1
  5. package/bin/task-pipeline.js +70 -9
  6. package/evals/cases/evidence-docs.json +188 -0
  7. package/evals/cases/project-audit.json +188 -0
  8. package/evals/cases/task-pipeline.json +191 -0
  9. package/package.json +5 -4
  10. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  11. package/plugins/task-pipeline/skills/evidence-docs/SKILL.md +16 -11
  12. package/plugins/task-pipeline/skills/evidence-docs/references/GENERATED.md +8 -0
  13. package/plugins/task-pipeline/skills/evidence-docs/references/documentation.md +472 -0
  14. package/plugins/task-pipeline/skills/evidence-docs/references/gates.md +645 -0
  15. package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +279 -0
  16. package/plugins/task-pipeline/skills/evidence-docs/references/learned.md +292 -0
  17. package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +551 -0
  18. package/plugins/task-pipeline/skills/evidence-docs/references/setup.md +149 -0
  19. package/plugins/task-pipeline/skills/evidence-docs/templates/decisions.md +50 -0
  20. package/plugins/task-pipeline/skills/evidence-docs/templates/docgate.sh +537 -0
  21. package/plugins/task-pipeline/skills/project-audit/SKILL.md +66 -25
  22. package/plugins/task-pipeline/skills/project-audit/scripts/audit.py +11 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +71 -54
  24. package/plugins/task-pipeline/skills/task-pipeline/execution-attempt.schema.json +68 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/execution-packet.example.json +42 -0
  26. package/plugins/task-pipeline/skills/task-pipeline/execution-packet.schema.json +217 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/execution-result.example.json +49 -0
  28. package/plugins/task-pipeline/skills/task-pipeline/execution-result.schema.json +261 -0
  29. package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +10 -1
  30. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +172 -2
  31. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +50 -1
  32. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +7 -0
  33. package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +23 -0
  34. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +6 -0
  35. package/plugins/task-pipeline/skills/task-pipeline/references/backlog.md +8 -1
  36. package/plugins/task-pipeline/skills/task-pipeline/references/browser.md +8 -0
  37. package/plugins/task-pipeline/skills/task-pipeline/references/build.md +32 -0
  38. package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +14 -3
  39. package/plugins/task-pipeline/skills/task-pipeline/references/decomposition.md +83 -2
  40. package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +53 -0
  41. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +3 -0
  42. package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +27 -8
  43. package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +10 -5
  44. package/plugins/task-pipeline/skills/task-pipeline/references/model-tiering.md +19 -0
  45. package/plugins/task-pipeline/skills/task-pipeline/references/planning.md +203 -26
  46. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  47. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +26 -8
  48. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +7 -1
  49. package/plugins/task-pipeline/skills/task-pipeline/scripts/context_packets.py +686 -0
  50. package/plugins/task-pipeline/skills/task-pipeline/scripts/execution_authority.py +271 -0
  51. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +415 -18
  52. package/plugins/task-pipeline/skills/task-pipeline/scripts/packet.py +400 -0
  53. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +2 -0
  54. package/plugins/task-pipeline/skills/task-pipeline/templates/browser-claims.json +54 -0
  55. package/plugins/task-pipeline/skills/task-pipeline/templates/finding-evidence.json +42 -0
  56. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +2 -2
  57. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +2 -2
@@ -0,0 +1,279 @@
1
+ # Hooks — agent-time enforcement, and the limit first
2
+
3
+ **One job: stop a bad edit before it lands, and never claim protection you do not
4
+ have.** A hook is rung 5 of [`gates.md`](gates.md)'s ladder — the only mechanism
5
+ that acts *while the agent is working* rather than after the commit.
6
+
7
+ ## Contents
8
+
9
+ - The limit, before the capability
10
+ - The events
11
+ - The `PreToolUse` contract
12
+ - What the hook receives
13
+ - Where it lives
14
+ - Matchers
15
+ - Performance
16
+ - What belongs in a hook, and what does not
17
+ - A worked example
18
+ - Debugging
19
+ - Removing them
20
+ - The one this skill ships
21
+ - Leases are not reimplemented here
22
+ - Rationalizations
23
+
24
+ ## The limit, before the capability
25
+
26
+ **Hooks exist only in Claude Code.** On Cursor, Codex and the other agents a skill
27
+ can be installed and read, but there is no `PreToolUse`, so nothing blocks anything.
28
+ On those agents the same rules run as a self-check written into the skill body, and
29
+ the run is recorded **`ungated`**.
30
+
31
+ **Never describe a project as protected when its agents run outside Claude Code.**
32
+ The gap between "the rule exists" and "the rule is enforced" is invisible from
33
+ inside a transcript, and a false guarantee is worse than a stated absence: everyone
34
+ downstream stops checking.
35
+
36
+ > **Provenance.** Every contract below is quoted from the Claude Code hooks
37
+ > reference (`code.claude.com/docs/en/hooks`), fetched **2026-08-03**. Re-fetch
38
+ > before relying on it: this is an external API, and stage 1 of this very pipeline
39
+ > exists because a contract recalled from memory is a contract that has already
40
+ > moved. Where the reference and this file disagree, the reference wins and this
41
+ > file is the bug.
42
+
43
+ ## The events
44
+
45
+ There are **35** hook events. These are the ones this pipeline reaches for; the
46
+ reference has the rest, grouped as session lifecycle, per-turn, tool execution,
47
+ subagents and tasks, file and config changes, compaction, worktrees, display, and
48
+ MCP elicitation.
49
+
50
+ | Event | Fires | Used for |
51
+ |---|---|---|
52
+ | `SessionStart` | session opens (matcher `startup\|resume`) | register the run, print the board, name the one next action |
53
+ | `PreToolUse` | before a tool call | **block** — the event that can refuse |
54
+ | `PostToolUse` | after a tool call | bookkeeping: renew a lease, stamp a marker |
55
+ | `Stop` | the turn ends | a last-word check — the run's own gate, not the repo's |
56
+ | `SubagentStart` · `SubagentStop` | a subagent starts or finishes | stage 5 runs implementers as subagents; this is where a per-agent identity or ledger line belongs |
57
+ | `WorktreeCreate` · `WorktreeRemove` | a worktree appears or goes | stage 5 isolates in worktrees; a guard that must not fire inside one can key off these |
58
+ | `PreCompact` · `PostCompact` | context is compacted | flush anything that only lives in context — the ledger exists because this happens |
59
+ | `SessionEnd` | session closes | release leases, flush the journal |
60
+
61
+ ## The `PreToolUse` contract
62
+
63
+ A hook blocks a call in **either** of two ways:
64
+
65
+ - **exit 2**, with the reason on **stderr** (stdout is ignored); or
66
+ - **exit 0** with this on stdout:
67
+
68
+ ```json
69
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse",
70
+ "permissionDecision":"deny",
71
+ "permissionDecisionReason":"docs gate failed: 2 undefined ids in docs/ARCHITECTURE.md"}}
72
+ ```
73
+
74
+ `permissionDecision` takes **four** values, not one: `allow` (permit it), `deny`
75
+ (block it), `ask` (escalate to the user), `defer` (fall through to the normal
76
+ permission flow). Exit 0 with empty stdout means `defer` by omission. A hook may
77
+ also rewrite the call instead of judging it, by returning `updatedInput` in the
78
+ same block — which is a different power from blocking and worth knowing before you
79
+ reach for it.
80
+
81
+ **Any other exit code is a non-blocking error**: execution continues and stderr is
82
+ shown in the transcript. The reference is explicit that **exit 1 is treated as
83
+ non-blocking, "even though 1 is the conventional Unix failure code"** — so the
84
+ single most likely way to write a guard, `command || exit 1`, is the one that does
85
+ not guard. And on exit 2 Claude Code **ignores stdout and any JSON in it**; only
86
+ stderr is read back.
87
+
88
+ So **a crashing guard fails open** — it stops guarding and nothing announces that
89
+ it has. Write the guard to `exit 2` on its own internal errors, or accept that a
90
+ typo in it silently removes the protection everyone believes is there.
91
+
92
+ That asymmetry is the whole reason this file leads with the limit: a hook is the
93
+ strongest rung and the one whose failure is quietest.
94
+
95
+ ## What the hook receives
96
+
97
+ JSON on stdin. Common to every event: `session_id`, `transcript_path`, `cwd`,
98
+ `permission_mode` (`default` · `plan` · `acceptEdits` · `auto` · `dontAsk` ·
99
+ `bypassPermissions`), `effort` (an object with `level`), `hook_event_name`, and —
100
+ inside a subagent — `agent_id` and `agent_type`. `prompt_id` is present from a
101
+ recent version onward, so treat it as optional unless you pin one.
102
+
103
+ Tool events add `tool_name`, `tool_input` and `tool_use_id`. **`tool_input` is
104
+ where the target lives** — `file_path` for an edit, `command` for a Bash call.
105
+ Parse it; do not infer the target from anything else in the environment
106
+ ([`learned.md`](learned.md) rule 15: a heuristic over strings the environment also
107
+ produces matched the throwaway shell of every tool call).
108
+
109
+ `agent_id` matters more here than it looks: stage 5 runs implementers as subagents
110
+ in worktrees, and a guard that must behave differently for the orchestrator and for
111
+ an implementer has exactly one honest way to tell them apart.
112
+
113
+ ## Where it lives
114
+
115
+ | Placement | Scope | Use when |
116
+ |---|---|---|
117
+ | the project's `.claude/settings.json` | this repository | the rule is this project's |
118
+ | a plugin's `hooks/hooks.json`, paths via `${CLAUDE_PLUGIN_ROOT}` | every project the plugin is installed in | the rule travels with a tool |
119
+
120
+ **A globally installed plugin must exit 0 immediately when the project has no
121
+ config for it.** Otherwise installing it once changes every other repository on the
122
+ machine, and the first surprising denial is debugged in the wrong project.
123
+
124
+ ## Matchers
125
+
126
+ ```json
127
+ { "matcher": "Edit|Write|MultiEdit|NotebookEdit",
128
+ "hooks": [{ "type": "command", "command": "…/guard.sh", "timeout": 20 }] }
129
+ ```
130
+
131
+ - a tool-name pattern (`Edit|Write|…`), or `"*"` for every call;
132
+ - for a specific shell command, add `"if": "Bash(git commit *)"` **inside the
133
+ handler object**, beside `"type"` and `"command"` — never beside `"matcher"`.
134
+ A matcher group is only `matcher` + `hooks`; a key Claude Code does not know
135
+ there is ignored, and from 2.1.270 announced at every session start as
136
+ `hooks.json: unknown key "if" … ignored`. This skill's own template carried it
137
+ at group level until v1.86.2, so its gate filtered nothing and ran on every
138
+ Bash call.
139
+
140
+ `if` uses **permission-rule syntax** (`Bash(git *)`, `Edit(*.ts)`) and is evaluated
141
+ **only on tool events** — `PreToolUse`, `PostToolUse`, `PostToolUseFailure`,
142
+ `PermissionRequest`, `PermissionDenied`. Anywhere else it is inert, which is a
143
+ silent way to write a guard that never fires.
144
+
145
+ **For Bash the match is best-effort — the reference's own word.** It inspects
146
+ subcommands, `$()` expansions and backticks, and strips leading `FOO=bar`
147
+ assignments before matching. So `if` is a good **filter** and a bad **boundary**:
148
+ narrow with it to keep the hook cheap, then re-check the real target inside the
149
+ script before refusing anything.
150
+
151
+ Match as **narrowly** as the rule allows. A `"*"` matcher on a blocking event puts
152
+ your script in the path of every tool call the agent makes.
153
+
154
+ ## Performance
155
+
156
+ A `PostToolUse` hook on `"*"` runs after **every** call, so it must be a no-op in
157
+ the common case: read one timestamp file, return. Touch the network at most once
158
+ per throttle interval. If it becomes slower than that, the throttle is broken —
159
+ **fix the throttle, never remove the hook**, because a hook removed for latency is
160
+ a protection removed permanently for a reason that had a fix.
161
+
162
+ ## What belongs in a hook, and what does not
163
+
164
+ **Belongs:** cheap, deterministic, and about an edit happening *right now* — a
165
+ guarded path, a staged file, a lease that is not held, a command with a shape the
166
+ project forbids.
167
+
168
+ **Does not belong:** the full test suite; anything needing the network on every
169
+ call; anything whose answer requires a human. Those are rungs 3 and 4
170
+ ([`gates.md`](gates.md)) — CI is late, and late is the correct trade for slow.
171
+
172
+ The test is one question: *if this fires, can the agent fix it in the next ten
173
+ seconds without asking anybody?* If not, blocking here only converts a review
174
+ comment into a dead end.
175
+
176
+ ## A worked example
177
+
178
+ The one hook this skill ships — run the documentation gate before a commit, and
179
+ refuse the commit if it fails. It is
180
+ [`../templates/hooks.example.json`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json); copy it into
181
+ the project's `.claude/settings.json`.
182
+
183
+ ```json
184
+ { "hooks": { "PreToolUse": [
185
+ { "matcher": "Bash",
186
+ "hooks": [{ "type": "command", "if": "Bash(git commit *)", "shell": "bash",
187
+ "timeout": 60, "command": "bash scripts/check-docs.sh >&2 || exit 2" }] } ] } }
188
+ ```
189
+
190
+ `|| exit 2` is the contract, not a flourish: without it the gate's own `exit 1`
191
+ lands in the "non-blocking error" branch and the commit proceeds.
192
+
193
+ ## Debugging
194
+
195
+ | Symptom | Cause |
196
+ |---|---|
197
+ | Guarded edits go through | the guard crashed — any exit code other than 2 is non-blocking. Run it by hand |
198
+ | Everything is denied | no config, or no lease. The tool's `status` says which |
199
+ | Session start is slow | the backend is unreachable; it must time out and degrade, never hang |
200
+ | The renew hook floods the log | the throttle file is not being written — check the path is writable |
201
+
202
+ Run the guard directly and see what it decides:
203
+
204
+ ```bash
205
+ echo '{"tool_name":"Edit","tool_input":{"file_path":"docs/DECISIONS.md"},"cwd":"'"$PWD"'"}' \
206
+ | bash .claude/hooks/guard.sh; echo "exit=$?"
207
+ ```
208
+
209
+ ## Removing them
210
+
211
+ Delete the `hooks` block from the project's `.claude/settings.json`. Everything the
212
+ hooks enforced is still available as a command and still stated in the doctrine —
213
+ and the run is **`ungated`** from then on, which is a thing to say out loud rather
214
+ than a detail to omit.
215
+
216
+ ## The one this skill ships
217
+
218
+ Since v1.50.0 the plugin carries hooks of its own. `hooks/release-gate.sh`
219
+ (`PreToolUse` on `Bash`) refuses an **outward, irreversible act** — `git tag`, a
220
+ tag push, `gh release create`, `npm publish` — while the run says the suite has not
221
+ passed. `hooks/gate-observer.sh` (`PostToolUse` and `PostToolUseFailure`) records
222
+ what the declared gate command actually did.
223
+
224
+ **Two lessons from v1.50.0, both shipped as defects and both fixed in v1.51.0.**
225
+
226
+ *A gate keyed to a stage NUMBER is the rail's mistake with worse consequences.*
227
+ The first version matched `stage: 6` literally. `progress.md` says the rail "is
228
+ computed, never eleven" because a host project replaces the flow — and a project
229
+ whose flow has six stages, tests green at stage 4, could never tag anything again.
230
+ A wrong rail misinforms; a wrong gate stops the work. The stage is resolved from
231
+ `pipeline.json` (`state: "tests"`, or a stage declaring `gate.command`), and
232
+ failing that from the ledger by name.
233
+
234
+ *A gate that reads a claim written by the party it constrains confirms an
235
+ assertion with itself.* `stage: … verdict pass` is typed by the agent. Where the
236
+ stage declares `gate.command`, the observer writes the **observed** exit code as a
237
+ `gate:` line and the release gate requires both. Declare no command and it
238
+ degrades to the claim alone — which is stated here rather than discovered.
239
+
240
+ It is the worked example above, made real, and its three narrownesses are the
241
+ reusable part:
242
+
243
+ | Narrowness | Why it is not a smaller feature |
244
+ |---|---|
245
+ | Only outward acts, never ordinary commits | stage 5 commits per task by design; a gate that fights the build loop is removed within a day |
246
+ | Silent where no `.task-pipeline/run.md` exists | enabling the plugin must change nothing in a repository that runs no pipeline |
247
+ | Reads the ledger, never reruns a suite | `progress.md` already makes the ledger append-only; a second source of truth about "did stage 6 pass" is the failure this file warns about below |
248
+
249
+ **Fail-closed, deliberately.** Every non-zero exit code other than `2` is
250
+ non-blocking, so an internal failure exits `2` as well. A crashing gate that fails
251
+ open is worse than no gate: it reads as one.
252
+
253
+ **The defect worth remembering** — the first implementation fed its own python
254
+ source to `python3 -` through a heredoc *and* read the payload from stdin. The
255
+ heredoc **is** stdin, so the payload came back empty, every act classified as "not
256
+ a release", and the gate allowed everything while looking installed. Eight fixtures
257
+ caught it. A hook that cannot see its own input is indistinguishable, from the
258
+ outside, from a hook that approves.
259
+
260
+ ## Leases are not reimplemented here
261
+
262
+ Guarded registers and lease arbitration belong to a coordination tool
263
+ ([`companion-skills.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md) names the optional one). This skill
264
+ ships the **doctrine** and the one example above.
265
+
266
+ Two implementations of one lease will disagree, and the disagreement is invisible:
267
+ each believes it holds the lock, both write, and the register ends up with two
268
+ entries carrying one id — which is the exact failure the lease existed to prevent.
269
+
270
+ ## Rationalizations
271
+
272
+ | Excuse | Reality |
273
+ |---|---|
274
+ | "The hook is installed, so the repo is protected" | Only in Claude Code, and only while the guard exits 2. Any other exit code fails open silently. |
275
+ | "It's fine, the guard can't crash" | Then it costs one line to make crashing block instead of pass. Write the line. |
276
+ | "I'll match `*` and filter inside the script" | Now every tool call pays your script's startup. Match narrowly; the matcher is free and the script is not. |
277
+ | "The hook is slow, I'll disable it for now" | "For now" survives the session and the memory of why. Fix the throttle. |
278
+ | "I'll put the test suite in the hook, it's the strongest gate" | It is the strongest and the most expensive. A rule that takes two minutes to answer belongs in CI. |
279
+ | "Other agents will follow the doctrine anyway" | They might. What they will not do is *block*, and the run must say `ungated` so nobody mistakes intention for enforcement. |
@@ -0,0 +1,292 @@
1
+ # Rules earned by failure
2
+
3
+ **One job: the mistakes that cost real time on a real project, each with the check that now catches
4
+ it.** Every rule here names the incident that produced it. A rule with no incident behind it is
5
+ somebody's preference, and it will be argued with at the worst moment.
6
+
7
+ They come from one 260-decision, 72-document specification built across four repositories with
8
+ several agents working at once. Nothing here is hypothetical.
9
+
10
+ **A rule belongs in the table only when it has a check.** Two of the lessons below could not be
11
+ mechanised and are kept out of it deliberately, as review questions — because a rule that pretends
12
+ to be enforced and is not is the same failure as a gate that prints `FAIL` and exits `0`.
13
+
14
+ ---
15
+
16
+ ## Contents
17
+
18
+ - The table — trigger · check · exit criterion
19
+ - The incidents, so the rules are not abstract
20
+ - The two that are not in the table, and why
21
+ - The one instruction that would have prevented the most
22
+ - Where these bind in the pipeline
23
+ - What leaves this file, and why there is no cap
24
+
25
+ ## The table — trigger · check · exit criterion
26
+
27
+ | # | Rule | Trigger | The check | Exit criterion |
28
+ |---|---|---|---|---|
29
+ | 1 | **Change the axis when it stops paying** | any second review or audit pass | count new findings and self-inflicted ones separately | self-inflicted exceeds new → change the axis, do not "look again more carefully" |
30
+ | 2 | **Absence needs its own check** | any two-layer model — entity/table, screen/frame, task/module, requirement/test | compute the mapping in **both** directions | zero orphans each way, printed |
31
+ | 3 | **Verify closure against the artefact** | closing anything that names a design, a build or a file | open the artefact, not the document describing it | the artefact carries a marker naming what it was last verified against |
32
+ | 4 | **Probe the detector** | every new check, lint rule or assertion | plant the defect → run → assert non-zero; restore → assert zero. **The plant asserts it CHANGED the file** — `assert text.count(old) == 1` before substituting — and the expected verdict is stated before the run, because a green suite and an unlanded plant are the same output | both directions observed once, and recorded; the plant's own effect asserted, and restored from a copy taken before it rather than by `git checkout`, which discards whatever else was uncommitted |
33
+ | 5 | **Doubt the probe first** | a check that stays silent when it should fire | prove the planted defect exists in the text the check actually parses | the probe is shown to have changed what the check reads |
34
+ | 6 | **Sweep the class, not the finding** | any correction | grep the whole corpus for the same shape before closing | the class is gated, or named in the note so the next reader can finish it |
35
+ | 7 | **Ratchet, never TODO** | anything that cannot be fixed now | a named, counted set printed on every run | the number may only fall, and `OK` prints it beside itself |
36
+ | 8 | **Compute, never restate** | any count or cross-reference stated in prose | derive it from the source at check time. **Where the token could occur inside something else, print the matched items classified beside the number** — `grep -oE '<token>' \| sort \| uniq -c`, never a bare `grep -c` — because *eighteen lines containing these three digits* and *eighteen throttle events* are different claims and only one of them was true | the stated number and the computed one are the same object, and the run has looked at WHAT matched, not only how many |
37
+ | 9 | **A generator seeds green** | any scaffold, template or code generator | run the generated project's own checks inside the generator's tests | fresh output exits zero |
38
+ | 10 | **Measure a detector before trusting it** | a new heuristic check | run it over the real corpus and count false positives | zero false positives, or the heuristic is replaced by a deterministic rule |
39
+ | 11 | **A gate's exit code is part of its output** | any gate or CI step | run it against a planted defect and assert on `$?` | non-zero, not merely a `FAIL` line on stdout |
40
+ | 12 | **Tests create what they assert on** | any test touching shared state | run the suite against a cold, empty environment | the cold run and the warm run agree |
41
+ | 13 | **Local infrastructure does not fight the host** | any dev compose or service definition | assume the host already runs the defaults | services reachable with the host's own still running |
42
+ | 14 | **A document may not send a reader to something absent** | any instruction naming a command, file or install | resolve it | the gate fails when the target does not exist |
43
+ | 15 | **Identity before coordination** | any lease, lock, claim or run id | ask what two instances with the same identity would do, and make the tool answer it | two instances demonstrably get two identities |
44
+ | 16 | **A carried-in claim is a recollection** | any run resuming from a summary, a handoff or a compacted context | re-derive the claim from its source before acting on it or reporting it | every state claim the run makes is marked `measured` with the command behind it, or it is not made |
45
+ | 17 | **The copy you are about to edit may not be the copy that ships** | any run editing a repository that has an upstream — a skill, a plugin, a vendored library, a fork | `git rev-list --count HEAD..@{u}` **before the first edit**; non-zero means stop and pull | the run states the count it measured, or it has not started |
46
+ | 18 | **State that accumulates locally is created from nothing everywhere else** | any run whose tests, migrations or fixtures read a database, a cache or a checkout that persists between runs on this machine | run the suite once against a **freshly created** instance of it, not the one that has been here for weeks | the run names the fresh instance it used, or the green is a green from residue |
47
+ | 19 | **An empty measurement is a refused measurement** | any command run to establish a fact — a count, a probe, a query, a suite | assert the output is **non-empty and shaped as expected** before reading meaning into it; a command that printed nothing did not answer | the run quotes the output it read, not the conclusion it drew from silence |
48
+ | 20 | **When a thing exists twice, ask which one is used — not whether they agree** | any artifact with a second copy: two build files, a vendored library, a schema and its mirror, doctrine in two documents | find the **consumer** and read what it names; the copies cannot answer this about themselves | the run names the file the build/test/deploy actually reads, quoted from the consumer |
49
+ | 21 | **A step that consumes what a later step produces is a deadlock, not an ordering** | any sequence where a check reads state another step writes — a prune reading stamps, a gate reading a ledger, a report reading counts | trace each input to the step that writes it; if that step is downstream, the check has never run on real data | every check names the step that produces its input, and that step is upstream of it |
50
+ | 22 | **An operation that changes nothing reports the same as one that changed everything** | a substitution whose needle is absent · a command whose output is suppressed at a decision point · an edit issued against a shape the file does not have | assert the effect, not the call: a replacement must report how many times it matched, and a command whose exit code governs the next step is never piped or silenced | four incidents in two programmes, each invisible until something downstream failed — an import that never landed, a doctrine phrase worded differently, a `gh` call refused behind `>/dev/null`, and a test piped to `head` so that `$?` belonged to `head` |
51
+ | 23 | **A commit reference is a prediction until the commit exists** | any record naming a sha, and by extension any token that looks like evidence — a line number, a file path, a flag name, a test name | fill the field AFTER the object exists and resolve it (`git cat-file -e <sha>`); where the record must ship inside the commit it names, name the commit that carried the CHANGE and let the record follow | every reference in the record resolves, checked by a command, not by its shape |
52
+ | 24 | **A guard that outlives the name it guards goes vacuous, not red** | renaming or replacing any construct other files name | search the test suite for the old name in the same pass; and derive a guard's subject list from a **runtime declaration**, never a literal maintained by hand | the guard names its subjects by reading what the code says about itself, so a new member is covered by construction |
53
+ | 25 | **An item leaving a ranked list is not evidence about that item** | any fix verified against a top-N report, a truncated list, or "it no longer appears" | re-run the SAME measurement of the SAME subject; and make every truncating report say what it dropped — `showing 5 of 12` | the closing evidence names the subject and its measured value, not its absence from a window |
54
+ | 26 | **A model too small to reproduce the defect cannot certify the fix** | any reduced case, minimal repro, fixture or scratch tree standing in for the real subject | reproduce the defect in the model FIRST; if it goes green on the unfixed input it is not a harness | the fidelity gap is stated as a list of what the model omits, with numbers, not as prose that invites reasoning past it |
55
+ | 27 | **A sweep states its denominator before it starts** | any pass over an enumerable corpus — locales, call sites, files, tools | name the size of the corpus up front and the number converted at close; and derive the guard from the set the project already enumerates, never from a list typed into the test | the closing evidence is a re-run of the finding census, not the diff — the diff shows what was touched, the census shows what is left |
56
+ | 28 | **Guard capability, not incompleteness** | any completeness metric, coverage score or "N of M" axis | assert that an input exists for which the axis answers *no*; a count that only ever rises cannot tell a finished axis from one that lost the ability to fail | the probe finds its own subject rather than naming a fixture, and an axis that cannot say no is labelled WHERE THE NUMBER IS PUBLISHED |
57
+ | 29 | **A census over a localised corpus reports per locale** | any pattern matched across translations, regions or notations | print subjects examined and subjects matched **per locale**; check the notation for numbers, units and signs in each target first | a locale reporting zero where its neighbours report several is raised as a question by the report's own shape — and where the subject is one fact in N dictionaries, compare the dictionaries to EACH OTHER, which needs no notation knowledge at all |
58
+ | 30 | **A gate scoped by exclusion judges every newcomer silently** | any lint, format or check configured with an ignore list rather than an include list | scope by what it COVERS; where the tool has no such switch, assert the covered set and fail on additions, deriving the allowlist by measuring the tool's own reach | the failure message names the intruder and the remedy, because the operator meets it mid-release |
59
+ | 31 | **A gate needs a third verdict — could not measure** | any check that can time out, lose a dependency, or run on a contended machine | distinguish *failed* from *could not measure* with different exit codes, and say which; neither is a verdict about the code | the refusal names its remedy, and a test that waits for a DURATION rather than an event is treated as a defect in the test |
60
+ | 32 | **A test whose assertion is implied by compilation is not a test** | any test whose name promises a behaviour | delete the subject: if that is a type or import error rather than a red test, the test is decoration | removing one asks what the name PROMISED and whether anything checks it — four of five such cases here were covered elsewhere and the fifth was a real gap only visible because the name was read as a claim |
61
+
62
+ ---
63
+
64
+ ## The incidents, so the rules are not abstract
65
+
66
+ **1 · Change the axis.** Seven passes of comparing documents against each other were instrumented.
67
+ Findings per pass: 12, 17, 13, 19 — and *caused by the previous pass*: 5, 9, 10, 4. By the sixth
68
+ pass the method was mostly repairing itself, because each pass edits the corpus the next one reads,
69
+ so the newest text is always the least reviewed. The fix is not more care; it is a different axis.
70
+
71
+ **2 · Absence.** Comparing documents found contradictions for weeks and never found that one
72
+ service had **no key store at all** — a decision made it the encryptor of every media object, the
73
+ other service's table had been modelled for weeks, and this one had no entity, no fields, no
74
+ rotation story. A contradiction needs two sides. Absence has one. The register that finally caught
75
+ it checks *entity → table* and *table → entity*, and **only the second direction found anything**:
76
+ four entities fully specified, cited by build tasks, with no schema anywhere.
77
+
78
+ **3 · Closure against the artefact.** Two gaps were marked **closed** while the design still
79
+ violated them — the closure had been verified by reading the specification, which had been fixed,
80
+ while the frames had not. Twice is a category, so every drawn screen now carries a marker naming the
81
+ newest decision its *frames* were checked against, and the linter fails when the prose cites
82
+ something newer.
83
+
84
+ **4 and 5 · Probes.** Every check written was exercised against a planted defect. **Five probes
85
+ failed before any check did:** one added a definition where the check looks for an unresolved
86
+ reference; one edited a string whose whitespace did not match; one hit the first prose mention
87
+ instead of the table row; one flipped a row whose producer cell was empty, so nothing was planted;
88
+ one counted matching lines and counted the check's own `INFO` line as a hit. Four times out of five
89
+ the probe was wrong, not the check.
90
+
91
+ **6 · Sweep the class.** A pass fixed four invented audit-action names in a table and stopped at the
92
+ rows it was looking at; two rows in the same table still said something else, found the next day.
93
+ The same shape recurred three times — a retired word corrected on one screen and left on two others,
94
+ a value fixed in a light frame and left in its dark twin, a stale retention window removed from
95
+ three documents' prose and surviving in a design.
96
+
97
+ **8 · Compute.** Two documents had to stop quoting totals after they went stale. The register that
98
+ replaced them computes its cross-reference column from lines the tables themselves carry, so the two
99
+ cannot disagree.
100
+
101
+ **9 · Seeds green.** A documentation gate added to a scaffold **failed on its own seeds** — it read
102
+ the template block and the allocation line as real identifiers. A project that starts red teaches
103
+ everyone on day one that the gate is noise.
104
+
105
+ **10 · Measure the detector.** A parity-based check for unbalanced markup produced **six false
106
+ positives out of six** on the real corpus. It was discarded for a deterministic rule. A gate that
107
+ cries wolf is removed by the third person who hits it.
108
+
109
+ **11 · Exit codes.** A repository's docs gate appended a check *after* its verdict block, so it
110
+ printed `FAIL` and returned `0`. CI had been green over it for an unknown period.
111
+
112
+ **12 · Cold runs.** An isolation test read whatever another test file had left in the database. Test
113
+ files run alphabetically and the one that creates that data runs later, so the test **passed on a
114
+ warm database and failed on a cold one** — which is what every new developer has.
115
+
116
+ **13 · Ports.** A dev compose published the default Postgres and Redis ports. The machine already
117
+ ran both, so the containers were silently shadowed: the tools connected to the host's services and
118
+ the migration failed with a permission error that named nothing about the collision.
119
+
120
+ **14 · Absent targets.** A repository's first instruction to every agent was to run a command that
121
+ was not installed, with no install line anywhere and no statement of what a session without it
122
+ actually is.
123
+
124
+ **15 · Identity.** The coordination plugin derived one run id **per checkout**. A hook has the
125
+ session id in its environment and a plain shell command does not, so the second session in a
126
+ checkout adopted the first one's identity: **an entire day of work was performed holding another
127
+ session's leases**, and the end-of-work check that had just been written offered to release
128
+ *theirs*. It was invisible from inside — `whoami` reported a lease and a run id, both plausible,
129
+ both somebody else's — and it surfaced only because a new command printed a lease nobody could
130
+ account for. This is the same failure as *the one instruction* below, and it is in the table rather
131
+ than only in that list because it **has** a check. Follow-on, from the first two attempted fixes:
132
+ **do not infer identity from strings the environment is also free to contain** — matching `"claude"`
133
+ in a process command line matched the throwaway shell of every tool call, and matching the binary
134
+ path hit the same wall. Prefer a fact something authoritative wrote down.
135
+
136
+ **16 · A carried-in claim.** A long autonomous run advanced one roadmap row per iteration. Each
137
+ iteration was correct: gates green, defects planted and watched to fail, docs closed. What was wrong
138
+ was the sentence between them — *"the remaining rows are these"* — taken from a list that had
139
+ arrived in the context through a compaction, had once been a filtered subset, and had lost its
140
+ provenance on the way. Eleven iterations later a single command over the register printed **36 open
141
+ rows out of 99**, not the handful being worked from. Nothing had failed, because nothing compares a
142
+ run's belief about the work-list against the register; the claim only ever appeared in prose.
143
+
144
+ **17 · The stale source.** A machine keeps its skills in two places: the working copy it publishes from and the installed plugin it runs. On 2026-08-07 the working copy was **two commits behind its own origin** — `v1.16.2` against `v1.18.0` — and the newer commits carried rule 16 itself. The repository was clean, nothing had diverged, and `git status` said so; the copy was simply never pulled. An edit made there would have landed on top of 1.16.2, and the release would have **silently deleted rule 16 and everything else in two versions** — not as a conflict, which git would have shown, but as a fast-forward over work that was already published. The project's own instruction names that directory as the source, so the person doing it would have been following the documentation. Nothing in the pipeline asked the one question that separates a source from a copy of one, and the check is a single command.
145
+
146
+ **18 · Residue.** A service's CI job started an empty database, created the runtime role in it, and ran the suite. Nothing between those two steps applied the schema. The result was **1039 failed, 1339 errors, 4704 × `UndefinedTable`** — every suite that touches a table — and it had been that way for as long as the repository had real tests. It was invisible because it is invisible *locally*: the compose database is migrated once, by hand, and stays migrated, so every author's machine has a schema and the runner's has none. The same day, in the same repository, a second instance: sixteen production tables owned by the **serving** role, because a migration had once been run as whoever was at the keyboard. Neither is a test defect. Both are the difference between state that accumulates and state that is created, and the only thing that tells them apart is running against something new.
147
+
148
+ **19 · Silence read as assent.** Three failures in one session, all the same shape. A `docker run` without `-i` does not attach stdin, so a heredoc carrying `ALTER ROLE` stopped at the docker CLI; `psql` read an empty script, did nothing, and exited **0** — and the step printed "password set". A `grep` pattern written against the wrong output format matched nothing, so three consecutive planted-defect runs printed empty strings that read as passes. And a migration step printed no lines at all, which looked like a step that had not run and was in fact a step that had. In every case the instrument failed and the failure was **indistinguishable from success**, because both produce nothing. Rule 11 covers the exit code; this covers the other half, which is louder in practice: an exit code of 0 from a command that never ran is the most convincing lie a run can tell itself.
149
+
150
+ **20 · The copy that wins.** A service had **two Dockerfiles**. One was added at the repository root by a run that checked whether a Dockerfile existed by looking where it expected one; `docker/Dockerfile` had been there all along, and `.github/workflows/ci.yml` says `file: docker/Dockerfile`. They disagreed about the port — 8080 at the root, 8000 in `docker/` — and the disagreement surfaced two days later as a **deployed service that answered nothing**, while `docker ps` said `Up` and `systemctl` said `active`. The built one also ran as root and copied the whole context, including `.git` and any `.env`; the hardened one was the one nobody built. Comparing the two files would have found the difference and not the direction. Only the workflow line says which one ships, and it is one grep. The same session hit this three more times: an autonomy sweep row added to the file that ASKS and not the file that RECORDS, twice, caught by a validator that knew to look at both.
151
+
152
+ **21 · The prune that could not run.** A retrospective's standing-instruction list has a hard cap of ten and three retirement triggers, one of which was, at the time of this incident, "it has not fired in the last five run stamps" — it has since gained a second unit, and the incident is left as it happened. The stage's own instruction was **prune first, then stamp**. So the trigger read a counter the same stage wrote afterwards: on a fresh list it is unreadable, and it stays unreadable for as long as nobody stamps. Measured on a real project: the last retro entry was five days old, stamps per day ran 33, 20, 26, **3, 0** — the zero on a day with 107 commits — and the list sat at exactly **10 of 10**, so every run arrived at a stage that opened with a full list, an unusable trigger and a mandatory deletion. It was not skipped out of laziness. It was skipped because its first step could not be performed, and the cheap step that would have made it performable was queued behind it.
153
+
154
+ The same class had already bitten that project twice from the other side, and its roadmap names the
155
+ property exactly: seven rows read `blocked` on producers the dependency board recorded as delivered,
156
+ and *"no gate can catch it because it breaks nothing — it only removes work from consideration"*;
157
+ and a row filed as *"the object nothing produces"* whose producer had shipped in between. **Stale
158
+ state does not throw.** It narrows what gets considered, silently, and every downstream gate passes
159
+ honestly on the smaller world.
160
+
161
+ Rule 8 is the neighbour, not the same rule: it governs a number *inside a document*, checked when
162
+ that document is checked. This one governs a fact that crossed a **session boundary** and is being
163
+ reported as current — the case where there is no document to check, only a memory that reads like
164
+ one.
165
+
166
+ ---
167
+
168
+ ## The two that are not in the table, and why
169
+
170
+ Kept as review questions, because no check can decide them:
171
+
172
+ - **Is this the right citation?** A stale reference was replaced with a *false* one — the new target
173
+ existed and said nothing about the subject. A gate can prove an identifier resolves; only a reader
174
+ can prove it is the right one. **Ask at review: did you open the target and confirm it says the
175
+ thing?**
176
+ - **Does the prose match the contract, or another paragraph of prose?** A "correction" to an attempt
177
+ cap introduced a second vocabulary for one counter, because it was written against a description
178
+ instead of the schema. **Ask at review: which artefact did this number come from?**
179
+
180
+ ---
181
+
182
+ ## The one instruction that would have prevented the most
183
+
184
+ > **Before trusting any tool's report about the world, make it report something you can already
185
+ > verify.**
186
+
187
+ Four of the worst failures on that project were the same failure wearing different clothes: a test
188
+ suite reporting green having skipped every assertion; a gate printing `FAIL` and exiting `0`;
189
+ containers reporting healthy while the tools talked to the host's services; a coordination plugin
190
+ reporting a lease held by an identity that belonged to a different session. In each case the tool
191
+ was describing a world it was not looking at, and in each case one deliberate check against a known
192
+ answer would have exposed it in a minute.
193
+
194
+ ---
195
+
196
+ ## Where these bind in the pipeline
197
+
198
+ | Stage | Rules that apply |
199
+ |---|---|
200
+ | 0 Inventory · 9 Docs · any register write | 8 (compute), 14 (targets resolve — including every commit SHA in the retro), 15 (identity before a lease) — see [`documentation.md`](documentation.md) |
201
+ | 0 Harvest · any run resuming from a summary | 16 — the work-list and every inherited state claim re-derived before use, [`knowledge-sources.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md) → *Carried-in claims* |
202
+ | any check you write | 4, 5, 7, 10, 11 — the procedure is [`gates.md`](gates.md) |
203
+ | **every stage** · any edit, any command whose result is read | 22 — a no-op is indistinguishable from success unless the effect is asserted; never suppress the output of a command a decision depends on |
204
+ | 3 Spec · 4 Plan | 2 (both directions), 8 (compute, never restate) |
205
+ | 5 Dev | 9 (generators seed green), 12 (tests create their own state), 13 (local infra) |
206
+ | 6 Tests | 4, 5, 10, 11 — every new check probed both ways, measured, and asserted on its exit code |
207
+ | 3 Spec | 14 — every check the spec **names** must resolve at the moment it is named, or be marked `review` |
208
+ | 4 Plan | 14 — every command, path and file a DoD names must resolve |
209
+ | 9 Docs | 8, 14 — every number computed, every target resolvable |
210
+ | 10 Acceptance | 1, 3, 6, 7 — axis rotation recorded, closure verified against artefacts, classes swept, ratchets printed |
211
+ | 10 Acceptance · every loop iteration | 16 — the work-list re-measured at close and printed beside its opening count ([`audit.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/audit.md), [`continuity.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/continuity.md)) |
212
+ | 0 Harvest · **before the first edit**, in any repository with an upstream | 17 — `git rev-list --count HEAD..@{u}` measured and its number printed, [`knowledge-sources.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md) → *The source is not the copy you have*; asked as row `0 Source` of [`grill.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/grill.md) → *The autonomy sweep* |
213
+ | 0 Harvest · 5 Dev · 6 Tests | 18 — the suite run once against a **freshly created** instance of whatever persists between runs, [`tdd.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/tdd.md) → *The green from residue*; asked as row `0 Fixtures` of [`grill.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/grill.md) → *The autonomy sweep* |
214
+ | any command run to establish a fact — 6 Tests · 9 Docs · 10 Acceptance · 5 review | 19 — the output asserted non-empty and shaped as expected, and **quoted** rather than concluded from, [`audit.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/audit.md) → *Silence is not a reading*; the reviewer's half is in [`review.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/review.md) |
215
+ | 0 Harvest · 10 Acceptance | 20 — the **consumer** read to learn which copy ships, never the copies compared against each other, [`audit.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/audit.md) → *Two copies, and which one wins*; asked as row `0 Duplicates` of [`grill.md`](https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/references/grill.md) → *The autonomy sweep* |
216
+ | 10 Retro · **any gate or check you order** | 21 — each input traced to the step that writes it, and that step proven upstream, [`retrospective.md`](retrospective.md) → *Stamp first, then prune, then write* |
217
+
218
+ **This file is the shipped list; a project keeps its own.** Every rule in the table
219
+ above was earned on someone else's build and travels with the skill. The lessons *your*
220
+ project buys go in its retro ([`retrospective.md`](retrospective.md) →
221
+ `<artifacts>/retro.md`), where they are capped, pruned and retired — and a
222
+ lesson there that would be true in any repository belongs here instead, as an issue
223
+ upstream. A local file that accumulates universal rules is a fork of this one that
224
+ nobody named.
225
+
226
+ ---
227
+
228
+ ## What leaves this file, and why there is no cap
229
+
230
+ `<artifacts>/retro.md` caps its standing instructions at **ten** and retires them
231
+ on three triggers. Somebody proposes the same cap here about once a programme. It is
232
+ the wrong instrument, and the reason is worth more than the rule.
233
+
234
+ **A cap belongs to a file you must finish reading.** The retro's standing instructions
235
+ are read *in full* at stage 0 of every run — bounded by construction, or the last one
236
+ is never reached. This file is never read in full: it is entered by citation from the
237
+ stages, and *Where these bind in the pipeline* is that entrance. **A file you enter
238
+ through an index needs its index to be right, not its length to be short.**
239
+
240
+ And the cap would have measured the axis that is not moving. Re-derived across releases
241
+ rather than recalled — **measured at each tag**, so these rows are history and cannot
242
+ go stale; the live shape is printed by `npm test` beside its verdict, and this file
243
+ states no number about itself:
244
+
245
+ | | v1.14.1 | v1.20.0 | v1.23.0 | v1.29.0 |
246
+ |---|---|---|---|---|
247
+ | rules in the table | 15 | 18 | 21 | **21** |
248
+ | words in the file | 2165 | 2987 | 3696 | **3919** |
249
+
250
+ Rules have been flat for four releases while the file grew — and every word of that
251
+ growth is in the binding map, the section that makes a rule *reachable*. Cutting there
252
+ shortens the index. The largest section by far is the incidents, and those are the only
253
+ record of those events anywhere in this repository — checked by taking each incident's
254
+ distinctive tokens against the whole retro corpus, which returned nothing. They are not
255
+ a compression target; they are the thing the rules are made of.
256
+
257
+ **Two triggers retire a rule, and neither is a count:**
258
+
259
+ 1. **The conditions cannot occur.** The tool, the layer, or the failure mode it names
260
+ is gone from every project the skill runs on — not "we have not hit it lately".
261
+ 2. **It is subsumed.** Another rule covers it entirely. This is a **merge**, not a
262
+ delete: the absorbing rule names the absorbed one, and every binding-map row that
263
+ pointed at the old number is repointed in the same change, or the map now sends a
264
+ stage to a rule that is not there.
265
+
266
+ Never *"it became a check"* — that trigger is right for a standing instruction, whose
267
+ whole purpose is to be read until the machine takes over. Here the rule is the reason
268
+ the check exists, and a check whose reason has been deleted is the next thing somebody
269
+ removes as noise.
270
+
271
+ **Every deletion is logged as one line**, in the same change, the same discipline as
272
+ the retro's prune: the rule's number, its name, which trigger fired, and the commit.
273
+ A rule that vanishes silently takes its incident with it, and the next run re-learns it
274
+ at full price. Numbers are never reused and never closed up — a gap in the table is the
275
+ evidence that something left, and the log below says what.
276
+
277
+ ### Retired
278
+
279
+ **Numbers issued so far: 32.** This is the high-water mark, and it is the only number
280
+ this file states about itself — deliberately, because the gap that proves a rule left
281
+ cannot be computed from the table alone: **deleting the highest-numbered rule shrinks
282
+ the maximum with it, and no gap ever opens.** That false negative shipped in the first
283
+ draft of this very section's guard and was found by a reader, not by its probe, which
284
+ had planted in the middle of the list.
285
+
286
+ *None retired yet.* Stated rather than omitted: an empty log and a missing log look
287
+ identical from outside, and only one of them means nothing has been retired. Each
288
+ retirement is one line, starting with the rule's number:
289
+
290
+ <!-- - **N · Name** — trigger: subsumed by M | conditions gone; `<commit>` -->
291
+
292
+ ---