# Reviewer lane R3 — safety-wave

> RETURN-only /fable-clone reviewer ensemble wf_78a84f9b-03e. Lead ghi (B3). agentId=a196f3052f681bacf

---

VERDICT: FAIL — W0.3's fixup mechanism provably breaks on this repo's exact state (detached HEAD → hook commits onto it → `--abort` destroys work, fault-injected end-to-end), W0.2's "dirty=0 / cửa-sổ sạch" premise is already false, and the push-guard deadlocks on first use because the squash and the count live in different turns.

Measured at review time (all read-only, real repo unless marked temp-tree):

---

## C — CRITICAL

**C1. W0.3's primary mechanism (`GIT_SEQUENCE_EDITOR` → all `pick <sha> wal:` → `fixup`) HARD-FAILS today, and the failure state destroys uncommitted work.**
- **what · where:** spec §2.0 W0.3 + §3.8 row "`rebase -i` bị chặn harness → `GIT_SEQUENCE_EDITOR` + **fixup** (KHÔNG drop)".
- **WHY sai:** `git merge-base --is-ancestor 3fde8af origin/main` → **YES**; `git rev-parse origin/main` = `3fde8af` exactly. So `origin/main..HEAD` = **5 commits, 5/5 `wal:`** — there is **no non-`wal:` commit in the range to fixup INTO**. `git rebase -i origin/main` only exposes commits *above* origin/main, so every todo line becomes `fixup`.
  Fault-injected in a temp tree (1 pushed base + 5 `wal:`, identical shape):
  ```
  error: cannot 'fixup' without a previous commit   ×5
  REBASE-EXIT=1
  .git/rebase-merge  → REBASE-STATE-LEFT-BEHIND
  git rev-parse --abbrev-ref HEAD → HEAD      (DETACHED)
  ```
  Then, still in that state, I replicated `.claude/hooks/wal-flush.ps1:7-17` verbatim (it is unconditional and fail-open — `exit 0`, `2>$null`, **no rebase-state guard**):
  ```
  hook commit landed at: f6e05f9 on HEAD        ← detached
  git rebase --abort  →  branch main
  grep -c PRECIOUS... wal.md → 0                ← WORKTREE: *** GONE ***
  merge-base --is-ancestor f6e05f9 HEAD → *** ORPHANED (reflog only) ***
  ```
  This is `E-029` reached by a route W0.2 does not guard. The spec's only mention of this case is a parenthetical "**Biên:** wal: ở đáy range … ⇒ giữ nhánh `reset --soft HEAD~K`" — it is stated as an *edge*, not as **the branch that fires today**, and no runtime discriminator is written. A worker that gets the spec via args and does not Read `session-end.md` will run the documented `fixup` path.
- **FIX:** W0.3 must specify an explicit runtime branch **before** any rebase, plus a hook interlock:
  1. `RANGE=$(git rev-list origin/main..HEAD)`; `NONWAL=$(git log --format='%s' origin/main..HEAD | grep -vc '^wal:' || true)`
  2. `NONWAL == 0` → **`reset --soft HEAD~K` ONLY. Never invoke rebase.**
  3. `NONWAL > 0` → rebase+fixup, and the script must leave the **first** todo line as `pick` (or `reword`) — never emit a leading `fixup`.
  4. Any rebase path must be wrapped: `trap 'git rebase --abort' ERR` + a hard rule "if `.git/rebase-merge` or `.git/rebase-apply` exists → STOP, do not let a turn end". Optionally add a rebase-state guard to `wal-flush.ps1` (`if (Test-Path .git/rebase-merge) { exit 0 }`) — otherwise the hook writes to a detached HEAD.
- **VERIFY:** re-run the temp-tree injection above → expect `reset --soft` branch chosen, `REBASE-EXIT` never non-zero, and `ls .git/rebase-merge` → absent after the step.

**C2. `K`/`n` is stale and still moving — three different values in three artifacts within one morning. A literal `K` either deadlocks or force-pushes over a pushed commit.**
- **what · where:** spec §1.1 "n=4 chưa-đẩy"; §2.0 W0.3 "ca **n=4=K** hôm nay"; §3.9.
- **WHY sai:** `.claude/WAL.md:7` says **n=3**; spec says **n=4**; `git rev-list --count origin/main..HEAD` **now = 5** (`110ade2 wal: flush 20260715T0940` landed after the spec was written — it committed `lead-evidence-hub-inspect.md`). The Stop-hook fires at **every** turn boundary, so K grows monotonically until the squash runs. Two failure modes:
  - `K` too small (4 when 5) → 1 `wal:` survives → push-guard `n=0` fails → **deadlock**.
  - `K` too large → `HEAD~K` lands **below** `origin/main` → branch behind remote → `git push` rejected non-FF → the tempting "fix" is `--force`, which **rewrites `3fde8af` on the shared remote** = the exact N.4 floor §3.5 spends three paragraphs defending. The off-by-one is not hypothetical — it is already present in the spec.
- **FIX:** delete every literal `n=4`/`K` from spec and §5.0 text. Mandate: `K=$(git rev-list --count origin/main..HEAD)`, then two asserts before touching anything — (i) `[ "$(git log --format='%s' origin/main..HEAD | grep -vc '^wal:' || true)" -eq 0 ]`, (ii) `[ "$(git rev-parse HEAD~$K)" = "$(git rev-parse origin/main)" ]`. Abort loudly if either fails.
- **VERIFY:** `git rev-list --count origin/main..HEAD` immediately before and after the squash; assert `HEAD~K == origin/main` pre-reset.

**C3. Push-guard deadlocks on first use: the count is moved into the push turn, the squash is not.**
- **what · where:** spec §2.1 row `push-guard`: "Chuyển **phép đếm** vào chính khối §5.2, chained ngay trước `git push origin main`"; acceptance "(ii) đếm **ngay trước** push = 0 **trong CÙNG transcript-turn** với `git push`".
- **WHY sai:** the Stop hook (`.claude/settings.json` → `wal-flush.ps1`) commits `.claude/WAL.md`, `.claude/agent-memory/`, `.claude/workflows/runs/` at **every turn boundary**. The spec mandates count+push share a turn but says nothing about **§5.0 (squash) sharing that turn**. Between §5.0 and §5.2 the closeout provably dirties hook paths: Sàn-2 writes `closing:` into WAL at "session-end BƯỚC 0" (§2.1 Sàn-2), W4 spawns a reviewer (agent-memory write), W4/W5 write into `runs/<id>/`. Each such turn boundary → one new `wal:` → `n≥1` at §5.2 → **guard blocks push → session cannot close**. The predictable human response is to bypass the guard, i.e. the gate deletes itself.
  Historical confirmation that the current design only survives because §5.0..§5.2 happen to be one turn: `3fde8af` (S118 closeout, pushed) @16:29:55, then `6de7db6 wal: flush 20260714T1631` fired ~1 min later — exactly the §6.4 note at `session-end.md:145`.
- **FIX:** §5.2's block must be **squash → count → push, all three chained in ONE turn**, i.e. re-run the §5.0 computation inside the §5.2 block rather than trusting the earlier §5.0 result. State it as: "§5.0 is a *pre-pass*; §5.2 re-executes it. If §5.2's recount is >0 → re-squash once, recount, then push; if still >0 → STOP + report." Add the acceptance "no turn boundary between the final squash and `git push`".
- **VERIFY:** dry-run a closeout that deliberately dirties WAL between §5.0 and §5.2 → the block must still reach `git push` without manual intervention; `git log --format='%s' origin/main -20 | grep -c '^wal:'` unchanged (57).

**C4. W0.2's "worktree đang sạch / dirty = 0" is FALSE right now, and `git add --renormalize .` silently stages dirty content while the acceptance is structurally blind to it.**
- **what · where:** spec §2.0 header "(a) worktree **đang sạch** = cửa-sổ an-toàn DUY-NHẤT"; W0.2 "Hiện dirty = 0 (chỉ 7 broadcast untracked) ⇒ **cửa-sổ sạch**"; acceptance "`git diff --stat` sau renormalize = rỗng".
- **WHY sai:** `git status --short` **now** → `M .claude/WAL.md` (plus 7 untracked broadcasts + 6 untracked run-folder files). The premise is stale, and the spec is bơm qua args — the worker will not re-measure unless told to. Fault-injected in a temp tree (`core.autocrlf=true`, one `i/lf w/crlf` file, one dirty file with uncommitted content):
  ```
  git add --renormalize .
  git diff --stat            → (empty)     ← spec acceptance PASSES
  git diff --cached --stat   → .gitattributes | 1 +
                               WAL.md        | 1 +   ← dirty file SILENTLY STAGED
  ```
  `--renormalize` is `git add`: unstaged diff is empty **because** everything moved to the index. The acceptance cannot distinguish "no-op" from "staged a pile", so "diff RỖNG ⇒ 0 commit ⇒ 0 CI" is unsound whenever the tree is dirty — which it is.
- **FIX:** (i) W0.2 step 0 = **re-measure**, not trust the spec: `git status --porcelain` must be captured and every entry classified before any git-index or worktree op; abort if unclassified. (ii) Replace the acceptance with `git diff --cached --stat` = empty (the staged view), and pre-check with `git add --renormalize --dry-run .` (verified working: exit 0, lists files) instead of the live `git add`. (iii) Scope the renormalize to the gate paths rather than `.`.
- **VERIFY:** `git status --porcelain` → non-empty today (proves the premise stale); `git add --renormalize --dry-run .` → lists exactly the intended set and nothing dirty.

**C5. §2.0 W0.2's acceptance (toàn-repo) contradicts §2.2 SCOPE + §3.9 (scoped) — and the scoped gate is ALREADY GREEN, so the toàn-repo variant is 188 files of pure risk for zero gate benefit.**
- **what · where:** §2.0 W0.2 acceptance: `git ls-files --eol | grep -c 'i/lf.*w/crlf'` = **0** (unscoped) **vs** §2.2 🔴 SCOPE + §3.9:262: `git ls-files --eol .claude/agents .claude/commands .claude/skills | grep -c …` = **0** (scoped).
- **WHY sai:** measured via `git ls-files --eol` (the authority the trục mandates):
  | metric | measured | spec |
  |---|---|---|
  | tracked total | 1312 | — |
  | `i/lf w/crlf` | **188** | 188 ✓ |
  | …of which `Migrations/` | **131** | 131 ✓ |
  | **gate scope** (`agents`+`commands`+`skills`) | **0** | 0 |
  The scoped gate **passes today with zero work**. The unscoped acceptance demands touching all 188 — and since `git add --renormalize` does **not** rewrite the worktree (proven: file stays `i/lf w/crlf` after renormalize; only `rm` + `git checkout --` yields `w/lf`), satisfying it requires `rm`+`checkout` on **188 files incl. 131 EF migrations**, plus the 6 files the spec itself names as live: I confirmed all 6 are in the 188 (`docs/STATUS.md`, `docs/HANDOFF.md`, `docs/CLAUDE.md`, `.claude/agent-memory/{implementer-backend,reviewer}/MEMORY.md`, `.claude/agent-memory/.mfe-state.json`). Worse, it is **un-holdable**: W1/W2 spawn agents that rewrite `agent-memory/*/MEMORY.md` → back to `w/crlf` → the gate re-breaks after it was "passed".
- **FIX:** delete the unscoped acceptance from §2.0 W0.2. Keep only §2.2/§3.9's scoped gate; declare the remaining 188 **INFORM-only** (as §2.2 already argues) and explicitly forbid the mass `rm`+`checkout`. If W0.1 is landed, new/rewritten files normalize on `git add` anyway — no sweep needed.
- **VERIFY:** `git ls-files --eol .claude/agents .claude/commands .claude/skills | grep -c 'i/lf.*w/crlf'` → **0 today**, and still 0 after W2 writes the 2 new agents.

---

## M — MAJOR

**M1. `ls .claude/agents/*.md = 14` fails by construction — the real number after +2 is 15.**
`ls -1 .claude/agents/*.md | wc -l` = **13** = 12 roster + **`README.md`** (which is also a `.md` in that dir). +2 new agents = **15**, not 14. Spec §2.4(1)(a) and §3.9:271 both assert 14. The gate can only be made green by *not* creating one agent, or by deleting `agents/README.md` — both catastrophic-by-Goodhart.
**FIX:** state the invariant on the roster, not the glob: `VALID_ROLES.length == 14` **AND** `(ls .claude/agents/*.md | grep -v README.md | wc -l) == 14`. **VERIFY:** both counts equal 14 post-W2.

**M2. W1① `governance-detectors.ps1` is NOT "🟢 1 agent owner, disjoint" — it is coupled to `docs/STATUS.md`, which §3.2's conflict table omits entirely.**
`Get-GovernanceMd:118-127` and `Write-Flag:43-53` / `Write-Section:55` anchors are exact (verified). But C2/B3 resolves canonicals from STATUS.md's CURRENT-STATE table via `Get-StatusValue` (`:154-157`, pattern `^\|\s*<label>\s*\|\s*\*\*(\d+)`). Measured rows: `Migrations|SQL tables|Master data|API endpoints|FE pages|Menu keys|Tests|Gotchas|User memory|Skills|Sub-agents`.
- `| Menu keys | **54** |` **exists** → the `menu` token works (and disk truth `grep -c "public const string" MenuKeys.cs` = **54**, matching §1.5 exactly).
- **There is NO `Policies` row** → `Get-StatusValue 'Policies'` = `$null` → the `policy` half of GAP-3 cannot fire without first adding `| Policies | **216** |` to STATUS.md. Not mentioned anywhere in the spec.
- `| Sub-agents | **12** |` is a **canonical row**. W2 takes the roster to 14. If STATUS.md is not updated in the same landing, C2/B3 will flag every derived doc that says "14" as stale-against-canonical-12 → the detector **storms on H24's own change**, and §2.4(2)(c)'s "TOTAL ≥ 46" gets met by garbage.
**FIX:** add `docs/STATUS.md` to §3.2 as a **SERIAL em-main** file; land `Policies` row + `Sub-agents 12→14` in the same commit as W2; make W1①'s task explicitly depend on it. **VERIFY:** `sed -n '10,24p' docs/STATUS.md | cut -c1-40` shows both rows; detector output has no `Sub-agents` stale flags.

**M3. §3.9's `TOTAL FLAGS ≥ 46 VÀ có dòng permission-matrix/SKILL.md` is unsatisfiable as a CLOSE gate — W4's entire purpose is to make that line disappear.**
Measured baseline by running the script: **`TOTAL FLAGS: 42`** (§1.5's "42 flag baseline" ✓ confirmed; `grep -c memory-budget` = 0 and `grep -c ConvertFrom-Json` = 0 ✓ confirmed). §2.4(4) requires "(4) chạy lại ⇒ dòng **BIẾN MẤT**, TOTAL giảm đúng số đã vá". So at close time the §3.9 line is FALSE by design. A worker running the final checklist sees FAIL and is incentivised to revert the patch to go green.
**FIX:** split into two time-anchored gates — pre-W4: "TOTAL ≥ 46 **AND** line present, evidence = detector stdout captured to `runs/<id>/w4-precontrol.txt`, reviewer-witnessed"; post-W4: "line **absent** AND `TOTAL == 46−k` where k = flags patched". The pre-gate must reference the saved artifact, never a re-run.

**M4. Sàn-3 ① drops the dual-accept clause → 5 guaranteed false positives; and the spec cites a blank line as its authority.**
`README.md:20` = **a blank line** (the code fence closes at :19). The real orphan definition is **`README.md:83` (L2)**, which reads: "orphan = folder có `run.md` mà KHÔNG có `<stage>-synthesis.md` … **run cũ S71 dual-accept `harvest/<stage>-synthesis.md`**" — mirrored at `session-start.md:97` and `README.md:123` (C8 "close-gate ACCEPT BOTH"). Spec §2.1 Sàn-3 ① states only "có `run.md` mà `*-synthesis.md` vắng/0-byte" — **no dual-accept**. Measured: a naive flat scan flags **13** folders; with dual-accept it flags **8** (the 5 `h10`/`h910` folders each have `harvest/<stage>-synthesis.md`, verified by `ls`). So the spec's headline "8/27 orphan, 7 tồn-đọng" is **correct** — but only under a rule the spec omits. Since Sàn-3's canonical text gets **re-authored** into `tiep.md` §0 (W3) and the worker does not Read `README.md`, the clause is lost → h10 ×3 + h910 ×2 storm — which W0.5's "close 7 folders" does **not** cover, because those 5 were never in the 7.
**FIX:** carry the dual-accept clause verbatim into Sàn-3 ①'s canonical text; fix the citation to `README.md:83`. **VERIFY:** run the new scan → exactly `{2026-07-15-S119-adap-6-broadcast}` (+ nothing else) after W0.5.
*(Side note, not spec error: `2026-07-14-S117-pe-approve-minmax-range` and `-pe-luyke-finalize` have **no `run.md` at all** → invisible to a `run.md`-anchored scan. C2 fragile-point, worth an INFORM bậc.)*

**M5. Sàn-1's tripwire breaks documented live paths and its wrapper list is wrong.**
`hmw.js` anchors verified exact: `checkpointApproved` :83-85 ✓, `VALID_ROLES` :22 (12 entries) ✓, STOP-HARD :104 ✓, `resolveModel` :43-51 ✓, H8 comment :32-42 ✓. But the proposed `if (A.taskList.length >= 3 && !(A.run?.dir && A.run?.name)) throw`:
- (a) **kills the documented legacy alias.** `hmw.js:116`: `const wave = (A.run && A.run.dir) ? A.run : ((A.wave && A.wave.dir) ? A.wave : null)` — `args.wave` is a supported entry point (`:19`, `:115`). A ≥3-task call using `wave:` throws even though RUN-TRACE is fully satisfied.
- (b) **contract mismatch:** the tripwire demands `.dir` **and** `.name`; hmw.js's own gate requires only `.dir`. A call with `run:{dir}` works today, throws after.
- (c) **wrapper list wrong.** Measured: `git grep -c "args\.run" -- .claude/commands/` → **exit 1, 0 hits**. `.claude/commands/` contains **no `hmw.md`** — the spec's third wrapper "`/hmw`" does not exist as a file. And **`fable-real.md` is absent from the list** although it launches ensembles ≥3 lanes (`fable-real.md:19` "Vai BẮT BUỘC ∈ roster 12"). First `/fable-real` ≥3-lane run after this lands → **throw** = "vỡ đường đang chạy", precisely what the spec's own §2.1 warns about.
**FIX:** tripwire condition → `const w = (A.run&&A.run.dir)?A.run:((A.wave&&A.wave.dir)?A.wave:null); if (A.taskList.length >= 3 && !w) throw` (mirror :116 exactly, drop the `.name` requirement or add `.name` to :116's contract in the same edit). Enumerate wrappers by measurement, not memory: `fable-clone.md`, `fable-real.md`, `ultra-on.md` (+ wherever `/hmw` is actually defined — locate it first). **VERIFY:** `git grep -c "args\.run" -- .claude/commands/` ≥ 3 **and** a ≥3-task `wave:`-style call still succeeds.

**M6. §2.4(5)(c)'s sweep grep is the wrong pattern and "5 chỗ" undercounts by ~3×.**
Grep tool (not Bash — per the trục's false-negative warning) for `12-agent|12 sub` → **~14 hits in 6 active files**: `session-start.md` :2/:66/:73/:74/:202 · `session-end.md` :2/:83 · `agents/README.md` :1/:3/:31/:35 · `workflows/README.md:36` · `ultra-on.md:24` · `fable-real.md:36`. The pattern **misses** the other live phrasing `roster 12`/`12 vai`/`10→12`: `fable-clone.md` :3/:18/:21/:34 · `fable-real.md` :3/:19 · `fable-real-runbook.md` :74/:94/:148/:191/:314/:444/:450/:501/:503/:509/:520 · `harness-11-engine.md` :332/:345 · `ACTIVE-MARKS.md:19`. It also misses an **already-stale** one: `hmw.js:9` meta.description says "fan-out **9-agent** roster". So the acceptance can return 0-hit while the drift is live = false-PASS. Note `ultra-on.md:24` literally heads a VALID_ROLES list — leave it at 12 and a lead reading it concludes `lead-view-auditor` is not a valid role → never spawns it → **H24 dead on arrival**.
**FIX:** sweep pattern `(12|9)[- ](agent|sub)|roster 12|12 vai|VALID_ROLES[^0-9]*12`; replace "5 chỗ" with the measured list; fix `hmw.js:9` in the same edit.

---

## m — minor

- **m1.** Spec cites "`.claude/WAL.md:15` `verify:`" twice (§2.1 push-guard (a), §3.8). The `grep -c '^wal:'` idiom is at **`:16`**; `:15` is `next:`. The idiom claim itself is **correct** (`grep -c` exits 1 on 0 matches). WAL is auto-rewritten, so pin by content, not line.
- **m2.** Spec's `1267 i/lf` → measured **1268**. `110ade2` added one file after the spec was written. Harmless in itself; it is the same staleness class as C2/C4 and a cheap tell.
- **m3.** Spec asserts `git rebase -i` "**BỊ CHẶN** trong harness". Empirically it **ran** in my Bash tool with `GIT_SEQUENCE_EDITOR` set (see C1 transcript). The premise is wrong; the chosen workaround is still fine, so this is cosmetic — but "blocked" shouldn't be sold as measured.
- **m4.** §2.4(1)(b) mandates FLAG format `^[A-Z0-9-]+ \| <class> \|`. The existing convention (`governance-detectors.ps1:24` doc + `:51` `Write-Flag`) is `[DETECTOR] {SEV,-4} | {file:line} | {desc} | resolve: {…}` — a `[DETECTOR] ` literal prefix. Two flag grammars in one governance surface = the vocab-fork class C3 already flags. Reuse the existing shape.
- **m5.** W0.5 alternative B ("scope scan `mtime > closeout cuối`") is **not durable**: the repo is `D:\Dropbox\…`; a Dropbox re-sync rewrites mtime → all 27 folders look new → total storm. Also "closeout cuối" is undefined as a measurable anchor. Use `git log -1 --format=%cI -- <folder>` (git-native, sync-proof); the reference point would be `3fde8af` @ `2026-07-14T16:29:55+07:00`.
- **m6.** W0.5 alternative A ("Đóng — thêm `*-synthesis.md`") invites fabrication: for 12-day-old runs (S112/S113/S114) the transcript is gone, so any "synthesis" written now is invented — poisoning the audit trail to green a gate. `README.md:100` already has the honest form: "**Đánh-dấu aborted** … verdict = `⚠️ ABORTED — <lý-do>`, ghi rõ phần nào hoàn-thành (recover qua git/disk/prod truth, **KHÔNG tin agent return-message**)". Say that explicitly, or the Goodhart path is the default.

---

## Trục-mandated answers

**Mọi chỗ spec có thể huỷ uncommitted work:**
1. **C1** — rebase-fixup → detached HEAD → hook commits onto it → `git rebase --abort` discards worktree + orphans the commit. **Proven**, highest severity.
2. **C2** — `reset --soft HEAD~K` with K too large → branch behind `origin/main` → non-FF push → `--force` temptation → rewrites `3fde8af` on the shared remote (Gitea runner + CI #388/#389 cite it).
3. **C5** — the unscoped `rm` + `git checkout --` sweep over **188** files, on a **stale "dirty = 0"** premise (C4). At-risk-and-live: `agent-memory/{implementer-backend,reviewer}/MEMORY.md`, `agent-memory/.mfe-state.json`, `docs/{STATUS,HANDOFF,CLAUDE}.md` — all 6 confirmed in the 188. *(Correction to a plausible worry: `.claude/WAL.md` is `i/lf w/lf` — **not** in the 188, so the checkout sweep does not reach it. The `renormalize .` staging in C4 does.)*
4. **Hook × index.lock** — `git rebase` or a mass `checkout` while `wal-flush.ps1` runs `git add`/`git commit` (fail-open, `2>$null`) on a **Dropbox-synced** `.git` → hook silently no-ops → the H22 durability guarantee is off exactly during the riskiest step. §3.8/N.7(e) name `index.lock` but only for the "dọn 57" scenario, not for W0.

**Rollback:** the spec has **no rollback/abort path for W0** — §3.8 lists risks and preventions only. **FIX:** W0 step 0 = `git tag w0-safepoint HEAD` + record `git rev-parse HEAD` + `git status --porcelain > runs/<id>/w0-pre.txt`; documented undo = `git rebase --abort` (if rebase state) → `git reset --mixed w0-safepoint` (unstage renormalize) → `git checkout w0-safepoint -- .gitattributes`; and a stated reflog escape (`git reflog | head`). Without this, C1's failure mode has no written exit.

**W1 disjointness:** file-level yes (`governance-detectors.ps1` / `scripts/agent-frontmatter-eol-check.ps1` / `scripts/wal-recovery-test.ps1` — all exist-or-new, no overlap). Semantically **no** — see M2 (STATUS.md coupling). Plus: the 3 lanes run as agents → each writes `agent-memory/*/MEMORY.md` → the Stop hook auto-commits that path each turn → wal: accumulation during W1 feeds C2/C3. And W1② ("PASS trên **12** agent thật") vs W2 (creates agents 13–14): the acceptance's hardcoded 12 **excludes from validation the two files most likely to carry the defect** — brand-new agent `.md` written on Windows. Use a glob, not a count.

**W2 vs W1 on `hmw.js`:** no collision — W1 does not touch `hmw.js`; W2 owns it in "1 EDIT" per §3.2. That part holds.

**Push "cùng lượt":** your reading is right — guard+push in one tool-call means the hook has not fired between them. The gap is that the **squash** is not in that lượt (C3). §3.3 W6 says "push-guard **CÙNG LƯỢT** với push" and §2.1 acceptance (ii) says count+push same transcript-turn; **neither binds §5.0**. That is the deadlock.

---

## REFUTATION LOG — claims I attacked and could NOT break

1. **"188 file `i/lf w/crlf`, 131 = `Migrations/`"** — attacked with `git ls-files --eol` (authority, per your CR-count warning): **188 ✓, 131 ✓**. Also `i/none` **25** ✓, `i/-text` **19** ✓, `i/crlf` **0** ✓. Only `i/lf` differs (1268 vs 1267) — one commit of drift, not an error.
2. **"`git add --renormalize .` = diff RỖNG cho 188"** — I expected index churn. Temp-tree test: the `i/lf w/crlf` file produced **zero** cached diff (clean filter → identical blob). **The core claim holds.** Only the *dirty-file side effect* and the *acceptance metric* are broken (C4).
3. **W0.1 premise "`* text=auto eol=lf` beats `core.autocrlf=true`@system"** — `core.autocrlf` confirmed `true` at `C:/Program Files/Git/etc/gitconfig`; `check-attr` on `reviewer.md` today = `text: unspecified, eol: unspecified` (matches spec). Temp-tree: after `* text=auto eol=lf` + `rm` + `checkout` → `attr/text=auto eol=lf`, `w/lf`, `od -c` shows `\n`. **Holds.**
4. **W0.1's binary-class safety** — tried to find a file the `* text=auto` rule would corrupt. The 19 `i/-text` = 8+5 `.docx` / 6 `.png` / 1 `.xlsx` — all already auto-detected binary and all covered by the proposed list. The 25 `i/none` = 23 `.gitkeep` (empty) + 2 `.svg` (no EOL) — unaffected. `.bat`/`.cmd`: **0 tracked**. All 20+ `.ps1`: already `w/lf` → rule is a no-op. **W0.1 is safe; the ⚠️ it raises is genuinely N/A.**
5. **"8/27 orphan, 7 tồn-đọng (S112 ×4, S113, S114, pe-negative-quote) + 1 bay"** — my naive scan said 13, so I went hunting for an error. `ls */harvest/` proved all 5 `h10`/`h910` carry `harvest/<stage>-synthesis.md`, and `README.md:83` grants dual-accept. **The spec's number is right.** The defect is the *omitted clause + wrong citation* (M4), not the count.
6. **§1.5 positive-control numbers** — `grep -c "public const string" MenuKeys.cs` = **54** ✓ (skill says "~60"). Baseline `TOTAL FLAGS: 42` ✓ by running the script. `grep memory-budget|ConvertFrom-Json` in the .ps1 = **0/0** ✓. **All verified.**
7. **"Detector can't see `.claude/skills/`" — my hypothesis, REFUTED.** `Get-GovernanceMd:119` scans `docs` + `.claude` recursively with `-Filter *.md`; `SKILL.md` matches; the 6-path exclusion list (`governance-detectors.ps1`, `harness-11-engine.md`, `vocab-alias-map.md`, `broadcasts/inbox`, `broadcasts/outbox`, `.claude/workflows/runs`) does **not** include skills. The positive control **can** fire. Spec correct.
8. **"`Menu keys` has no canonical row" — my hypothesis, REFUTED.** `| Menu keys | **54** |` exists in STATUS.md's CURRENT-STATE table. Only `Policies` is missing (M2).
9. **AS-6 CI claims** — `.gitea/workflows/*.yml:20-27` `paths-ignore` = `docs/**`, `**/*.md`, `.claude/skills/**`, `.claude/agent-memory/**`, `.claude/workflows/runs/**`, `.gitignore`, `scripts/**.md`. So `.gitattributes` **triggers** ✓, `scripts/*.ps1` **triggers** ✓, `.json` **triggers** ✓, `agent-memory/**` + `runs/**` **do not** ✓. **Spec §3.8 correct** (it merely omits that `hmw.js` also triggers, which is harmless).
10. **All `hmw.js` line anchors** (:22 / :83-85 / :104 / :43-51 / :32-42) and **all `governance-detectors.ps1` anchors** (:43-53 / :55 / :118-127 / :154-157) — **exact**. `session-start.md:51` ("chain rỗng" → "WAL sạch") ✓, `:54` ("CHỈ LÀ CON-TRỎ") ✓, `:97` (orphan scan) ✓, `:98` (skippable) ✓, `session-end.md:112-113` (§5.0 counts LIÊN-TỤC from HEAD, ":113 GIỮ NGUYÊN, chấp-nhận noise") ✓, `:145` (§6.4 note) ✓, `fable-clone.md:15` ("ENSEMBLE tier-2") ✓ with **0** `tier:'opus'` (GAP #6 real) ✓, `fable-clone.md:19` roster-12 = VALID_ROLES-12 ✓. **§1.2 L1's leak-channel diagnosis is independently confirmed** by the S118 shape: `e88e75d → b56659b(wal) → c2135ba → b7f7b56(wal) → 1491d29(wal) → c808b9a → 3fde8af` — sandwiched wal: rode to origin because §5.0 only counts trailing. The spec's headline analysis is sound; it is the **execution mechanics** that fail.
11. **`WAL.md:16` `grep -c` idiom "dính exit-1-khi-0"** — confirmed real (only the line number is off by one, m1).