# Router-gate dev/prod re-scope — 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:** Разрешить локальную разработку (composer/npm/git/worktree) через контроллера, сохранив блок боевого/опасного и дисциплины.

**Architecture:** Точечно расширить whitelist Bash-гейта (`enforce-router-gate.mjs`) дев-инструментами + разрешить dev-safe git в общем `shell-content-rules.mjs` (`classifyGitCommand`) с «стражем main» для push. Философия default-deny сохраняется; hard-blacklist опасного и дисциплинарные хуки не трогаются.

**Tech Stack:** Node ESM, vitest (`vitest.config.tools.mjs`, root `app`).

**Spec:** `docs/superpowers/specs/2026-06-02-router-gate-dev-prod-rescope-design.md`

**Verify-команда (вся регрессия tools):**
`npx vitest run --root app --config vitest.config.tools.mjs`
Узкий прогон файла: добавить хвост `<имя>.test` (например `enforce-router-gate.test`).

**Bootstrap-нюанс (важно):** до того как Task 3 (git dev-allow) применится, `git commit` ещё
заблокирован самим гейтом. Поэтому коммиты НЕ делаем по ходу — все правки складываем в рабочее
дерево, гоняем тесты, и **один раз** коммитим в конце (Task 5), когда git уже разрешён. Реализация —
в основной копии (worktree пока недоступен; это и есть bootstrap-исключение из спеки).

---

## Задачи

### Task 1: Разрешить `composer` (install/update/require/remove/dump-autoload)

**Files:**

- Modify: `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST ~line 59; SAFE_EXACT ~line 124)
- Test: `tools/enforce-router-gate.test.mjs`

- [ ] **Step 1: Write failing tests** — добавить в конец `enforce-router-gate.test.mjs`:

```js
import { matchBashHardBlacklist as mhb2, classifyBashCommand as cbc2 } from './enforce-router-gate.mjs';

describe('composer dev-allow (owner-authorized 2026-06-02)', () => {
  it('allows composer install', () => {
    expect(mhb2('composer install')).toBe(null);
    expect(cbc2('composer install', {}).result).toBe('allow');
  });
  it('allows composer require / update / dump-autoload', () => {
    expect(cbc2('composer require monolog/monolog', {}).result).toBe('allow');
    expect(cbc2('composer update', {}).result).toBe('allow');
    expect(cbc2('composer dump-autoload', {}).result).toBe('allow');
  });
  it('still allows composer install with -d working-dir', () => {
    expect(cbc2('composer install -d app --no-interaction', {}).result).toBe('allow');
  });
});
```

- [ ] **Step 2: Run to verify FAIL**

Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: FAIL (composer install currently hard-blacklisted → matchBashHardBlacklist truthy, classify 'block').

- [ ] **Step 3: Remove composer from hard-blacklist** — в `tools/enforce-router-gate.mjs` удалить строку:

```js
  { re: /\bcomposer\s+(?:install|update|require|remove)\b/, reason: 'composer install/update/require/remove запрещён' },
```

- [ ] **Step 4: Add composer to whitelist** — в массив `SAFE_EXACT`, рядом с существующей `/^composer\s+(?:show|outdated)\b/`, добавить:

```js
  /^composer\s+(?:install|update|require|remove|dump-autoload|dump)\b/, // dev-allow 2026-06-02
```

- [ ] **Step 5: Run to verify PASS**

Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: PASS (включая новый describe).

---

### Task 2: Разрешить `npm` (install/ci/run-скрипты)

**Files:**

- Modify: `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST ~line 60; SAFE_EXACT ~line 122)
- Test: `tools/enforce-router-gate.test.mjs`

- [ ] **Step 1: Write failing tests** — добавить describe:

```js
describe('npm dev-allow (owner-authorized 2026-06-02)', () => {
  it('allows npm install / i / ci', () => {
    expect(mhb2('npm install')).toBe(null);
    expect(cbc2('npm install', {}).result).toBe('allow');
    expect(cbc2('npm ci', {}).result).toBe('allow');
  });
  it('allows npm run <script>', () => {
    expect(cbc2('npm run build', {}).result).toBe('allow');
  });
});
```

- [ ] **Step 2: Run to verify FAIL**

Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: FAIL (npm install hard-blacklisted).

- [ ] **Step 3: Remove npm from hard-blacklist** — удалить строку:

```js
  { re: /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, reason: 'npm install/update/remove запрещён' },
```

- [ ] **Step 4: Add npm to whitelist** — в `SAFE_EXACT`, рядом с существующей `/^npm\s+(?:test|run\s+test|run\s+lint(?::[\w-]+)?)\b/`, добавить:

```js
  /^npm\s+(?:install|i|ci)\b/,        // dev-allow 2026-06-02
  /^npm\s+run\s+[\w:-]+/,             // dev-allow 2026-06-02 (любой script)
```

- [ ] **Step 5: Run to verify PASS**

Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: PASS.

---

### Task 3: Разрешить dev-safe git (commit/add/branch/switch/checkout/stash/worktree)

**Files:**

- Modify: `tools/shell-content-rules.mjs` (GIT_CONDITIONAL_SUB ~line 167; classifyGitCommand ~line 215)
- Test: `tools/shell-content-rules.test.mjs`

- [ ] **Step 1: Write failing tests** — добавить в `shell-content-rules.test.mjs`:

```js
import { classifyGitCommand as cgc2 } from './shell-content-rules.mjs';

describe('git dev-allow (owner-authorized 2026-06-02)', () => {
  const noApproval = { approvedGitOps: [], now: 0 };
  it('allows commit/add/branch/switch/checkout/stash/worktree without approval', () => {
    for (const c of [
      'git commit -m "x"', 'git add .', 'git branch feature-x',
      'git switch -c feature-x', 'git checkout -b feature-x',
      'git stash push -m wip', 'git worktree add ../wt -b feat origin/main',
    ]) {
      expect(cgc2(c, noApproval).result).toBe('allow');
    }
  });
  it('STILL blocks commit --no-verify and add -f (hard patterns)', () => {
    expect(cgc2('git commit --no-verify -m x', noApproval).result).toBe('block');
    expect(cgc2('git add -f ignored.txt', noApproval).result).toBe('block');
  });
  it('keeps merge/rebase/reset conditional (needs approval)', () => {
    expect(cgc2('git reset --hard HEAD~1', noApproval).result).toBe('block');
    expect(cgc2('git merge feature', noApproval).result).toBe('block');
  });
});
```

- [ ] **Step 2: Run to verify FAIL**

Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
Expected: FAIL (commit/branch/... currently conditional → block без approval; worktree → default-deny).

- [ ] **Step 3: Add GIT_DEV_SUB + trim GIT_CONDITIONAL_SUB** — в `tools/shell-content-rules.mjs`:

Заменить блок `GIT_CONDITIONAL_SUB`:

```js
const GIT_CONDITIONAL_SUB = new Set([
  'add', 'commit', 'merge', 'rebase', 'reset', 'checkout', 'switch',
  'branch', 'stash', 'cherry-pick', 'revert', 'pull', 'push', 'clean',
]);
```

на:

```js
// dev-safe (owner-authorized 2026-06-02): allow без approval. GIT_HARD_PATTERNS
// (--no-verify / add -f / -c / force / --output) пре-фильтруют опасное ВЫШЕ.
const GIT_DEV_SUB = new Set([
  'add', 'commit', 'branch', 'switch', 'checkout', 'stash', 'worktree',
]);
const GIT_CONDITIONAL_SUB = new Set([
  'merge', 'rebase', 'reset', 'cherry-pick', 'revert', 'pull', 'clean',
]);
```

- [ ] **Step 4: Insert dev-allow + push-guard в classifyGitCommand** — после блока `if (sub === 'remote') { … }` (≈line 213) и ПЕРЕД `// 3. conditional → approve check`, вставить:

```js
  // dev-safe git (owner-authorized 2026-06-02): hard-patterns уже отсеяли опасное выше.
  if (GIT_DEV_SUB.has(sub)) return { result: 'allow', reason: `dev-safe git ${sub}` };

  // push: фичевые ветки — allow; main/master — клик владельца (force уже заблокирован hard).
  if (sub === 'push') {
    if (/\b(?:main|master)\b/.test(norm)) {
      return { result: 'block', reason: 'git push в main/master — клик владельца' };
    }
    return { result: 'allow', reason: 'git push в фичевую ветку' };
  }
```

- [ ] **Step 5: Run to verify PASS**

Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
Expected: PASS.

---

### Task 4: «Страж main» для push — отдельные явные тесты

**Files:**

- Test: `tools/shell-content-rules.test.mjs` (логика уже добавлена в Task 3 Step 4 — тут только тесты-замок)

- [ ] **Step 1: Write tests**

```js
describe('git push main-guard (owner-authorized 2026-06-02)', () => {
  const na = { approvedGitOps: [], now: 0 };
  it('allows push to a feature branch', () => {
    expect(cgc2('git push origin worktree-lead-region-tails', na).result).toBe('allow');
    expect(cgc2('git push', na).result).toBe('allow');
    expect(cgc2('git push -u origin feature-x', na).result).toBe('allow');
  });
  it('blocks push to main/master', () => {
    expect(cgc2('git push origin main', na).result).toBe('block');
    expect(cgc2('git push origin HEAD:main', na).result).toBe('block');
    expect(cgc2('git push origin master', na).result).toBe('block');
  });
  it('blocks force-push (hard pattern, unchanged)', () => {
    expect(cgc2('git push --force origin feature-x', na).result).toBe('block');
    expect(cgc2('git push origin feature-x --force-with-lease', na).result).toBe('block');
  });
});
```

- [ ] **Step 2: Run to verify PASS** (логика из Task 3 уже на месте)

Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
Expected: PASS.

---

### Task 5: Полная регрессия + коммит в фичевую ветку + PR

- [ ] **Step 1: Полная регрессия tools**

Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: всё GREEN (baseline ~1989 + новые). 0 падений.

- [ ] **Step 2: Дымовая проверка живьём** — после правок гейт читается заново; проверить, что
  ранее блокированное теперь проходит (а опасное — нет). Прогнать через Bash:

```
composer --version
```

Expected: проходит (раньше любой `composer install` блокировался; `--version` и так был ок — проверка, что не сломали). Затем убедиться, что `git worktree list` (readonly) и `git status` работают.

- [ ] **Step 3: Создать фичевую ветку + worktree (теперь разрешено) и закоммитить**

```bash
git worktree add "../worktree-gate-rescope" -b feat/gate-dev-prod-rescope origin/main
```

(или коммит в основной копии на новой ветке — на усмотрение исполнителя; main НЕ трогать)

```bash
git add tools/enforce-router-gate.mjs tools/shell-content-rules.mjs \
        tools/enforce-router-gate.test.mjs tools/shell-content-rules.test.mjs \
        docs/superpowers/specs/2026-06-02-router-gate-dev-prod-rescope-design.md \
        docs/superpowers/plans/2026-06-02-router-gate-dev-prod-rescope.md
git commit -m "feat(gate): re-scope router-gate — allow local dev (composer/npm/git/worktree), keep prod+discipline blocks"
git push origin feat/gate-dev-prod-rescope
```

- [ ] **Step 4: Открыть PR (клик владельца)** — дать владельцу ссылку из вывода `git push`; слияние в main — его клик.

---

## Self-Review

- **Spec coverage:** composer (Task 1) ✓ / npm (Task 2) ✓ / git dev-subs + worktree (Task 3) ✓ /
  push main-guard (Task 4) ✓ / discipline+prod untouched (явно не трогаем в Task 1-4) ✓ /
  «main = owner» (push-guard + PR в Task 5) ✓.
- **Placeholders:** нет — весь код приведён дословно.
- **Type/имена:** `GIT_DEV_SUB` / `GIT_CONDITIONAL_SUB` согласованы Task 3↔4; `classifyGitCommand`,
  `matchBashHardBlacklist`, `classifyBashCommand` — реальные экспортируемые имена (проверено по коду).
- **Bootstrap:** коммит батчем в Task 5 (git разрешается только после применения Task 3) — учтено.
