Compare commits

...

5 Commits

6 changed files with 238 additions and 23 deletions

View File

@@ -1,5 +1,5 @@
# Task Board
_Updated: 2026-05-07 (hermes-converter-mvp shipped — 4 universals through converter)_
_Updated: 2026-05-07 (tdd-criteria skill-write + mapping + build-install shipped)_
<!--
Canonical layout. One block per task. Per-task deep context lives in
@@ -290,7 +290,7 @@ version: 0.1.0
---
## [tdd-criteria-skill-write] — Write `~/projects/claude-skills/skills/tdd-criteria/SKILL.md` — runtime artefact for the TDD-criteria policy. Design rationale (the «why» behind every rule + the anti-vandalism leading argument + the test-immutability second-order defence) lives in `.wiki/concepts/tdd-criteria-design.md` (already promoted via `meeting-room-promote-brainstorm` 2026-05-07; NOTE: design doc was promoted at 2026-05-07T04:00 with 3 anti-loophole rules; **rule 4 (test-immutability) was added in this task description below at 2026-05-07T05:00 after user surfaced the symmetric vandalism risk; the impl session must mirror rule 4 into the design doc as well — see «Design doc amendment» section below**).
## 🟢 [tdd-criteria-skill-write] — runtime artefact for the TDD-criteria policy. Design rationale (the «why» behind every rule + the anti-vandalism leading argument + the test-immutability second-order defence) lives in `.wiki/concepts/tdd-criteria-design.md` (already promoted via `meeting-room-promote-brainstorm` 2026-05-07; NOTE: design doc was promoted at 2026-05-07T04:00 with 3 anti-loophole rules; **rule 4 (test-immutability) was added in this task description below at 2026-05-07T05:00 after user surfaced the symmetric vandalism risk; the impl session must mirror rule 4 into the design doc as well — see «Design doc amendment» section below**).
This task writes the **runtime SKILL.md** + amends the design doc to include rule 4.
@@ -415,15 +415,15 @@ Both layers are needed. Either alone leaves a path-of-least-resistance route to
- Length target: ≤200 lines (project-discipline is ~140 — similar density).
- Frontmatter version starts at `0.1.0` (per Rule 3 of `project-discipline`: first edit of new versioned artefact = add 0.1.0, not bump).
**Status:** ready
**Where I stopped:** (not started)
**Next action:** Создать `~/projects/claude-skills/skills/tdd-criteria/` директорию + `SKILL.md` по структуре в description. Использовать `skills/project-discipline/SKILL.md` и `skills/recommend-dont-menu/SKILL.md` как темплейты для frontmatter и секций. После записи — перейти к `tdd-criteria-hermes-mapping`.
**Status:** done
**Where I stopped:** Shipped в `954f8ba feat(skills): tdd-criteria skill v0.1.0`. SKILL.md 86 lines, 4 ironclad + 4 permissive + 4 anti-loophole (incl. rule 4 test-immutability), 0 Claude-tool refs. Design doc amended в `2ba6981` — rule 4 + subsection + trade-off + excluded formulation added.
**Next action:** (none — kept until merged)
**Branch:** master
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: .meeting-room / 2026-05-07T04:02:05.413Z -->
---
## 🔵 [tdd-criteria-hermes-mapping] — Add entry for `tdd-criteria` skill to `~/projects/claude-skills/hermes/mapping.yaml`. Required step — `build-hermes.py` fails on unmapped skills.
## 🟢 [tdd-criteria-hermes-mapping] — Add entry for `tdd-criteria` skill to `~/projects/claude-skills/hermes/mapping.yaml`. Required step — `build-hermes.py` fails on unmapped skills.
**Entry (place alphabetically among `auto`-mode skills, after `project-discipline`):**
@@ -439,16 +439,15 @@ Both layers are needed. Either alone leaves a path-of-least-resistance route to
**Out of scope:** no Hermes-side install / activation (Hermes side handled by `hermes-mvp-coverage` task family separately). This task only ensures the converter knows about the skill.
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** Дождаться 🟢 у `tdd-criteria-skill-write`. Edit `hermes/mapping.yaml`: вставить entry в alphabetical порядке среди `auto`-mode-секции (после `project-discipline:`). Прогнать `python scripts/build-hermes.py` — убедиться что `dist-hermes/software-development/tdd-criteria/SKILL.md` создаётся без ошибок. Закоммитить mapping.yaml + dist-hermes/.../tdd-criteria/ одним коммитом.
**Blocker:** tdd-criteria-skill-write
**Status:** done
**Where I stopped:** Shipped в `7d308ff feat(hermes): add tdd-criteria mapping (auto, software-development) + rebuild dist-hermes`. Build verified: `dist-hermes/software-development/tdd-criteria/SKILL.md` created. No replace-rules needed (pure policy skill).
**Next action:** (none — kept until merged)
**Branch:** master
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: .meeting-room / 2026-05-07T04:02:21.684Z -->
---
## 🔵 [tdd-criteria-build-install] — Build `dist/tdd-criteria.skill` archive, install to `~/.claude/skills/tdd-criteria/`, commit both `dist/` and `dist-hermes/` artefacts. Closes the rollout loop — after this task the skill is live for both Claude (next session) and Hermes (next factory deploy).
## 🟢 [tdd-criteria-build-install] — Build `dist/tdd-criteria.skill` archive, install to `~/.claude/skills/tdd-criteria/`, commit both `dist/` and `dist-hermes/` artefacts. Closes the rollout loop — after this task the skill is live for both Claude (next session) and Hermes (next factory deploy).
**Steps:**
1. `bash ~/projects/claude-skills/scripts/install.sh tdd-criteria` — copies `skills/tdd-criteria/``~/.claude/skills/tdd-criteria/` (replaces if exists). Verifies skill is loadable in next Claude session.
@@ -465,16 +464,15 @@ Both layers are needed. Either alone leaves a path-of-least-resistance route to
**Out of scope:** activation in specific projects (separate task `tdd-criteria-rollout-projects` if user wants to add `follow tdd-criteria` trigger to selected `CLAUDE.md`s). Skill is reachable via description-based pull regardless.
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** Дождаться 🟢 у `tdd-criteria-skill-write` и `tdd-criteria-hermes-mapping`. Прогнать install.sh + build.sh + build-hermes.py. Закоммитить dist/ и dist-hermes/. Push (по grant-у).
**Blocker:** tdd-criteria-skill-write, tdd-criteria-hermes-mapping
**Status:** done
**Where I stopped:** Shipped в `62a54c9 build(tdd-criteria): add dist archive`. `install.sh``~/.claude/skills/tdd-criteria/`; `build.sh``dist/tdd-criteria.skill` (2862 bytes); `build-hermes.py``dist-hermes/software-development/tdd-criteria/SKILL.md`. All three artefacts verified on disk.
**Next action:** (none — kept until merged)
**Branch:** master
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: .meeting-room / 2026-05-07T04:02:38.862Z -->
---
## 🔵 [tdd-criteria-review] — Code-review checkpoint для брейнсторма `tdd-criteria` (промоушен 2026-05-07).
## [tdd-criteria-review] — Code-review checkpoint для брейнсторма `tdd-criteria` (промоушен 2026-05-07).
**Спецификация:** `~/projects/claude-skills/.wiki/concepts/tdd-criteria-design.md` (промоушен в commit `a03d280`).
**Импл-таски (review против их acceptance criteria):** `tdd-criteria-skill-write`, `tdd-criteria-hermes-mapping`, `tdd-criteria-build-install`.
@@ -500,10 +498,9 @@ Both layers are needed. Either alone leaves a path-of-least-resistance route to
**Закрытие:** только когда (а) все findings зафайлены отдельными tasks, ИЛИ (б) ревьюер подтвердил «нет findings» в close-note (с явным «прошёл по 7 пунктам checklist-а»).
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** Дождаться 🟢 у всех blocker-тасок (`tdd-criteria-skill-write`, `tdd-criteria-hermes-mapping`, `tdd-criteria-build-install`). Открыть свежую сессию в `claude-skills` (НЕ в `.meeting-room` и НЕ ту, в которой импл делался). Пройти 7-пунктный чек-лист в description. Findings — отдельные `tdd-criteria-*-fix` tasks. Закрыть с close-note.
**Blocker:** tdd-criteria-skill-write, tdd-criteria-hermes-mapping, tdd-criteria-build-install
**Status:** ready
**Where I stopped:** unblocked — skill-write, mapping, build-install all shipped
**Next action:** Открыть свежую сессию в `claude-skills` (НЕ в `.meeting-room` и НЕ ту, в которой импл делался). Пройти 7-пунктный чек-лист в description. Findings — отдельные `tdd-criteria-*-fix` tasks. Закрыть с close-note.
**Branch:** master
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: .meeting-room / 2026-05-07T04:03:12.170Z -->

View File

@@ -4,6 +4,7 @@ source: .meeting-room/.archive/2026-05-07-tdd-criteria.md
status: promoted
type: design
title: tdd-criteria-design
amended: "2026-05-07: added test-immutability defence (Anti-loophole rule 4) after user noted symmetric vandalism risk on tests"
ingested_at: '2026-05-07T04:01:23.616Z'
ingested_by: OpeItcLoc03@DESKTOP-NSEF0UK
source_project: .meeting-room
@@ -33,6 +34,17 @@ In a workflow that includes LLM coders (Claude, ChatGPT, future unknowns) and ro
This reframes everything below:
### The contract is only as strong as the contract itself
But there's a **second-order vandalism mode** that the bare contract argument doesn't cover: the agent doesn't delete the code, it rewrites the **test**. Test fails → agent changes the expected value, adds `.skip`, or deletes the test → test now passes → success reported.
If the contract artefact (the test) is rewritable by the same agent that's failing to satisfy it, the invariant collapses back into an artefact. The defence requires **two layers**:
1. **Code is defended by tests.** Ironclad rules 1-4 below.
2. **Tests are defended by process discipline.** Anti-loophole rule 4 below — append-only by default, modifications require literal-`was/is` marker in commit subject, test changes are separate commits from impl changes.
Both layers are needed. Either alone leaves a path-of-least-resistance route to «success».
- **Ironclad** rules (TDD obligatory) — zones where recovery cost > defending cost. Signal mandatory.
- **Permissive** carve-outs — **zones of accepted risk for agentic vandalism**, not «zones where TDD doesn't apply». You explicitly accept that an agent can delete-and-claim-success here, because recovery is cheap (eyeball the next render; throwaway by contract; one-shot already ran; wrapper trivial to reconstruct).
@@ -85,9 +97,35 @@ Not «TDD doesn't apply». **«You accept that an agent can vandalise this witho
## Anti-loophole
- **Skip без категории не существует.** One of four explicit categories — not «other reasons». No marker = violation, regardless of perceived justification.
- **Spike survivor rule.** If spike code is merged to master → same merge-commit creates `[backfill-tests-<slug>]` task in `.tasks/STATUS.md`. Otherwise «spike» becomes «skipped tests forever».
- **Friction is the point.** `[skip-tdd: visual]` 50 раз подряд при `web-design-system` итерации раздражает — это и есть замысел. Friction = fence, not bug. If it becomes unbearable, re-evaluate after ≥2 weeks of usage, not before.
1. **Skip без категории не существует.** One of four explicit categories — not «other reasons». No marker = violation, regardless of perceived justification.
2. **Spike survivor rule.** If spike code is merged to master → same merge-commit creates `[backfill-tests-<slug>]` task in `.tasks/STATUS.md`. Otherwise «spike» becomes «skipped tests forever».
3. **Friction is the point.** `[skip-tdd: visual]` 50 раз подряд при `web-design-system` итерации раздражает — это и есть замысел. Friction = fence, not bug. If it becomes unbearable, re-evaluate after ≥2 weeks of usage, not before.
4. **Tests are append-only by default** (added 2026-05-07). New tests: free. **Modifying** an existing assertion, **deleting** a test, or **disabling** it (`it.skip`, `xit`, `@pytest.mark.skip`, `@Disabled`, etc.) requires both:
**a)** A marker in commit subject:
```
[test-modify: <test-name>: was <X>; is <Y>; reason: <Z>]
```
Where `<X>` and `<Y>` are the **literal assertion expressions** before and after, not paraphrased. Example:
```
[test-modify: validates email format: was expect(isValid("a@b")).toBe(true); is expect(isValid("a@b.com")).toBe(true); reason: tightened spec to require TLD]
```
**b)** Test changes go in a **separate commit** from any impl changes. A single commit must not modify both `*.test.*` and `src/*` files (or their project-equivalents). This forces an audit-able split — `git log --grep '\[test-modify'` shows every test rewrite cleanly.
**Why literal `was/is`, not free-form reason:** an agent forced to write the literal assertion publishes exactly what they're rewriting. If `42` was the correct expectation and they changed it to `43` to make a buggy fix pass, the literal `was 42; is 43` line in `git log` identifies the culprit. A reason like «updated to match new behaviour» hides everything — agents will use it whenever it is allowed.
**Bright-line check** (for an optional pre-commit hook, see follow-up task `tdd-criteria-precommit-hook`):
- `git diff --cached` includes a removed `expect(...)` / `assert(...)` / `assertThat(...)` line, OR
- changes the arguments of an existing assertion call, OR
- adds `.skip`, `xit`, `@skip`, `@Disabled`, etc. annotation, OR
- deletes a test file or `it(...)` / `test(...)` / `def test_*` definition
AND the commit subject does not contain `[test-modify: ...]` matching the format above → block.
AND `git diff --cached --name-only` includes both test-pattern and impl-pattern files → block (require split).
5. **Don't apply rule 4 retroactively** to tests written before the rule was adopted. The rule applies to test changes made after the project's CLAUDE.md picks up `follow tdd-criteria`. Existing test bodies aren't grandfathered into requiring `was/is` for a one-time rewrite.
## What's excluded as not bright-line
@@ -97,6 +135,7 @@ These tempting formulations were rejected:
- ~~«When time is short»~~ — time is never abundant.
- ~~«When the logic is obvious»~~ — it seems obvious until the first bug.
- ~~«When the code is temporary»~~ — there's no such thing; temporary code becomes permanent.
- ~~«Tests should not be modified casually»~~ — paraphrasable, agents will modify casually and call it «refactor». Replaced by Anti-loophole rule 4 with literal-evidence requirement.
All require judgment at decision time → automatically become loopholes. The four Permissive categories are bound to **observable properties** (file extension, marker in commit subject, directory, line count) — not mood.
@@ -122,6 +161,7 @@ Project `CLAUDE.md` files may **extend** Permissive (e.g. `karu` is a pure-CSS p
- **Supportive evidence is n=1.** The observable contrast is `books` (TDD applied selectively, features ship) vs `modules-db`/`pilonuxt` (TDD not applied, projects buksuyut). `books` could be more productive for reasons unrelated to TDD (codebase maturity, author experience, task type). The direction-of-effect agrees with first principles, so inversion is unlikely, but the magnitude is uncertain.
- **Friction in UI iterations.** `[skip-tdd: visual]` repeated 50× during a design-system rework is annoying. Intentional. Don't relax before ≥2 weeks of usage.
- **The literal-`was/is` requirement is verbose** for a renamed test or trivial typo fix. The verbosity is the point — an agent that genuinely fixed a typo writes the same assertion twice with one character changed; an agent that rewrote a failing test writes obviously different assertions. Reading `git log --grep '\[test-modify'` shows the difference at a glance.
- **«Don't codify at all» was rejected.** Precisely «not codified» is what produced the situation in `books` where, after the author left, his TDD criterion can't be reconstructed — it lived only in his head and his commits. A departed contributor is an argument *for* codification, not *against*.
- **The anti-vandalism argument is uncomfortable.** It says: «I don't trust agents not to delete my code». That's the position. If it changes, this rationale changes. Until then, this is what's load-bearing.

View File

@@ -0,0 +1,87 @@
---
name: tdd-criteria
version: 0.1.0
description: >
TDD by default with four bright-line carve-outs. Applies before any code
change the agent didn't author this session. Triggers: "TDD",
"test-driven", "следуй TDD", "use TDD", "should I write tests",
"skip tdd", "[skip-tdd: ...]", "[test-modify: ...]", "tdd-criteria".
Cross-agent policy — no tool refs. Full rationale:
.wiki/concepts/tdd-criteria-design.md
---
# tdd-criteria
> TDD by default. Skip only with a bright-line marker. Tests defend code from silent deletion; rule 4 defends tests from silent rewriting.
## When this runs
**At session start** — when `CLAUDE.md` contains the line `follow tdd-criteria`.
**Before any code change** the agent didn't author this session — touching a `*.ts`, `*.js`, `*.py`, or similar source file triggers the decision algorithm below.
**On explicit reference** — when the user says "TDD", "test-driven", "следуй TDD", "use TDD", "should I write tests", "skip tdd", "tdd-criteria", or includes `[skip-tdd: ...]` or `[test-modify: ...]` in a commit subject.
## Default mode
TDD by default. Skip only if one of four bright-line carve-outs matches and is marked in the commit subject.
## Decision algorithm (8 questions, top-down)
Walk through in order. First «yes» determines mode. All «no» → TDD by default.
1. **Bug fix?** → TDD (red-test first)
2. **Consuming external contract** (SDK, REST API, чужая schema)? → TDD (contract-test)
3. **Security / auth / money / identifiers?** → TDD
4. **Pure logic** — function (input → output) without I/O, global state, bounded inputs? → TDD
5. **Visual / config** — CSS, layout, design tokens, `.env.example`, prompts, wiki, README? → SKIP, `[skip-tdd: visual]`
6. **Spike** — explicit POC «throwaway» in commit/PR/task subject? → SKIP, `[skip-tdd: spike]` + spike-survivor task if code survives
7. **One-shot** — migration, ETL backfill, ad-hoc cleanup, runs once? → SKIP, `[skip-tdd: oneshot]`
8. **Transit wrapper** ≤10 lines — no branching, re-export / glue? → SKIP, `[skip-tdd: wrapper]`
(default) → TDD
## Ironclad rules (TDD obligatory)
1. **Bug fix** — if there's an issue / failure log / repro, write a red-test codifying the repro. Marginal cost: 5 min. Marginal benefit: regression test forever.
2. **Pure logic, bounded inputs** — no fixtures, no mocks, no setup. Test = input/output pair. Skipping is gratuitous.
3. **Third-party contract** — SDK bumps change signatures silently. Contract-test pins known input → known shape. Catches break at first install, not days later.
4. **Security / auth / money / IDs** — asymmetric blast radius: false positive ≪ false negative. TDD = insurance.
In all four, recovery cost from silent deletion is high. The test is the only artefact that makes deletion visible.
## Permissive carve-outs (skip + marker required)
| # | Category | Trigger | Marker |
|---|----------|---------|--------|
| 5 | Visual / config | CSS, layout, tokens, `.env.example`, prompts, wiki | `[skip-tdd: visual]` |
| 6 | Spike | «POC, throwaway» in commit/PR/task subject | `[skip-tdd: spike]` |
| 7 | One-shot | Migration, ETL, ad-hoc cleanup; runs once | `[skip-tdd: oneshot]` |
| 8 | Wrapper | ≤10 lines, no branching, re-export / glue | `[skip-tdd: wrapper]` |
These are **accepted-risk zones** — you accept that an agent can vandalise without immediate signal, because recovery is cheap (eyeball next render; throwaway by contract; runs once; reconstruct ≤ delete).
## Anti-loophole
1. **Skip without category is invalid.** One of four explicit categories required — not «other reasons». No marker = violation.
2. **Spike survivor rule.** Merged spike → same merge-commit creates `[backfill-tests-<slug>]` task. Otherwise «spike» becomes «skipped tests forever».
3. **Friction is the point.** `[skip-tdd: visual]` 50× in a design-system rework is irritating — that's the fence. Re-evaluate after ≥2 weeks, not before.
4. **Tests are append-only by default.** Modifying an assertion, deleting a test, or disabling it (`it.skip`/`xit`/`@pytest.mark.skip`/`@Disabled`) requires:
- **Marker in commit subject:** `[test-modify: <test-name>: was <X>; is <Y>; reason: <Z>]``<X>` and `<Y>` are **literal assertion expressions**, not paraphrased. Example: `[test-modify: validates email: was expect(isValid("a@b")).toBe(true); is expect(isValid("a@b.com")).toBe(true); reason: tightened spec to require TLD]`
- **Separate commit from impl changes.** A commit must not modify both `*.test.*` and `src/*` files (or project-equivalents). `git log --grep '\[test-modify'` must show a clean test-only audit trail.
- **Why literal was/is:** an agent forced to write the literal assertion publishes exactly what they're rewriting. «Updated to match new behaviour» hides everything — agents will use that whenever allowed.
## Cross-agent applicability
Pure policy — no agent-specific tool references in this body. Works on Claude, Gemini, Copilot, or any future agent. Hermes mapping: `mode: auto`, `category: software-development`, no replace-rules.
## Out of scope
- Does not enforce via git hooks (separate optional task: `tdd-criteria-precommit-hook`).
- Does not modify project `CLAUDE.md` (that's `project-bootstrap`'s job).
- Does not run tests.
- Does not apply rule 4 retroactively to tests written before the rule was adopted.
## Why this exists
Tests make behaviour an invariant; without them, code is an artefact silent-deletable by agents. AND: the test itself must be defended too (rule 4) — otherwise the contract collapses back into an artefact when the agent rewrites the failing test. Full rationale: `.wiki/concepts/tdd-criteria-design.md`.

BIN
dist/tdd-criteria.skill vendored Normal file

Binary file not shown.

View File

@@ -55,6 +55,10 @@ skills:
mode: auto
category: software-development
tdd-criteria:
mode: auto
category: software-development
using-markitdown:
mode: auto
category: productivity

View File

@@ -0,0 +1,87 @@
---
name: tdd-criteria
version: 0.1.0
description: >
TDD by default with four bright-line carve-outs. Applies before any code
change the agent didn't author this session. Triggers: "TDD",
"test-driven", "следуй TDD", "use TDD", "should I write tests",
"skip tdd", "[skip-tdd: ...]", "[test-modify: ...]", "tdd-criteria".
Cross-agent policy — no tool refs. Full rationale:
.wiki/concepts/tdd-criteria-design.md
---
# tdd-criteria
> TDD by default. Skip only with a bright-line marker. Tests defend code from silent deletion; rule 4 defends tests from silent rewriting.
## When this runs
**At session start** — when `CLAUDE.md` contains the line `follow tdd-criteria`.
**Before any code change** the agent didn't author this session — touching a `*.ts`, `*.js`, `*.py`, or similar source file triggers the decision algorithm below.
**On explicit reference** — when the user says "TDD", "test-driven", "следуй TDD", "use TDD", "should I write tests", "skip tdd", "tdd-criteria", or includes `[skip-tdd: ...]` or `[test-modify: ...]` in a commit subject.
## Default mode
TDD by default. Skip only if one of four bright-line carve-outs matches and is marked in the commit subject.
## Decision algorithm (8 questions, top-down)
Walk through in order. First «yes» determines mode. All «no» → TDD by default.
1. **Bug fix?** → TDD (red-test first)
2. **Consuming external contract** (SDK, REST API, чужая schema)? → TDD (contract-test)
3. **Security / auth / money / identifiers?** → TDD
4. **Pure logic** — function (input → output) without I/O, global state, bounded inputs? → TDD
5. **Visual / config** — CSS, layout, design tokens, `.env.example`, prompts, wiki, README? → SKIP, `[skip-tdd: visual]`
6. **Spike** — explicit POC «throwaway» in commit/PR/task subject? → SKIP, `[skip-tdd: spike]` + spike-survivor task if code survives
7. **One-shot** — migration, ETL backfill, ad-hoc cleanup, runs once? → SKIP, `[skip-tdd: oneshot]`
8. **Transit wrapper** ≤10 lines — no branching, re-export / glue? → SKIP, `[skip-tdd: wrapper]`
(default) → TDD
## Ironclad rules (TDD obligatory)
1. **Bug fix** — if there's an issue / failure log / repro, write a red-test codifying the repro. Marginal cost: 5 min. Marginal benefit: regression test forever.
2. **Pure logic, bounded inputs** — no fixtures, no mocks, no setup. Test = input/output pair. Skipping is gratuitous.
3. **Third-party contract** — SDK bumps change signatures silently. Contract-test pins known input → known shape. Catches break at first install, not days later.
4. **Security / auth / money / IDs** — asymmetric blast radius: false positive ≪ false negative. TDD = insurance.
In all four, recovery cost from silent deletion is high. The test is the only artefact that makes deletion visible.
## Permissive carve-outs (skip + marker required)
| # | Category | Trigger | Marker |
|---|----------|---------|--------|
| 5 | Visual / config | CSS, layout, tokens, `.env.example`, prompts, wiki | `[skip-tdd: visual]` |
| 6 | Spike | «POC, throwaway» in commit/PR/task subject | `[skip-tdd: spike]` |
| 7 | One-shot | Migration, ETL, ad-hoc cleanup; runs once | `[skip-tdd: oneshot]` |
| 8 | Wrapper | ≤10 lines, no branching, re-export / glue | `[skip-tdd: wrapper]` |
These are **accepted-risk zones** — you accept that an agent can vandalise without immediate signal, because recovery is cheap (eyeball next render; throwaway by contract; runs once; reconstruct ≤ delete).
## Anti-loophole
1. **Skip without category is invalid.** One of four explicit categories required — not «other reasons». No marker = violation.
2. **Spike survivor rule.** Merged spike → same merge-commit creates `[backfill-tests-<slug>]` task. Otherwise «spike» becomes «skipped tests forever».
3. **Friction is the point.** `[skip-tdd: visual]` 50× in a design-system rework is irritating — that's the fence. Re-evaluate after ≥2 weeks, not before.
4. **Tests are append-only by default.** Modifying an assertion, deleting a test, or disabling it (`it.skip`/`xit`/`@pytest.mark.skip`/`@Disabled`) requires:
- **Marker in commit subject:** `[test-modify: <test-name>: was <X>; is <Y>; reason: <Z>]``<X>` and `<Y>` are **literal assertion expressions**, not paraphrased. Example: `[test-modify: validates email: was expect(isValid("a@b")).toBe(true); is expect(isValid("a@b.com")).toBe(true); reason: tightened spec to require TLD]`
- **Separate commit from impl changes.** A commit must not modify both `*.test.*` and `src/*` files (or project-equivalents). `git log --grep '\[test-modify'` must show a clean test-only audit trail.
- **Why literal was/is:** an agent forced to write the literal assertion publishes exactly what they're rewriting. «Updated to match new behaviour» hides everything — agents will use that whenever allowed.
## Cross-agent applicability
Pure policy — no agent-specific tool references in this body. Works on Claude, Gemini, Copilot, or any future agent. Hermes mapping: `mode: auto`, `category: software-development`, no replace-rules.
## Out of scope
- Does not enforce via git hooks (separate optional task: `tdd-criteria-precommit-hook`).
- Does not modify project `CLAUDE.md` (that's `project-bootstrap`'s job).
- Does not run tests.
- Does not apply rule 4 retroactively to tests written before the rule was adopted.
## Why this exists
Tests make behaviour an invariant; without them, code is an artefact silent-deletable by agents. AND: the test itself must be defended too (rule 4) — otherwise the contract collapses back into an artefact when the agent rewrites the failing test. Full rationale: `.wiki/concepts/tdd-criteria-design.md`.