# RLS tooling boundary — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Document the boundary between the `rls-check` skill and the `rls-reviewer` agent so a reader knows which tool to use, and recolor the tracked conflict on the automation graph from RED (unresolved) to GREEN (resolved by rule).

**Architecture:** This is a documentation / data-file change — two Markdown tool definitions (`.claude/skills/rls-check/SKILL.md`, `.claude/agents/rls-reviewer.md`) plus one HTML data graph (`docs/automation-graph.html`). There is **no application code** and **no automated test target**. Verification is done by the project lint suite (cspell, markdownlint, stylelint) plus a Playwright visual smoke of the graph — these play the role unit tests would for code. The boundary rule is added symmetrically to both tool files (each gets a mirrored "Граница…" section), and the graph's `sk_rls↔ag_rls` conflict is recolored in all four places it is encoded. All three files form **one logical change** and ship in **one commit** (splitting them would leave an inconsistent intermediate state — e.g. the skill referencing an agent section that does not yet exist).

**Tech Stack:** Markdown (Claude Code skill/agent definitions), HTML + vanilla JS + vis.js (automation graph), lefthook pre-commit hooks (gitleaks / markdownlint / cspell / stylelint), cspell + `cspell-words.txt` glossary, Playwright MCP (visual smoke), `python -m http.server` (quirk 90 — Playwright rejects `file://`).

**Spec:** `docs/superpowers/specs/2026-05-16-rls-tooling-boundary-design.md` (committed `21debac`).

**Branch:** `feat/rls-tooling-boundary` (already created off `feat/regression-skill` HEAD; spec already committed there).

---

## File Structure

| File | Responsibility | Change |
|---|---|---|
| `.claude/skills/rls-check/SKILL.md` | Manual single-table RLS check skill | +section «Граница с агентом rls-reviewer», +1 bullet, +1 clause in `description` |
| `.claude/agents/rls-reviewer.md` | Diff/PR RLS review subagent | +section «Граница со скилом /rls-check», +1 bullet, +1 clause in `description` |
| `docs/automation-graph.html` | vis.js automation map (83 nodes / 90 edges / 11 conflicts) | conflict `sk_rls↔ag_rls` RED→GREEN in 4 spots (edge 384, node 685, node 766, EDGE_META 1303) |
| `cspell-words.txt` | Project spelling glossary | conditionally — only if cspell flags valid new Russian inflections (Task 4) |
| `…/memory/project_automation_map.md` | Memory: automation-map description (**outside git repo**) | counter `🔴3/⚫4/🟢4` → `🔴2/⚫4/🟢5` |
| `…/memory/MEMORY.md` | Memory index (**outside git repo**) | same counter in the automation-map index line |

**Commit boundary:** the 3 repo files (skill + agent + graph) + `cspell-words.txt` (if changed) = **one commit**. The 2 memory files are outside the git repo — they are plain `Write`/`Edit` actions, **not committed**.

---

## Task 1: `rls-check` skill — boundary section

**Files:**

- Modify: `.claude/skills/rls-check/SKILL.md` (frontmatter `description` ~line 7; insert section between `## Когда использовать` and `## Checklist`; +bullet in `## Не использовать когда` ~line 96)

- [ ] **Step 1.1: Add boundary clause to frontmatter `description`**

Edit — `old_string` (the last `description` line + the next frontmatter key, so the anchor is unique):

```text
  5-role GRANTs, db/CHANGELOG_schema.md entry, squawk, smoke test).
disable-model-invocation: true
```

`new_string` (the new clause is inserted as a third `description` line; `disable-model-invocation: true` stays unchanged):

```text
  5-role GRANTs, db/CHANGELOG_schema.md entry, squawk, smoke test).
  For reviewing a diff, branch, or PR with DB changes - use the rls-reviewer agent.
disable-model-invocation: true
```

Rationale: `description` is plain ASCII English today — keep the new sentence ASCII (plain hyphen, not em dash) for style consistency.

- [ ] **Step 1.2: Insert the `## Граница с агентом rls-reviewer` section**

Edit — `old_string`:

````text
Invoke via `/rls-check <table_name>`.

## Checklist
````

`new_string`:

````text
Invoke via `/rls-check <table_name>`.

## Граница с агентом rls-reviewer

`rls-check` (этот скил) и `rls-reviewer` (агент, `.claude/agents/rls-reviewer.md`)
оба проверяют RLS, но в разных ситуациях. Правило выбора:

- Знаешь имя одной конкретной таблицы, проверка вручную перед коммитом →
  **`/rls-check <table>`** (этот скил).
- Есть diff / ветка / PR с изменениями БД, набор таблиц заранее не известен →
  **агент `rls-reviewer`**.

Скил работает в основном контексте по одной названной таблице и прогоняет
**8 строк вывода** — 7 статических пунктов + живой дымовой тест
(`pest --filter RlsSmokeTest`, шаг 7). Агент работает в отдельном контексте
субагента, разбирает diff/миграцию/PR и прогоняет только **7 статических**
строк — дымовой тест намеренно не запускает.

Первые 7 строк вывода у обоих — общее статическое ядро (tenant_id, ENABLE RLS,
SELECT/ALL политики, GRANT'ы 5 ролей, CHANGELOG, squawk). Это не дублирование:
ядро проверок одно, сценарии вызова разные. Дымовой тест — только в скиле:
запуск Pest в ревью-субагенте медленный и задевает гонки `--parallel`
(квирки 72/77, см. `.claude/agents/pest-parallel-debugger.md`).

## Checklist
````

Wording note: this is the spec §3.1(a) text with two minor wording polishes for spelling-dictionary cleanliness — meaning is identical; spec §3 explicitly allows final wording to be settled at this stage.

- [ ] **Step 1.3: Add the third bullet to `## Не использовать когда`**

Edit — `old_string`:

````text
- Tables explicitly outside RLS (e.g., Laravel `migrations`, `cache` — internal).
````

`new_string`:

````text
- Tables explicitly outside RLS (e.g., Laravel `migrations`, `cache` — internal).
- Проверяешь не одну названную таблицу, а diff/ветку/PR с изменениями БД —
  это сценарий агента `rls-reviewer`, не скила.
````

- [ ] **Step 1.4: Verify the edits applied**

Run: `Read` `.claude/skills/rls-check/SKILL.md`
Expected: frontmatter has the new clause; a `## Граница с агентом rls-reviewer` section sits between `## Когда использовать` and `## Checklist`; `## Не использовать когда` has 3 bullets.

---

## Task 2: `rls-reviewer` agent — mirrored boundary section

**Files:**

- Modify: `.claude/agents/rls-reviewer.md` (frontmatter `description` ~line 9; insert section between `## Контекст проекта` and `## Workflow`; +bullet in `## Out of scope` ~line 79)

- [ ] **Step 2.1: Add boundary clause to frontmatter `description`**

Edit — `old_string`:

```text
  columns, inconsistent GRANTs, missing CHANGELOG entries.
tools: Read, Grep, Glob, Bash
```

`new_string`:

```text
  columns, inconsistent GRANTs, missing CHANGELOG entries.
  For manually checking a single named table before commit - use the /rls-check skill.
tools: Read, Grep, Glob, Bash
```

- [ ] **Step 2.2: Insert the `## Граница со скилом /rls-check` section**

Edit — `old_string`:

````text
Каждое schema change требует записи в `db/CHANGELOG_schema.md` (CLAUDE.md §5 п.8).

## Workflow
````

`new_string`:

````text
Каждое schema change требует записи в `db/CHANGELOG_schema.md` (CLAUDE.md §5 п.8).

## Граница со скилом /rls-check

`rls-reviewer` (этот агент) и скил `/rls-check`
(`.claude/skills/rls-check/SKILL.md`) оба проверяют RLS. Правило выбора:

- Есть diff / ветка / PR с изменениями БД, набор таблиц заранее не известен →
  **этот агент**.
- Знаешь имя одной конкретной таблицы, проверка вручную перед коммитом →
  **скил `/rls-check <table>`**.

Этот агент прогоняет **7 статических пунктов** чеклиста. Живой дымовой тест
(`pest --filter RlsSmokeTest`) намеренно **не входит** в агентский чеклист:
запуск Pest в ревью-субагенте медленный и задевает гонки `--parallel`
(квирки 72/77, см. `.claude/agents/pest-parallel-debugger.md`). Живой дымовой
тест — 8-я строка скила `/rls-check`. 7 пунктов агента === первые 7 строк
вывода скила (общее статическое ядро).

## Workflow
````

- [ ] **Step 2.3: Add the fourth bullet to `## Out of scope`**

Edit — `old_string`:

````text
- Performance review (separate concern).
````

`new_string`:

````text
- Performance review (separate concern).
- Проверка одной названной таблицы вручную перед коммитом + живой дымовой
  тест — сценарий скила `/rls-check`, не агента.
````

- [ ] **Step 2.4: Verify the edits applied**

Run: `Read` `.claude/agents/rls-reviewer.md`
Expected: frontmatter has the new clause; a `## Граница со скилом /rls-check` section sits between `## Контекст проекта` and `## Workflow`; `## Out of scope` has 4 bullets.

---

## Task 3: `automation-graph.html` — recolor the conflict RED → GREEN

**Files:**

- Modify: `docs/automation-graph.html` (4 spots: line ~384 `CONFLICT(...)`, line ~685 `sk_rls` `nd()`, line ~766 `ag_rls` `nd()`, line ~1303 `EDGE_META`)

All 4 edits are in-place single-line replacements — line numbers do **not** shift. The conflict edge is **not deleted**: the graph models resolved conflicts as GREEN entries in the `conflicts` array (see existing GREEN conflicts at lines 388–391); deleting the edge would lose the historical record.

- [ ] **Step 3.1: Re-read the 4 anchor regions**

Run: `Read` `docs/automation-graph.html` at offsets ~380, ~678, ~758, ~1296 (≈12 lines each)
Expected: confirms the four `old_string` anchors below still match verbatim before editing.

- [ ] **Step 3.2: Edit 1 — `CONFLICT(...)` edge (line ~384)**

Edit — `old_string`:

```text
  CONFLICT('sk_rls',         'ag_rls',          'RLS compliance: оба покрывают, нет регламента', 'RED'),
```

`new_string`:

```text
  CONFLICT('sk_rls',         'ag_rls',          'RLS: граница задана — скил по таблице, агент по diff/PR (spec 2026-05-16)', 'GREEN'),
```

(Keep the arg-1/arg-2 padding identical; arg-3/arg-4 are not column-aligned across `CONFLICT` lines, so no extra padding needed.)

- [ ] **Step 3.3: Edit 2 — `sk_rls` node conflict entry (line ~685)**

Edit — `old_string`:

```text
    [{ name: 'агент rls-reviewer', desc: 'оба проверяют соответствие политик RLS (защиты строк по тенанту) — скил для ручной проверки таблицы, агент для разбора PR (запроса на слияние кода) и diff; нет чёткой границы когда какой', type: 'RED' }]
```

`new_string`:

```text
    [{ name: 'агент rls-reviewer', desc: 'граница задана регламентом (spec 2026-05-16): скил — ручная проверка одной названной таблицы + живой дымовой тест; агент — разбор diff/ветки/PR. См. секцию «Граница с агентом rls-reviewer» в SKILL.md.', type: 'GREEN' }]
```

(The `desc` is a single-quoted JS string — verified it contains no `'` apostrophe; «» guillemets and — em dash are safe.)

- [ ] **Step 3.4: Edit 3 — `ag_rls` node conflict entry (line ~766)**

Edit — `old_string`:

```text
    [{ name: 'скил rls-check', desc: 'оба покрывают соответствие RLS (защиты строк по тенанту), чёткой границы нет: агент — для PR (запроса на слияние кода) и diff, скил — для ручной проверки таблицы', type: 'RED' }]
```

`new_string`:

```text
    [{ name: 'скил rls-check', desc: 'граница задана регламентом (spec 2026-05-16): агент — разбор diff/ветки/PR со статическим 7-пунктовым чеклистом; скил — ручная проверка одной названной таблицы + живой дымовой тест. См. секцию «Граница со скилом /rls-check» в rls-reviewer.md.', type: 'GREEN' }]
```

- [ ] **Step 3.5: Edit 4 — `EDGE_META` entry (line ~1303)**

Edit — `old_string`:

```text
  'sk_rls->ag_rls':                { type: 'конфликт',      when: 'skill и agent оба претендуют на RLS-аудит',                                transfers: 'coverage',     mandatory: 'опционально',   rule: 'нет регламента (двойное покрытие)' },
```

`new_string` (semantic content — finalise spacing against neighbours):

```text
  'sk_rls->ag_rls':                { type: 'конфликт',      when: 'граница задана: скил — по одной названной таблице, агент — по diff/ветке/PR', transfers: 'coverage',     mandatory: 'опционально',   rule: 'секции «Граница…» в SKILL.md + rls-reviewer.md (spec 2026-05-16)' },
```

`type: 'конфликт'` is **kept** — `EDGE_META.type` is the edge category, not the RED/GREEN resolution colour (which lives in the `CONFLICT()` call). A resolved (GREEN) conflict is still a `'конфликт'`-category edge. Only `when` and `rule` change. EDGE_META is a column-aligned JS object literal: after editing, eyeball that `transfers:` / `mandatory:` / `rule:` still line up with neighbouring line 1304 — if the new `when` value length makes exact alignment awkward, single-space separators are acceptable (alignment is cosmetic, not functional).

- [ ] **Step 3.6: Verify the 4 edits applied**

Run: `Grep` pattern `sk_rls.*ag_rls` in `docs/automation-graph.html`, output_mode `content`
Expected: the `CONFLICT(...)` line shows `'GREEN'`; no remaining `'RED'` on any `sk_rls`/`ag_rls` line. (Node `nd()` entries are matched by their `name:` strings — confirm both show `type: 'GREEN'`.)

**Not edited — `ag_rls` node `limits` line (~762).** It already reads "Не замена скилу rls-check — у них разные сценарии." Spec §3.2 marks updating it **optional** ("править не обязательно") because after this change the statement is simply factually accurate. Leaving it as-is is intentional and spec-compliant — do **not** edit it.

---

## Task 4: Lint sweep on the changed repo files

**Files:**

- Verify: `.claude/skills/rls-check/SKILL.md`, `.claude/agents/rls-reviewer.md`, `docs/automation-graph.html`
- Conditionally modify: `cspell-words.txt`

- [ ] **Step 4.1: Run cspell on the two `.md` files**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; npx cspell --no-progress --no-summary ".claude/skills/rls-check/SKILL.md" ".claude/agents/rls-reviewer.md"`
Expected: `0` issues, exit 0.

This is the exact tool/flags the lefthook `cspell` pre-commit job uses (`glob: "*.md"` matches `.claude/**/*.md`). Russian prose **outside backticks** is checked; code-spans `` `...` `` and fenced blocks are ignored by `cspell.json` `ignoreRegExpList`.

- [ ] **Step 4.2: If cspell flags words — add valid project terms to `cspell-words.txt`**

Only if Step 4.1 reports `Unknown word`: for each flagged word that is a **valid Russian inflection of an established project term** (e.g. another case form of «скил», «субагент», «чеклист», «квирк»), append it to `cspell-words.txt`. Append a new dated section at the **end** of the file (the file is organised in dated comment-header sections):

```text
# RLS tooling boundary — skill/agent body sections (2026-05-16)
<word>
<word>
```

If a flagged word is a genuine typo — fix the typo in the source file instead. Then re-run Step 4.1 — expected `0` issues. (This is the same procedure already used for the spec commit `21debac`, which added «скиле»/«скилом».)

- [ ] **Step 4.3: Run markdownlint on the two `.md` files**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; npx markdownlint-cli2 --fix ".claude/skills/rls-check/SKILL.md" ".claude/agents/rls-reviewer.md"`
Expected: exit 0, no unfixable problems. `--fix` mirrors the lefthook job (`stage_fixed: true`); if it auto-fixes anything, that is fine — re-Read the files to confirm the boundary sections are intact.

- [ ] **Step 4.4: Run stylelint on the graph HTML**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; npx stylelint "docs/automation-graph.html"`
Expected: exit 0. Our edits touch only `<script>` JS, not `<style>` CSS, so stylelint should pass exactly as it did before (the file is already committed through this hook). If it fails on pre-existing CSS unrelated to our change — stop and report; do **not** fix unrelated CSS (out of scope per spec §5).

---

## Task 5: Visual smoke of the automation graph

**Files:**

- Verify: `docs/automation-graph.html` (rendered)

Spec §6 п.3. Playwright `browser_navigate` rejects `file://` (quirk 90) → serve over HTTP.

- [ ] **Step 5.1: Start a local HTTP server in `docs/`**

Run (background): `cd "c:\моя\проекты\портал crm\Документация\docs" ; python -m http.server 8765`
Expected: server starts, serving `docs/` on port 8765.

- [ ] **Step 5.2: Load the graph in Playwright**

Load Playwright MCP tools first: `ToolSearch` query `select:mcp__playwright__browser_navigate,mcp__playwright__browser_console_messages,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_close`.
Then `browser_navigate` to `http://localhost:8765/automation-graph.html`.
Expected: page loads.

- [ ] **Step 5.3: Check the console is clean**

Run: `browser_console_messages`
Expected: **0 error-level messages**. A JS syntax error from a botched edit would surface here — this is the primary check that the 4 in-place edits did not break the file.

- [ ] **Step 5.4: Screenshot and eyeball**

Run: `browser_take_screenshot`
Expected: the vis.js graph renders (≈83 nodes, no blank canvas); the `rls-check ↔ rls-reviewer` edge renders **green** (resolved-conflict colour), not red. Since no nodes/edges were added or removed, the totals are structurally unchanged.

- [ ] **Step 5.5: Tear down**

Run: `browser_close`, then stop the background `http.server` process.

---

## Task 6: Commit the three repo files (one atomic commit)

**Files:**

- Commit: `.claude/skills/rls-check/SKILL.md`, `.claude/agents/rls-reviewer.md`, `docs/automation-graph.html`, and `cspell-words.txt` **only if** Task 4 modified it.

- [ ] **Step 6.1: Confirm the working-tree scope**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; git status --short`
Expected: among the changes are exactly our intended files. The repo also has **pre-existing unrelated WIP** (`app/...BillingController.php`, `app/dev-indices.json`, `app/phpstan-baseline.neon`, `app/tests/.../BillingOverviewControllerTest.php`, `docs/superpowers/specs/2026-05-16-regression-skill-design.md`, untracked `.hive-mind/`, `app/C:Temppest_out.txt`) — these must **not** be staged.

- [ ] **Step 6.2: Stage exactly the intended files**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; git add ".claude/skills/rls-check/SKILL.md" ".claude/agents/rls-reviewer.md" "docs/automation-graph.html"`
If Task 4 changed `cspell-words.txt`, also: `git add cspell-words.txt`
**Do not** use `git add -A` / `git add .` — that would sweep the unrelated WIP.

- [ ] **Step 6.3: Verify the staged set**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; git diff --cached --name-only`
Expected: exactly `.claude/skills/rls-check/SKILL.md`, `.claude/agents/rls-reviewer.md`, `docs/automation-graph.html` (+ `cspell-words.txt` if applicable). **No** normative files (`docs/Pravila_*`, `CLAUDE.md`, `docs/Plugin_stack_rules_*`, `docs/Tooling_*`). If any normative file appears — stop, unstage, investigate.

- [ ] **Step 6.4: Commit (pre-commit hooks run)**

Run:

```text
git commit -m "docs(rls): document rls-check skill <-> rls-reviewer agent boundary

Both tools check RLS compliance; the boundary 'когда какой' was
undocumented (tracked as RED conflict on the automation graph).

- .claude/skills/rls-check/SKILL.md: +section 'Граница с агентом
  rls-reviewer', +bullet in 'Не использовать когда', +clause in
  frontmatter description.
- .claude/agents/rls-reviewer.md: +section 'Граница со скилом
  /rls-check', +bullet in 'Out of scope', +clause in description.
- docs/automation-graph.html: conflict sk_rls<->ag_rls recolored
  RED->GREEN (edge + both node entries + EDGE_META).

Rule: one named table -> /rls-check; diff/branch/PR -> rls-reviewer.
Smoke test stays skill-only by design (Pest --parallel quirks 72/77).

Spec: docs/superpowers/specs/2026-05-16-rls-tooling-boundary-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

Expected pre-commit (lefthook, `parallel: false`): gitleaks ✔️, markdownlint ✔️ (`*.md` → 2 files), cspell ✔️ (`*.md` → 2 files), stylelint ✔️ (`*.html` → 1 file). pint / larastan / squawk / eslint-vue skipped (no matching staged files). Show the **full** hook output. If cspell blocks here, an inflection slipped through Task 4 — return to Step 4.2.

- [ ] **Step 6.5: Verify the commit**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; git show --stat HEAD`
Expected: one commit, 3 files (+ `cspell-words.txt` if applicable), the message above.

---

## Task 7: Memory sync (outside the git repo)

**Files:**

- Modify: `C:\Users\Administrator\.claude\projects\c---------------------crm-------------\memory\project_automation_map.md`
- Modify: `C:\Users\Administrator\.claude\projects\c---------------------crm-------------\memory\MEMORY.md`

These live under the user's `.claude` home dir — **not** in the git repo, **not** committed. After Task 3 the graph shows `🔴2/⚫4/🟢5`; the memory description must follow or it goes stale (a new memory↔reality inconsistency — exactly the kind of new conflict the spec forbids).

- [ ] **Step 7.1: Find every occurrence of the stale counter**

Run: `Grep` pattern `🔴3` in the `memory/` directory, output_mode `content`
Expected: occurrences in `project_automation_map.md` and in the `MEMORY.md` index line — both read `🔴3/⚫4/🟢4`.

- [ ] **Step 7.2: Update `project_automation_map.md`**

Edit: replace `🔴3/⚫4/🟢4` → `🔴2/⚫4/🟢5` (total stays 11). If the file has a narrative line about the RLS conflict being unresolved, update it to note the boundary is now documented (spec 2026-05-16) — keep it one line.

- [ ] **Step 7.3: Update the `MEMORY.md` index line**

Edit: in the automation-map index line, replace `🔴3/⚫4/🟢4` → `🔴2/⚫4/🟢5`.

---

## Task 8: Final verification (spec §6)

- [ ] **Step 8.1: Confirm the committed file set**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; git show --name-only HEAD`
Expected: exactly the 3 repo files (+ `cspell-words.txt` if applicable). **Zero** normative files. (Spec §6 п.1.)

- [ ] **Step 8.2: lychee link check on the two `.md` files**

Run: `cd "c:\моя\проекты\портал crm\Документация" ; ./bin/lychee.exe --config .lychee.toml ".claude/skills/rls-check/SKILL.md" ".claude/agents/rls-reviewer.md"`
Expected: 0 broken links. The cross-references in the new sections are **code-spans** (`` `...` ``), not markdown links — lychee extracts no link from them, so the count is 0 links / 0 errors. (Note: the pre-push `lychee-links` job globs `docs/**/*.md db/**/*.md *.md` and does **not** cover `.claude/**` — hence this manual run. Spec §6 п.2.)

- [ ] **Step 8.3: Tick off the spec §6 checklist**

Confirm: (1) committed set = 3 files, no normative — Step 8.1 ✔️; (2) lychee 0 broken — Step 8.2 ✔️; (3) graph visual smoke — Task 5 ✔️; (4) full pre-commit ran on commit — Step 6.4 ✔️; (5) one atomic commit — Step 6.5 ✔️.

- [ ] **Step 8.4: Report**

Summarise: commit hash, files changed, lint results (cspell/markdownlint/stylelint/lychee — actual numbers), visual-smoke result, memory files updated. State explicitly anything **not** verified.

---

## Notes / constraints

- **No application code** changes → Pest / Vitest are **not applicable** (nothing in `app/` is touched). The relevant verification is the lint suite + graph visual smoke (Tasks 4–5, 8).
- **One atomic commit** for the 3 repo files (Task 6) — they are one logical change; per-file commits would create inconsistent intermediate states. This intentionally overrides the writing-plans default of commit-per-task.
- **Normative docs untouched** — both tools are project-local, outside the Tooling registry / PSR_v1 (origin spec §8). Spec §4 п.1. Step 8.1 enforces this.
- **Memory files are outside the git repo** — Task 7 is `Write`/`Edit` only, never `git add`-ed.
- **Do not** `git add -A` — the repo carries unrelated WIP (Step 6.1).
- **Spec self-review** found this plan's only deviation from the spec text: two cspell-safety wording polishes in the skill section (Task 1 Step 1.2 note) — meaning unchanged, sanctioned by spec §3. The plan also adds the `MEMORY.md` index line to Task 7 (spec §3.4 named only `project_automation_map.md`; the index carries the same counter and would otherwise drift).
