task-pipeline-skill 1.11.0 → 1.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.12.0 — 2026-08-05
4
+
5
+ ### Added — a gate for the defects an agent leaves behind
6
+
7
+ Every other check in this repository asks whether the work is *right*. None of
8
+ them asked whether the text is *intact* — whether a merge was left half-resolved,
9
+ a stub outlived its task, a generation stopped mid-fence, a file was "shortened"
10
+ while being rewritten, a retried edit duplicated a block instead of replacing it,
11
+ or a section was opened and abandoned. That class has an author, and the author is
12
+ an agent.
13
+
14
+ `templates/hygiene.sh` is the answer: a seeded, executable gate, sibling of
15
+ `docgate.sh`, carrying the same contracts — a `SCOPE:` header that names its own
16
+ false-positive surfaces, a VERDICT block that must be last, portability to macOS
17
+ bash 3.2, progressive arming, and a final line of computed numbers. It runs in two
18
+ modes: the run's diff at **zero tolerance**, and the whole tree behind per-check
19
+ **ratchet floors**, so an existing repository can adopt it without starting red
20
+ while nothing new is forgiven.
21
+
22
+ **It never edits.** None of the six defects is safely machine-fixable — deleting a
23
+ "duplicated block" sometimes deletes a legitimate repetition, and deleting a `TODO`
24
+ erases a reminder instead of discharging it. `references/build.md` makes fixing the
25
+ agent's obligation, at the point it is cheapest: after each task reports `DONE`,
26
+ before the reviewer sees it. Found one task later, a defect costs a re-dispatch;
27
+ found eight tasks later it is fixed by an agent that no longer remembers the code.
28
+
29
+ **Two of the six definitions were wrong until they were measured**, which is the
30
+ point of the rule that says measure a detector before shipping it. Matching the
31
+ word `TODO` anywhere found 33 hits here, 28 of them ordinary English — this is a
32
+ repository whose own doctrine is *"a ratchet, never a TODO"*. Anchored as a
33
+ line-leading marker: 0. Flagging a heading followed by any heading found 62
34
+ legitimate nestings; restricted to same-or-higher level it found 4, and the
35
+ detector still had a bug — fenced lines were skipped entirely, so a section whose
36
+ body is one code block looked empty. Counting fenced content as body: 2, both real.
37
+ Check 4 then repeated check 2's mistake in its sibling and was caught the same way.
38
+
39
+ Those last 2 were fixed rather than absorbed into a floor, so all six ship at 0.
40
+ One of them was a shape mistake in the retro template every project copies: a
41
+ one-line retirement record written as a heading, when a heading promises a section.
42
+
43
+ ### Fixed — a guard that had been decorative since it shipped
44
+
45
+ Writing the first probe for the VERDICT-last contract revealed that the guard split
46
+ each gate script on the *word* `VERDICT`, which also appears in the header sentence
47
+ forbidding anything after it. Its "tail after the verdict" was therefore most of the
48
+ script, exit lines included, and it passed on anything. It now splits on the
49
+ `# ---------- VERDICT` marker and requires that marker to exist. A green from a
50
+ check nobody has watched fail is not evidence; this one was green for nine releases.
51
+
52
+ The template contract guards now iterate a `GATE_SCRIPTS` list instead of naming one
53
+ file, so a third gate costs one line rather than a copied block that drifts.
54
+
55
+ Negative self-tests: 68 → 75.
56
+
3
57
  ## v1.11.0 — 2026-08-04
4
58
 
5
59
  ### Added — run continuity: the pacing rules the operator was repeating by hand
package/CONTRIBUTING.md CHANGED
@@ -117,6 +117,13 @@ found**, and run a minimum of live checks — because every section can go `dorm
117
117
  and a gate blind to a shape passes exactly like one that reads it. Change a template
118
118
  → run `npm test`, not just your eyes.
119
119
 
120
+ **The same law now covers `templates/hygiene.sh`.** It is run over a clean scratch
121
+ project, must exit `0`, and must report all six of its check counts — exit `0` alone
122
+ proves nothing, because every check can go `dormant` and dormant is green. Both gate
123
+ scripts are validated by one iterated block, not two copies, and both must keep a
124
+ `# ---------- VERDICT` marker with nothing after it.
125
+ *(guard: `the VERDICT block must be last and must `)*
126
+
120
127
  **11. Every reference over 100 lines carries a `## Contents` list**, and the list is
121
128
  compared against that file's own `##` headings. The guidance asks for it because a
122
129
  long file gets previewed with a partial read; the comparison is because a hand-kept
package/README.md CHANGED
@@ -776,7 +776,7 @@ recommendation, so you arm the whole run in one exchange. Detail:
776
776
  | [`references/knowledge-graph.md`](plugins/task-pipeline/skills/task-pipeline/references/knowledge-graph.md) | the code graph: install line, stage-0 reach queries, the stage-9 refresh, the graph↔docs divergence check |
777
777
  | [`references/retrospective.md`](plugins/task-pipeline/skills/task-pipeline/references/retrospective.md) | the project retro: the three grades of fix, the mandatory prune, the cap of ten |
778
778
  | [`references/model-tiering.md`](plugins/task-pipeline/skills/task-pipeline/references/model-tiering.md) | model policy, the `/model` reminder, overrides |
779
- | [`templates/`](plugins/task-pipeline/skills/task-pipeline/templates/README.md) | brief, carry-over ledger, `CONTEXT.md` and ADR skeletons, the doc map, both registers, the retro and its archive, the seeded `docgate.sh`, a worked hook |
779
+ | [`templates/`](plugins/task-pipeline/skills/task-pipeline/templates/README.md) | brief, carry-over ledger, `CONTEXT.md` and ADR skeletons, the doc map, both registers, the retro and its archive, the seeded `docgate.sh` and `hygiene.sh`, a worked hook |
780
780
  | [`references/adoption.md`](plugins/task-pipeline/skills/task-pipeline/references/adoption.md) | the first run in a project: greenfield seeding, and the brownfield walkthrough |
781
781
  | [`references/setup.md`](plugins/task-pipeline/skills/task-pipeline/references/setup.md) | the entry audit: seven passes over the docs a project already has, offered once, output as a fix plan |
782
782
  | [`references/portability.md`](plugins/task-pipeline/skills/task-pipeline/references/portability.md) | the manifest of workflow decisions and their homes in the bundle, and the boundary against a project's own answers |
package/SKILL-CARD.md CHANGED
@@ -12,7 +12,7 @@ harmless.
12
12
  |---|---|
13
13
  | **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
14
14
  | **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
15
- | **Version** | 1.11.0 |
15
+ | **Version** | 1.12.0 |
16
16
  | **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
17
17
  | **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
18
18
  | **Evaluation status** | Suite authored (15 evals, 5 categories). **Never executed** — see [`evals/RESULTS.md`](evals/RESULTS.md) |
@@ -38,7 +38,7 @@ apply.
38
38
  instruction surface, and every one is linked directly from `SKILL.md`.
39
39
  2. Read `templates/docgate.sh` before seeding it; it is the only shipped script a
40
40
  host project will run on its own repository.
41
- 3. Run `npm run test:all` — 68 guards, each with a negative self-test that plants a
41
+ 3. Run `npm run test:all` — 76 guards, each with a negative self-test that plants a
42
42
  defect and requires rejection.
43
43
  4. Run `python3 evals/run.py` for the behavioural protocol, and read
44
44
  `evals/RESULTS.md` for what has actually been observed.
@@ -54,7 +54,7 @@ apply.
54
54
  - **Versions are pinned by git tag** and mirrored into `sshlg-skills`'s catalogue.
55
55
  Rollback is `git checkout v<previous>` or pinning the previous plugin version;
56
56
  the previous version is never deleted.
57
- - **Behavioural evidence is missing, not merely thin.** 68 structural guards prove
57
+ - **Behavioural evidence is missing, not merely thin.** 76 structural guards prove
58
58
  the skill is well-formed. Until `evals/RESULTS.md` carries a dated run, nothing in
59
59
  this repository proves it *behaves* — triggers correctly, stays quiet on a
60
60
  question, or performs the steps it documents.
@@ -207,7 +207,15 @@ not authorize an outward, irreversible action — stage 7 stops and asks.
207
207
  commit), then a review with **all three** verdicts — spec compliance, **REQ
208
208
  satisfied** (judged against the requirement's own statement, not the task's
209
209
  instructions), and code quality. Findings loop back to the implementer (max 5
210
- rounds, then adjudicate in writing); minor findings are recorded, never blocking. Keep a progress ledger in
210
+ rounds, then adjudicate in writing); minor findings are recorded, never blocking.
211
+ **After each task reports done, before the review, scan what that task changed
212
+ for the defects an agent leaves behind** — a half-resolved merge, a stub that
213
+ outlived the task, a code fence left open, a file "shortened" while being
214
+ rewritten, a block duplicated by a retried edit, a section opened and abandoned.
215
+ Zero tolerance on a diff: that task wrote it, that task fixes it. A subagent's own
216
+ report is not evidence about the text it left behind, and a duplicated block eight
217
+ tasks later is repaired by an agent that no longer remembers the code.
218
+ Keep a progress ledger in
211
219
  a file so a lost context can resume instead of redoing finished work. Tasks in
212
220
  the same parallel group run concurrently **only** if each worker has its own
213
221
  worktree — otherwise sequential. Close the stage by integrating: sync with the
package/evals/RESULTS.md CHANGED
@@ -37,13 +37,16 @@ or it belongs in a check.
37
37
  | Dated runs recorded | **0** | 2026-08-03 |
38
38
 
39
39
  The bottom two numbers are the honest state of this skill's behavioural evidence.
40
- Everything else in this repository is proven by 68 structural guards that check the
40
+ Everything else in this repository is proven by 76 structural guards that check the
41
41
  *form*; these are the only checks that would speak to the *behaviour*, and they have
42
42
  not been run yet. Printed here so "63 of 63 green" is never read as "the skill is
43
43
  known to work".
44
44
 
45
45
  ## Runs
46
46
 
47
+ Newest first. Each entry states the model, the task, and how the evidence was
48
+ obtained — a self-observed run and a blind one are not the same claim.
49
+
47
50
  ## 2026-08-03 · opus · self-observed, not a blind run
48
51
 
49
52
  **Scope of this evidence.** The author ran the skill on a real task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-pipeline-skill",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "task-pipeline": "bin/task-pipeline.js"
@@ -2,7 +2,7 @@
2
2
  "name": "task-pipeline",
3
3
  "displayName": "Task Pipeline",
4
4
  "description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/manual gates, a frozen requirement spine that must close with evidence, a loop guard that breaks churn, one provider-agnostic model confirmed up front, and an optional super-ux UX track for user-facing work.",
5
- "version": "1.11.0",
5
+ "version": "1.12.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -156,6 +156,7 @@ plugins/task-pipeline/
156
156
  conventions.md artifacts.md # host conventions, this layout
157
157
  companion-skills.md # optional companions + preflight
158
158
  templates/ # skeletons seeded into a host project
159
+ hygiene.sh # -> scripts/check-hygiene.sh (stages 5, 6, 9)
159
160
  README.md brief.md carryover.md context.md adr.md retro.md
160
161
  cursor/rules/task-pipeline.mdc # Cursor channel (self-contained rule)
161
162
  bin/task-pipeline.js # npx installer (package task-pipeline-skill)
@@ -247,11 +247,35 @@ physical: **two implementers writing one working tree corrupt each other's state
247
247
 
248
248
  | Status | Action |
249
249
  |---|---|
250
- | `DONE` | Build the review package, dispatch the task review ([`review.md`](review.md)). |
250
+ | `DONE` | **Run the hygiene gate in diff mode first** (below), then build the review package and dispatch the task review ([`review.md`](review.md)). |
251
251
  | `DONE_WITH_CONCERNS` | Read the concerns first. Correctness or scope → resolve before review. Observations ("this file is getting large") → **append to the carry-over ledger**, then proceed. A concern that stays only in the report dies with the workspace. |
252
252
  | `NEEDS_CONTEXT` | Supply exactly what's missing, re-dispatch. |
253
253
  | `BLOCKED` | Diagnose: missing context → re-dispatch with it; needs more reasoning → a more capable model; too large → split the task; the plan itself is wrong → escalate to the operator. |
254
254
 
255
+ ### The hygiene gate — after every task, before the review
256
+
257
+ A subagent's own report is not evidence about the text it left behind. Run the
258
+ seeded gate over **what this task changed**, before the reviewer sees it:
259
+
260
+ ```bash
261
+ HYGIENE_BASE=<the commit recorded before dispatching> bash scripts/check-hygiene.sh
262
+ ```
263
+
264
+ Diff mode has **no floor**: this task wrote it, this task fixes it. Six checks — a
265
+ half-resolved merge, a stub that outlived the task, a fence left open, a file
266
+ "shortened" while being rewritten, a **block duplicated by a retried edit**, and a
267
+ section opened and abandoned.
268
+
269
+ The fifth is why the gate runs here rather than only at stage 6. A batch of edits
270
+ where one applied twice, or never applied at all, is the incident behind standing
271
+ instruction R-002 — and it is invisible in a status report. Found one task later it
272
+ costs a re-dispatch; found eight tasks later it is fixed by an agent that no longer
273
+ remembers the code.
274
+
275
+ **The gate never edits. Fixing is yours.** A finding is repaired inside the same
276
+ task, or it becomes a carry-over row with a reason — never a silent pass. A gate
277
+ whose findings nobody acts on is a slower way of ignoring them.
278
+
255
279
  **Never** ignore an escalation, and never re-dispatch the same model with the same
256
280
  prompt after a BLOCKED. If the implementer says it's stuck, something must change.
257
281
  If the implementer asks a question — before or mid-task — answer it completely; do
@@ -57,6 +57,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
57
57
  | Rules earned by failure | `references/learned.md` |
58
58
  | **The routing default and its boundary** | `templates/routing-rule.md` |
59
59
  | The seeded doc map, registers and gate | `templates/docmap.md`, `templates/decisions.md`, `templates/open-questions.md`, `templates/docgate.sh` |
60
+ | Which agent-introduced defects are found, and that the agent fixes them rather than the script | `templates/hygiene.sh`, `references/build.md` |
60
61
  | What a stage reads, and which host files bind it | `references/artifacts.md` |
61
62
  | The design conversation, its hard gate, UI detection, user paths | `references/brainstorm.md` |
62
63
  | Cutting a platform into modules, brick criteria, build order | `references/decomposition.md` |
@@ -296,7 +296,9 @@ never that the work was skipped quietly.
296
296
  on the result, land it the project's way (merge, or a PR — outward, so it needs a
297
297
  go), remove the worktree. Stages 7–9 act on the integrated result, so a branch the
298
298
  operator chose to leave unmerged is recorded as such.
299
- - **GATE (auto):** all plan tasks DONE (three review verdicts per task: spec
299
+ - **GATE (auto):** **the hygiene gate green in diff mode after every task**
300
+ (`references/build.md`) — six checks over what that task changed, no floor, and a
301
+ finding fixed in-task or carried over with a reason; all plan tasks DONE (three review verdicts per task: spec
300
302
  compliance, **REQ satisfied**, code quality); every finding fixed or parked with a
301
303
  ruling; **every parked finding and implementer concern harvested into the
302
304
  carry-over ledger** — nothing stays only in the scratch workspace, which is
@@ -311,7 +313,8 @@ never that the work was skipped quietly.
311
313
  - **Invoke:** the host test runner (see `conventions.md` → *Lint + test*); the
312
314
  built-in [`tdd.md`](tdd.md) cycle for any uncovered gap — failing test first,
313
315
  same as stage 5.
314
- - **GATE (auto):** the **full** suite is green (not just the new tests); new/changed code
316
+ - **GATE (auto):** **the hygiene gate green over the whole tree**, its six counts
317
+ printed beside their floors; the **full** suite is green (not just the new tests); new/changed code
315
318
  is covered; no `skip`/`xfail` smuggling a red suite past the gate. Never advance
316
319
  to deploy on a red or partial run. **The carry-over count is printed beside this
317
320
  verdict** — a ratchet nobody prints is a TODO with a better name
@@ -381,7 +384,7 @@ never that the work was skipped quietly.
381
384
  - **Docs living in another repository** are outward: propose the edit, get an
382
385
  explicit go, then open a PR there. No go → the exact edit goes in the carry-over
383
386
  ledger.
384
- - **GATE (auto):** **the propagation matrix walked for every change type this run
387
+ - **GATE (auto):** **the hygiene gate green**, counts printed; **the propagation matrix walked for every change type this run
385
388
  produced**, with every settled thing recorded under an id and every answered
386
389
  question resolved; **the documentation gate green, its ratchet counts printed and
387
390
  any skip stated** — this is what replaced the unfalsifiable *"docs in sync with
@@ -17,6 +17,7 @@ from `super-ux`.
17
17
  | `decisions.md` | `docs/DECISIONS.md` — the decision register | 0 seeds it, the Doc Loop appends |
18
18
  | `open-questions.md` | `docs/OPEN_QUESTIONS.md` | 0 seeds it, the Doc Loop resolves rows |
19
19
  | `docgate.sh` | `scripts/check-docs.sh` | 0 seeds it · 9 runs it · 10 proves it |
20
+ | `hygiene.sh` | `scripts/check-hygiene.sh` | 0 seeds it · **5 runs it after every task** · 6 and 9 run it · 10 proves it |
20
21
  | `hooks.example.json` | the project's `.claude/settings.json` | 0 — offered, never installed silently |
21
22
  | `routing-rule.md` | the operator's `CLAUDE.md` — **offered by `setup`, never written silently** | 0 / `setup` |
22
23
  | `retro.md` | `docs/superpowers/retro.md` — **one per project, not per run** | 10 writes (prune → stamp → entry), 0 reads it in full |
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env bash
2
+ # hygiene.sh — the artifact hygiene gate for <project>.
3
+ #
4
+ # Seeded by task-pipeline (references/gates.md). IT IS YOURS NOW: extend it here,
5
+ # section by section. Each section is independent and removable.
6
+ #
7
+ # WHAT IT IS FOR: the defect class an AGENT produces and no other check looks for —
8
+ # a half-resolved merge, a stub that outlived its task, a generation cut off in the
9
+ # middle, a file "shortened" while being rewritten, a batch of edits where one
10
+ # applied twice, a section opened and abandoned.
11
+ #
12
+ # SCOPE: walks tracked files. In diff mode it walks only what this run changed; in
13
+ # tree mode it walks everything, behind per-check floors.
14
+ # It does NOT check: prose meaning, code correctness, style, formatting, spelling,
15
+ # another repository, or generated output. It is not a linter — your language has
16
+ # one, and this is not it.
17
+ # KNOWN FALSE-POSITIVE SURFACES, one per check, because a gate that hides them
18
+ # gets disabled the first time it is wrong:
19
+ # 1 conflict markers — a document explaining merge conflicts
20
+ # 2 placeholders — only a LINE-LEADING marker counts. The word TODO in the
21
+ # middle of a sentence is prose, not a stub. This
22
+ # distinction is the whole check: measured on the project
23
+ # that ships it, 28 of 33 hits were ordinary English.
24
+ # 3 unterminated fence — a fence deliberately shown unclosed as an example
25
+ # 4 truncation stubs — a document quoting one as an example of what not to do
26
+ # 5 duplicated block — a legitimately repeated stanza 3+ lines long
27
+ # 6 empty section — a heading used as a one-line record. If you have those,
28
+ # they want to be list items: a heading promises a section.
29
+ # Read this header before quoting a green from here as evidence.
30
+ #
31
+ # IT NEVER EDITS. It reports file:line and exits non-zero. None of the six is safely
32
+ # machine-fixable: deleting a "duplicated block" sometimes deletes a legitimate
33
+ # repetition, and deleting a TODO erases a reminder instead of discharging it.
34
+ # Fixing is the agent's job, and task-pipeline's references/build.md makes it one.
35
+ #
36
+ # EXIT CODE IS THE OUTPUT: non-zero on any failure. Nothing may run after the
37
+ # VERDICT block at the bottom — a gate that appended a check after its verdict
38
+ # printed FAIL and returned 0, and CI was green over it for an unknown period.
39
+ #
40
+ # PORTABLE to macOS bash 3.2: no grep -P, no sed -i, no readarray, no mapfile.
41
+ #
42
+ # PROGRESSIVE ARMING: a check whose input does not exist prints
43
+ # "dormant: … — no <input>" and does NOT fail. Dormant is visible so it is not
44
+ # forgotten, and green so a freshly seeded project does not start red.
45
+ #
46
+ # TWO MODES, AND THE DIFFERENCE IS THE POINT:
47
+ # diff mode (HYGIENE_BASE set) — ZERO tolerance. This run wrote it, this run fixes
48
+ # it. There is no floor, because a floor for work you just did is a licence.
49
+ # tree mode (default) — per-check RATCHET FLOORS. A count at or below its
50
+ # floor passes; above it fails. A floor may only fall. This is what lets an
51
+ # existing repository adopt the gate without starting red, while forgiving
52
+ # nothing new.
53
+
54
+ set -u
55
+
56
+ FAIL=0
57
+ HYGIENE_BASE=${HYGIENE_BASE:-}
58
+ HYGIENE_EXCLUDE=${HYGIENE_EXCLUDE:-graphify-out/}
59
+ HYGIENE_FLOOR_1=${HYGIENE_FLOOR_1:-0}
60
+ HYGIENE_FLOOR_2=${HYGIENE_FLOOR_2:-0}
61
+ HYGIENE_FLOOR_3=${HYGIENE_FLOOR_3:-0}
62
+ HYGIENE_FLOOR_4=${HYGIENE_FLOOR_4:-0}
63
+ HYGIENE_FLOOR_5=${HYGIENE_FLOOR_5:-0}
64
+ HYGIENE_FLOOR_6=${HYGIENE_FLOOR_6:-0}
65
+
66
+ TMP=${TMPDIR:-/tmp}/hygiene.$$
67
+ mkdir -p "$TMP" || exit 2
68
+ trap 'rm -rf "$TMP"' EXIT
69
+
70
+ err() { echo " FAIL: $*"; FAIL=1; }
71
+ ok() { echo " ok: $*"; }
72
+ dormant() { echo " dormant: $*"; }
73
+
74
+ # ---------- the file list ----------
75
+ # git is the source of truth for "authored": generated and ignored files are not
76
+ # this gate's business, and scanning them is how a gate becomes noise.
77
+ if git rev-parse --git-dir >/dev/null 2>&1; then
78
+ if [ -n "$HYGIENE_BASE" ]; then
79
+ MODE=diff
80
+ { git diff --name-only "$HYGIENE_BASE" 2>/dev/null
81
+ git diff --name-only 2>/dev/null
82
+ git diff --name-only --cached 2>/dev/null
83
+ } | sort -u > "$TMP/all"
84
+ else
85
+ MODE=tree
86
+ git ls-files > "$TMP/all"
87
+ fi
88
+ else
89
+ MODE=tree
90
+ find . -type f -not -path './.git/*' | sed 's|^\./||' > "$TMP/all"
91
+ fi
92
+
93
+ : > "$TMP/files"
94
+ while read -r f; do
95
+ [ -n "$f" ] || continue
96
+ [ -f "$f" ] || continue
97
+ case "$f" in
98
+ $HYGIENE_EXCLUDE*) continue ;;
99
+ esac
100
+ echo "$f" >> "$TMP/files"
101
+ done < "$TMP/all"
102
+
103
+ NFILES=$(wc -l < "$TMP/files" | tr -d ' ')
104
+ : > "$TMP/md"
105
+ while read -r f; do
106
+ case "$f" in *.md) echo "$f" >> "$TMP/md" ;; esac
107
+ done < "$TMP/files"
108
+ NMD=$(wc -l < "$TMP/md" | tr -d ' ')
109
+
110
+ echo "hygiene gate — mode $MODE · $NFILES file(s), $NMD markdown"
111
+
112
+ # The floor a check is judged against. Diff mode has none: zero tolerance.
113
+ floor_for() {
114
+ if [ "$MODE" = diff ]; then echo 0; return; fi
115
+ eval "echo \${HYGIENE_FLOOR_$1}"
116
+ }
117
+
118
+ judge() { # judge <n> <count> <label>
119
+ _f=$(floor_for "$1")
120
+ if [ "$2" -gt "$_f" ]; then
121
+ err "check $1 — $3: $2 finding(s), floor $_f"
122
+ sed 's/^/ /' "$TMP/hits$1"
123
+ else
124
+ ok "check $1 — $3: $2 (floor $_f)"
125
+ fi
126
+ }
127
+
128
+ if [ "$NFILES" -eq 0 ]; then
129
+ dormant "every check — no files in scope"
130
+ C1=0; C2=0; C3=0; C4=0; C5=0; C6=0
131
+ else
132
+
133
+ # ---------- 1. conflict markers ----------
134
+ : > "$TMP/hits1"
135
+ while read -r f; do
136
+ grep -n -E '^(<<<<<<< |=======$|>>>>>>> )' "$f" 2>/dev/null |
137
+ sed "s|^|$f:|" >> "$TMP/hits1"
138
+ done < "$TMP/files"
139
+ C1=$(wc -l < "$TMP/hits1" | tr -d ' ')
140
+ judge 1 "$C1" "conflict markers"
141
+
142
+ # ---------- 2. surviving placeholders ----------
143
+ # A LINE-LEADING marker, optionally behind a comment introducer. Not the word.
144
+ : > "$TMP/hits2"
145
+ while read -r f; do
146
+ grep -n -E '^[[:space:]]*([#/*<!-]+[[:space:]]*)*(TODO|TBD|FIXME|XXX)\b' "$f" 2>/dev/null |
147
+ sed "s|^|$f:|" >> "$TMP/hits2"
148
+ done < "$TMP/files"
149
+ C2=$(wc -l < "$TMP/hits2" | tr -d ' ')
150
+ judge 2 "$C2" "surviving placeholders"
151
+
152
+ # ---------- 3. unterminated code fence ----------
153
+ : > "$TMP/hits3"
154
+ if [ "$NMD" -eq 0 ]; then
155
+ dormant "check 3 — unterminated fence: no markdown in scope"
156
+ C3=0
157
+ else
158
+ while read -r f; do
159
+ # grep -c exits 1 on zero matches, so a `|| echo 0` would append a SECOND zero
160
+ # and make the test below see "0\n0". Swallow the status instead.
161
+ n=$(grep -c '^[[:space:]]*```' "$f" 2>/dev/null) || n=0
162
+ [ -z "$n" ] && n=0
163
+ if [ "$n" -gt 0 ] && [ "$(expr "$n" % 2)" -ne 0 ]; then
164
+ echo "$f:1: odd fence count ($n) — a code block is not closed" >> "$TMP/hits3"
165
+ fi
166
+ done < "$TMP/md"
167
+ C3=$(wc -l < "$TMP/hits3" | tr -d ' ')
168
+ judge 3 "$C3" "unterminated fence"
169
+ fi
170
+
171
+ # ---------- 4. truncation stubs ----------
172
+ # LINE-LEADING, exactly like check 2, and for exactly the same reason: the first
173
+ # version matched the phrase anywhere and fired three times on the very documents
174
+ # that DEFINE these patterns. A stub is a line standing in for content; a phrase in
175
+ # the middle of a sentence is prose about stubs.
176
+ : > "$TMP/hits4"
177
+ while read -r f; do
178
+ grep -n -E '^[[:space:]]*([#/*<!-]+[[:space:]]*)*(\.\.\. existing code|\[TRUNC|rest of (the )?file unchanged|unchanged\.\.\.)' "$f" 2>/dev/null |
179
+ sed "s|^|$f:|" >> "$TMP/hits4"
180
+ done < "$TMP/files"
181
+ C4=$(wc -l < "$TMP/hits4" | tr -d ' ')
182
+ judge 4 "$C4" "truncation stubs"
183
+
184
+ # ---------- 5. duplicated adjacent block ----------
185
+ # The mechanical form of "re-verify every edit in a batch": a retry that duplicated
186
+ # instead of replacing leaves two identical blocks back to back. Three lines is the
187
+ # floor because two-line repeats occur legitimately (table rows, list pairs).
188
+ : > "$TMP/hits5"
189
+ while read -r f; do
190
+ awk -v F="$f" '
191
+ NF { blk = blk "\n" $0; n++; next }
192
+ {
193
+ if (n >= 3) { if (blk == prev) print F ":" NR ": duplicated " n "-line block"; prev = blk }
194
+ else prev = ""
195
+ blk = ""; n = 0
196
+ }
197
+ END { if (n >= 3 && blk == prev) print F ":" NR ": duplicated " n "-line block at end of file" }
198
+ ' "$f" >> "$TMP/hits5" 2>/dev/null
199
+ done < "$TMP/files"
200
+ C5=$(wc -l < "$TMP/hits5" | tr -d ' ')
201
+ judge 5 "$C5" "duplicated adjacent block"
202
+
203
+ # ---------- 6. empty section ----------
204
+ # A heading of level N followed by a heading of level <= N with no body between.
205
+ # FENCED CONTENT COUNTS AS BODY — the first version of this check skipped fenced
206
+ # lines entirely and so reported every section whose body is one code block.
207
+ : > "$TMP/hits6"
208
+ if [ "$NMD" -eq 0 ]; then
209
+ dormant "check 6 — empty section: no markdown in scope"
210
+ C6=0
211
+ else
212
+ while read -r f; do
213
+ awk -v F="$f" '
214
+ /^[[:space:]]*(```|~~~)/ { fence = !fence; prev = 0; next }
215
+ fence { prev = 0; next }
216
+ /^#{1,6} / {
217
+ match($0, /^#+/); lvl = RLENGTH
218
+ if (prev && lvl <= prev) print F ":" NR ": empty section — h" prev " then h" lvl " with no body"
219
+ prev = lvl; next
220
+ }
221
+ NF { prev = 0 }
222
+ ' "$f" >> "$TMP/hits6" 2>/dev/null
223
+ done < "$TMP/md"
224
+ C6=$(wc -l < "$TMP/hits6" | tr -d ' ')
225
+ judge 6 "$C6" "empty section"
226
+ fi
227
+
228
+ fi
229
+
230
+ # ---------- VERDICT — nothing may run after this block ----------
231
+ if [ "$FAIL" -ne 0 ]; then
232
+ echo "FAIL: hygiene gate — mode $MODE · conflict ${C1} · placeholder ${C2} · fence ${C3} · truncation ${C4} · duplicate ${C5} · empty-section ${C6}"
233
+ exit 1
234
+ fi
235
+ echo "OK: hygiene gate — mode $MODE · $NFILES file(s) · conflict ${C1} · placeholder ${C2} · fence ${C3} · truncation ${C4} · duplicate ${C5} · empty-section ${C6}"
236
+ exit 0
@@ -48,7 +48,13 @@ stays short precisely so that reading it in full at stage 0 stays cheap.
48
48
  - **Upstream?** … (a lesson true in any repo belongs in the skill's own
49
49
  `references/learned.md` — open an issue and say so here)
50
50
 
51
- ### 2026-05-04 · retired R-000 — became a check (`npm run lint:paths`) · `<sha>`
51
+ ### Retirements
52
+
53
+ One line each, newest first. A retirement is a record, not a section — so it is a
54
+ list item. A heading promises a body, and a heading with nothing under it is the
55
+ shape the hygiene gate's check 6 exists to find.
56
+
57
+ - 2026-05-04 · retired R-000 — became a check (`npm run lint:paths`) · `<sha>`
52
58
 
53
59
  ## Run stamps
54
60