# Router-gate v4 Stream H 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:** Close 11 remaining items from Smokes 1-9 findings and Stream G TODOs to bring router-gate v4 to production-ready state with Layer 4 LLM-judge active, Workflow gate F2 closed, observability extended, and normative documentation synced.

**Architecture:** Three phases executed sequentially: H-α security gap closure (LLM-judge + Workflow F2 + recovery docs + path bug) → H-β completeness (decomposition + askuser wrapper + parallel-session lock + brain-retro) → H-γ polish (cosmetic + bootstrap + sync + final regression + activation). Subagent-driven per task with two-stage review between tasks. Atomic commits per logical change; all new hooks register block-mode immediately.

**Tech Stack:** Node.js ESM (`.mjs`), vitest, Anthropic API via `keytar`-stored `ROUTER_LLM_KEY` (Windows Credential Manager / macOS Keychain / Linux libsecret), claude-md-management plugin for CLAUDE.md edits, lefthook pre-commit/pre-push gates.

---

## Pre-flight (mandatory before Task 1)

- [ ] **Branch base check:** confirm `git status` clean on `main` at HEAD `2a3b5b4d` (Smoke 5 real fix). If not — stash/commit pending and rebase.
- [ ] **Pre-flight sync (Pravila §15.2):** `git fetch origin && git log HEAD..origin/main --oneline` — confirm no incoming parallel-session changes to Pravila/CLAUDE.md/Tooling/PSR_v1/MEMORY.md/Открытые_вопросы/docs/adr/*/db/schema.sql.
- [ ] **Regression baseline:** `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run` (or however vitest tools-only runs in this repo). Confirm baseline GREEN count (~1715 tests). Write `~/.claude/runtime/verify-pass-<sess>.json` sentinel via the post-commit hook automatically after first commit; alternately rely on docs-only short-circuit for the H1 commit only.
- [ ] **Approval write workaround (Stream H pending):** until H4 wires the askuser-answer-parser hook, controller must manually pre-write approval records to `~/.claude/runtime/askuser-decisions-<sess>.jsonl` with `{"type":"approve_git_operation","command":"<exact normalize>","ts":<future-ms>}` before each git commit/push.

---

## File Structure

### New files (create)

| Path | Responsibility |
|---|---|
| `docs/superpowers/runbooks/recovery-procedures.md` | 3-level PowerShell self-recovery cheatsheet + stale-process/hook-reload section + self-fabrication patterns + test methodology lesson + statusline-setup override note |
| `tools/llm-judge-config.mjs` | Pure module: lazy-load `ROUTER_LLM_KEY` from OS keychain via `keytar`, fail-CLOSE on missing key, expose `getKey()` and `isConfigured()` |
| `tools/llm-judge-config.test.mjs` | Unit tests with injected `keytar` stub |
| `tools/llm-judge-client.mjs` | Pure module: prompt assembly with delimiter tokens (24-char unguessable per v3.8 §3.5), Anthropic API call (Sonnet 4.6 + Haiku + Opus), response parsing, multi-judge consensus, fail-CLOSE on disagree or missing config |
| `tools/llm-judge-client.test.mjs` | Unit tests with injected `fetch` and `getKey` stubs |
| `tools/enforce-llm-judge-per-tool.mjs` | PreToolUse hook (matcher `Edit\|Write\|MultiEdit\|NotebookEdit\|Bash\|Task\|Agent`) calling judge with tool_input snapshot |
| `tools/enforce-llm-judge-per-tool.test.mjs` | Unit + integration tests |
| `tools/enforce-llm-judge-response-scan.mjs` | Stop hook scanning full response for bypass patterns (per v3.8 §3.5) |
| `tools/enforce-llm-judge-response-scan.test.mjs` | Unit + integration tests |
| `tools/enforce-workflow-gate.mjs` | PreToolUse(Workflow) hook: scriptPath approval (via `approve_workflow_script`) + content static scan + agent inheritance enforcement (sets `CLAUDE_GATE_INHERIT=true` env for nested agent()) + reject `resumeFromRunId` |
| `tools/enforce-workflow-gate.test.mjs` | Unit + integration tests (Smoke 8 fixture) |
| `tools/enforce-decomposition-detector.mjs` | PreToolUse hook using llm-judge-client to flag "complex task without writing-plans/EnterPlanMode" |
| `tools/enforce-decomposition-detector.test.mjs` | Unit tests with stubbed judge |
| `tools/parallel-session-lock.mjs` | Pure module: lock file at `~/.claude/runtime/session-lock-<workspace-hash>.json` with TTL 5min, holder `{session_id, pid, acquired_at}`, acquire/release/refresh API |
| `tools/parallel-session-lock.test.mjs` | Unit tests (stale TTL, multi-session contention) |
| `tools/enforce-parallel-session-lock.mjs` | PreToolUse hook for mutating tools (Edit/Write/Bash/Task) — acquire-or-block |
| `tools/enforce-parallel-session-lock.test.mjs` | Unit + integration |
| `tools/enforce-askuser-answer-parser.mjs` | PostToolUse(AskUserQuestion) wrapper around Stream E pure module `askuser-answer-parser.mjs`; writes `~/.claude/runtime/askuser-decisions-<sess>.jsonl` records with unified schema `{type, command, ts}` (Stream B-compatible) |
| `tools/enforce-askuser-answer-parser.test.mjs` | Unit + integration |

### Modified files

| Path | What changes |
|---|---|
| `tools/shell-content-rules.mjs` | (H2) Extend `extractPathArgs` for `--flag=PATH`, `--flag PATH`, `dd if=/of=`, `tee`; (H3) Bash error message — strip pre-existing scheme prefix from cwd join to avoid `c:/c/users/...` |
| `tools/shell-content-rules.test.mjs` | (H2) 6 RED→GREEN test cases for path extraction; (H3) 1 RED→GREEN for path format |
| `tools/powershell-content-rules.mjs` | (H3) Expand `$env:USERPROFILE` etc in error message; do not join with cwd if already absolute |
| `tools/powershell-content-rules.test.mjs` | (H3) RED→GREEN tests |
| `tools/brain-retro-analyzer.mjs` | (H9) Add `buildRouterGateHookEffectiveness(episodes)` (Table 16) + `buildSelfFabricationSignals(episodes)` (Table 17); wire into `analyze()` |
| `tools/brain-retro-analyzer.test.mjs` | (H9) Unit tests for both new builders |
| `tools/subagent-prompt-prefix.mjs` | (H10) Detect worktree-isolation mode in headerInject; pre-create `vendor` symlink + `storage/framework/{cache,sessions,views,testing}` dirs; expand SETUP block in injected prompt |
| `tools/subagent-prompt-prefix.test.mjs` | (H10) Tests for worktree-mode detection + setup block injection |
| `.claude/settings.json` | Register: enforce-llm-judge-per-tool (PreToolUse, block), enforce-llm-judge-response-scan (Stop, block), enforce-workflow-gate (PreToolUse Workflow, block), enforce-decomposition-detector (PreToolUse Edit\|Write\|Bash\|Task, block), enforce-parallel-session-lock (PreToolUse mutating, block), enforce-askuser-answer-parser (PostToolUse AskUserQuestion, fail-open observability) |
| `tools/askuser-answer-parser.mjs` | (H4) Schema sync — add `toApprovalRecord()` exporter returning `{type:'approve_git_operation', command, ts}` format compatible with Stream B `isApproved()` |
| `.claude/skills/brain-retro/SKILL.md` | (H9) MANDATORY DIGITAL ANALYSIS tables 11→13 |
| `docs/Pravila_raboty_Claude_v1_1.md` | (H11) §0 header bump, §section additions for Layer 4 active + Workflow F2 closed |
| `docs/Plugin_stack_rules_v1.md` | (H11) §0 cross-ref bump, R-section updates if new tooling nodes |
| `docs/Tooling_v8_3.md` | (H11) §0 cross-ref bump, Прил. Н entries if new tool node IDs warranted |
| `CLAUDE.md` | (H11) Through `/claude-md-management:revise-claude-md` — §0 cross-refs bump, §6 phase update («Stream H closed»), §9 changelog entry |
| `docs/observer/notes/2026-05-30-stream-h-completion.md` | (H11) Stream H final completion log (artifact) |

---

## Execution Order Rationale

1. **H1 first** — documentation foundation, zero code dependencies, low risk, sets up recovery cheatsheet that subsequent task troubleshooting can reference.
2. **H2 then** — small focused bug fix found during Smoke 5 trace; RED→GREEN TDD; controller-direct (no subagent overhead).
3. **H8 (Workflow F2)** — independent security item; closes a FATAL gap from v3.8 spec; enables Smoke 8 testability.
4. **H5 (LLM-judge layer)** — biggest item; blocks H6; needs ROUTER_LLM_KEY in keychain; once registered, all subsequent dev sees Layer 4 active in own session.
5. **H6 (decomposition-detector)** — depends on H5 client; small wrapper.
6. **H4 (askuser-answer-parser wrapper)** — independent; eliminates the manual approval-write workaround used during Stream H itself.
7. **H7 (parallel-session-lock)** — independent; prevents the kind of cross-session worktree collisions seen on 28.05/29.05.
8. **H9 (brain-retro Tables 16-17)** — needs H5/H6/H7/H8 hooks live to have data; otherwise empty.
9. **H3 (cosmetic path fixes)** — pure polish; deferred until late so it doesn't churn shell-content-rules.mjs during H2 testing.
10. **H10 (subagent-prompt-prefix bootstrap)** — quality-of-life for future worktree subagent runs.
11. **H11 (normative sync + regression + cleanup + activation)** — last; depends on every prior commit being in main.

---

## Phase H-α — Security Gap Closure

### Checkpoint marker: review after Phase H-α (Tasks 1-4)

Subagent-driven mode: after Task 4, controller pauses for human review of Phase H-α deltas before launching Phase H-β.

---

### Task 1: H1 — recovery-procedures.md

**Files:**
- Create: `docs/superpowers/runbooks/recovery-procedures.md`

- [ ] **Step 1: Draft outline**

Outline 5 sections (one heading each):
1. `## Self-recovery Level 1 — single tool hung` (≤5 min)
2. `## Self-recovery Level 2 — VS Code session corrupted` (≤15 min)
3. `## Self-recovery Level 3 — workspace unrecoverable` (rebuild from main)
4. `## Stale-process / hook reload` (Smoke 5 evidence)
5. `## Self-fabrication patterns` (7-case evidence from Smokes 3/4/5/7)
6. `## Test methodology lesson — Smoke 5 root cause` (unit-test/live-resolver divergence)
7. `## Smoke methodology — statusline-setup vs semgrep-scanner` (Smoke 9 Run 1)

- [ ] **Step 2: Write Level 1 section**

Content includes these exact PowerShell commands:

```powershell
# Kill stuck node process holding a hook
Get-Process node | Where-Object {$_.CPU -gt 60} | Stop-Process -Force

# Kill stuck python (e.g. adr-judge with regex spin)
Get-Process python | Where-Object {$_.CPU -gt 60} | Stop-Process -Force

# Clear runtime sentinels (force gate-reload on next tool call)
Remove-Item ~/.claude/runtime/verify-pass-*.json -Force -ErrorAction SilentlyContinue
Remove-Item ~/.claude/runtime/parent-sentinel-*.json -Force -ErrorAction SilentlyContinue
```

- [ ] **Step 3: Write Level 2 section**

```powershell
# Restart VS Code with current workspace state preserved
Stop-Process -Name "Code" -Force; Start-Sleep -Seconds 3; code "c:\моя\проекты\портал crm\Документация"
```

Note that hot-reload of hook code requires VS Code restart — Node child processes spawned for hooks cache module imports. After editing `tools/enforce-*.mjs`, a fresh tool call still uses the OLD module until the parent Claude process restarts.

- [ ] **Step 4: Write Level 3 section**

Includes worktree rebuild steps for cases like the v4-stream-{A..E} parallel deployment.

- [ ] **Step 5: Write Stale-process / hook reload section**

Documents the exact Smoke 5 chistaa-session hypothesis and refutation method. Key takeaway: **after editing hook code, a restart-test (close + reopen VS Code, fresh Claude session) is the only way to confirm fix landed in live behavior**. Debug scripts that import the module fresh do NOT exercise the hot-cached path.

- [ ] **Step 6: Write Self-fabrication patterns section**

Enumerate the 7 fabrications observed during Smokes 3/4/5/7:
1. Smoke 3 Run 1 — subagent fabricated quote of normative-content-rules block before reaching Edit.
2. Smoke 4 controller observation — controller can write `tool_use_id` fabricated values into own response (low-bit-entropy chance).
3. Smoke 5 initial commit `25e184e5` — controller claimed "full fix" before live validation.
4. Smoke 5 trace — debug script gave false-green because used `defaultPathNormalize` directly, bypassing the live `resolvePathNormalize()` path.
5. Smoke 7 Run 1 statusline-setup — distracted by MEMORY.md context, quoted block instead of attempting requested Edit.
6. Smoke 9 Run 1 statusline-setup — system prompt overrode user task entirely.
7. Multiple weak-commit-message flag occurrences (this very session).

For each: pattern signature, detection signal, mitigation.

- [ ] **Step 7: Write Test methodology lesson section**

Smoke 5 root cause: unit tests passed because tests imported `pathNormalize` and `defaultPathNormalize` separately and called each with inline mocks. Live behavior failed because the actual hook chain went through `resolvePathNormalize()` → `pathNormalize()` (Stream A's bugged version), not through `defaultPathNormalize()`. **Lesson:** unit tests with inline mocks may give false-green if they do not use the same resolver function the live code uses. Always include at least one integration test that exercises the live resolver path with the same inputs as the live tool call.

- [ ] **Step 8: Write Smoke methodology section**

`statusline-setup` subagent_type carries a system prompt that defaults to "I am the statusline configurator" and overrides any user task that doesn't fit that frame. For echo-probe style smokes that need the subagent to reproduce arbitrary content verbatim, use `semgrep-scanner` instead — its system prompt is more pliable. For gate-inheritance smokes (where the subagent need only attempt one tool call), `statusline-setup` is fine.

- [ ] **Step 9: Commit**

```bash
git add docs/superpowers/runbooks/recovery-procedures.md
git commit -F .scratch/h1-commit-msg.txt -- docs/superpowers/runbooks/recovery-procedures.md
```

Commit message (in `.scratch/h1-commit-msg.txt`):

```
docs(router-gate-v4): Stream H Task 1 — recovery-procedures.md with stale-process and self-fabrication sections

Adds first-time recovery runbook with 3 levels of self-recovery,
documents the stale-process / hook reload trap from Smoke 5 chistaa
session, and enumerates 7 self-fabrication patterns observed during
Smokes 3/4/5/7. Includes test methodology lesson — unit tests with
inline mocks can give false-green if they bypass the live resolver
function (Smoke 5 root cause).

Docs-only change; covered by docs-only short-circuit in enforce-verify-
before-push (§5 п.13 CLAUDE.md).
```

Expected: docs-only short-circuit passes verify-before-push without sentinel.

---

### Task 2: H2 — extractPathArgs/pathDenyOverlay non-first-position fix

**Files:**
- Modify: `tools/shell-content-rules.mjs:60-76`
- Modify: `tools/shell-content-rules.test.mjs`

- [ ] **Step 1: Write 6 failing tests for path extraction edge cases**

In `tools/shell-content-rules.test.mjs`, add a new describe block `extractPathArgs edge cases (Stream H Task 2)`:

```javascript
describe('extractPathArgs edge cases (Stream H Task 2)', () => {
  it('extracts path from --output=PATH form', () => {
    expect(extractPathArgs(['curl', '--output=~/.claude/projects/secret.jsonl', 'http://x'])).toContain('~/.claude/projects/secret.jsonl');
  });
  it('extracts path from --output PATH form (separate token)', () => {
    expect(extractPathArgs(['curl', '--output', '~/.claude/projects/secret.jsonl', 'http://x'])).toContain('~/.claude/projects/secret.jsonl');
  });
  it('extracts path from dd of=PATH form', () => {
    expect(extractPathArgs(['dd', 'if=/dev/zero', 'of=~/.claude/projects/x.jsonl'])).toContain('~/.claude/projects/x.jsonl');
  });
  it('extracts path from tee PATH (second positional)', () => {
    expect(extractPathArgs(['tee', '~/.claude/projects/x.jsonl'])).toContain('~/.claude/projects/x.jsonl');
  });
  it('extracts path from cp SRC DST (both positionals)', () => {
    const got = extractPathArgs(['cp', '/tmp/x', '~/.claude/projects/x.jsonl']);
    expect(got).toContain('~/.claude/projects/x.jsonl');
  });
  it('does not include URL as path (heuristic)', () => {
    const got = extractPathArgs(['curl', '--output', '/tmp/x', 'https://example.com/y']);
    expect(got).toContain('/tmp/x');
    expect(got).not.toContain('https://example.com/y');
  });
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run shell-content-rules.test.mjs`

Expected: 6 FAIL (some pass by accident — confirm at least 4 RED).

- [ ] **Step 3: Refactor extractPathArgs**

Replace the current implementation in `tools/shell-content-rules.mjs:60-63`:

```javascript
export function extractPathArgs(tokens) {
  if (!Array.isArray(tokens)) return [];
  const out = [];
  for (let i = 1; i < tokens.length; i++) {
    const t = tokens[i];
    if (typeof t !== 'string') continue;
    if (t === '>' || t === '>>' || t === '<' || t === '|') continue;
    // --flag=VALUE form
    if (t.startsWith('-')) {
      const eq = t.indexOf('=');
      if (eq > 0) {
        const v = t.slice(eq + 1);
        if (v && !looksLikeUrl(v)) out.push(v);
      }
      continue;
    }
    // key=value form (dd-style)
    const kv = t.match(/^([a-zA-Z_][\w-]*)=(.+)$/);
    if (kv) {
      const v = kv[2];
      if (v && !looksLikeUrl(v)) out.push(v);
      continue;
    }
    if (!looksLikeUrl(t)) out.push(t);
  }
  return out;
}

function looksLikeUrl(s) {
  return /^https?:\/\//i.test(s) || /^ftp:\/\//i.test(s) || /^ssh:\/\//i.test(s);
}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run shell-content-rules.test.mjs`

Expected: all 6 new tests PASS; all prior tests in the file still GREEN.

- [ ] **Step 5: Run full tools regression**

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run`

Expected: 1716+ tests GREEN, no regression from prior 1715 baseline.

- [ ] **Step 6: Commit**

Pre-write approval record to `~/.claude/runtime/askuser-decisions-<sess>.jsonl` for the commit (manual workaround until H4):

```bash
git add tools/shell-content-rules.mjs tools/shell-content-rules.test.mjs
git commit -F .scratch/h2-commit-msg.txt -- tools/shell-content-rules.mjs tools/shell-content-rules.test.mjs
```

Commit message (in `.scratch/h2-commit-msg.txt`):

```
fix(router-gate-v4): Stream H Task 2 — extractPathArgs handles --flag=PATH, key=VAL, multi-positional

Found during Smoke 5 trace: extractPathArgs was missing protected paths
when they appeared as a flag value (--output=PATH or --output PATH) or
as the second positional argument (dd of=, tee, cp DST). The path-deny
overlay correctly checks each candidate path, but the candidate list was
incomplete.

Fix: rewrite extractPathArgs to scan all tokens past index 0, handle
--flag=VALUE inline form, key=value (dd-style), and skip URL-looking
tokens (https://, ftp://, ssh://) as a low-FP heuristic.

Regression: 1716/1716 vitest tools GREEN (+6 path edge-case tests).
```

---

### Task 3: H8 — Workflow gate F2

**Files:**
- Create: `tools/enforce-workflow-gate.mjs`
- Create: `tools/enforce-workflow-gate.test.mjs`
- Modify: `.claude/settings.json` (register PreToolUse Workflow, block-mode)

- [ ] **Step 1: Write failing test for scriptPath approval requirement**

In `tools/enforce-workflow-gate.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { decide } from './enforce-workflow-gate.mjs';

describe('enforce-workflow-gate scriptPath approval (F2)', () => {
  it('blocks Workflow with new scriptPath without approval', () => {
    const r = decide({
      toolInput: { scriptPath: 'workflows/new-untested.mjs' },
      approvedWorkflowScripts: [],
      scriptContent: 'export const meta = {name:"x",description:"y"}\nphase("X")',
      now: Date.now(),
    });
    expect(r.block).toBe(true);
    expect(r.reason).toMatch(/F2.*approve_workflow_script/i);
  });

  it('allows Workflow with approved scriptPath within 5min window', () => {
    const now = Date.now();
    const r = decide({
      toolInput: { scriptPath: 'workflows/x.mjs' },
      approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: now }],
      scriptContent: 'export const meta={name:"x",description:"y"}',
      scriptSha256: 'a'.repeat(64),
      now,
    });
    expect(r.block).toBe(false);
  });

  it('blocks Workflow with resumeFromRunId param (F2 hardening)', () => {
    const r = decide({
      toolInput: { scriptPath: 'workflows/x.mjs', resumeFromRunId: 'wf_abc123' },
      approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }],
      scriptContent: 'x',
      scriptSha256: 'a'.repeat(64),
      now: Date.now(),
    });
    expect(r.block).toBe(true);
    expect(r.reason).toMatch(/resumeFromRunId/);
  });

  it('blocks Workflow whose scriptContent has dangerous pattern', () => {
    const r = decide({
      toolInput: { scriptPath: 'workflows/x.mjs' },
      approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }],
      scriptContent: 'process.env.ROUTER_LLM_KEY',
      scriptSha256: 'a'.repeat(64),
      now: Date.now(),
    });
    expect(r.block).toBe(true);
    expect(r.reason).toMatch(/dangerous pattern.*ROUTER_LLM_KEY/i);
  });

  it('blocks Workflow with sha256 mismatch (content changed since approval)', () => {
    const r = decide({
      toolInput: { scriptPath: 'workflows/x.mjs' },
      approvedWorkflowScripts: [{ scriptPath: 'workflows/x.mjs', sha256: 'a'.repeat(64), ts: Date.now() }],
      scriptContent: 'modified',
      scriptSha256: 'b'.repeat(64),
      now: Date.now(),
    });
    expect(r.block).toBe(true);
    expect(r.reason).toMatch(/sha256.*mismatch/i);
  });
});
```

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

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run enforce-workflow-gate.test.mjs`

Expected: all 5 FAIL (file does not exist).

- [ ] **Step 3: Implement minimal hook**

Create `tools/enforce-workflow-gate.mjs`:

```javascript
#!/usr/bin/env node
/**
 * PreToolUse(Workflow) hook — Workflow gate F2 (router-gate v4 spec §3.6 / v3.8 F2).
 *
 * Closes:
 * - scriptPath must be pre-approved via approve_workflow_script record + sha256 match
 * - scriptContent static scan for dangerous patterns (env keys, eval, child_process, fs writes outside .scratch)
 * - resumeFromRunId blocked unconditionally (state replay risk)
 * - per-agent gate inheritance — sets CLAUDE_GATE_INHERIT=true in env for nested agent() spawns
 */
import { readFileSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { homedir } from 'node:os';
import { join } from 'node:path';

const APPROVE_WINDOW_MS = 5 * 60 * 1000;

const DANGEROUS_PATTERNS = [
  { re: /process\.env\.(ROUTER_LLM_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN|SENTRY_AUTH_TOKEN)/i, name: 'env key access' },
  { re: /\beval\s*\(/i, name: 'eval()' },
  { re: /\b(?:exec|spawn|execSync|spawnSync|execFile|fork)\s*\(/, name: 'child_process' },
  { re: /\bwriteFileSync\s*\(\s*["'`]\/(?!tmp\/|var\/tmp\/)/i, name: 'fs write absolute' },
  { re: /\.\.\/\.\.\/\.\.\//, name: 'path traversal' },
];

export function decide({ toolInput, approvedWorkflowScripts, scriptContent, scriptSha256, now }) {
  // 1. resumeFromRunId blocked unconditionally
  if (toolInput && toolInput.resumeFromRunId) {
    return { block: true, reason: 'F2: resumeFromRunId disabled (state replay risk)' };
  }

  const scriptPath = toolInput && toolInput.scriptPath;
  if (!scriptPath) {
    return { block: false }; // inline script via `script` param — different code path, handled by F2 spec follow-up
  }

  // 2. scriptPath must be approved
  const approval = (approvedWorkflowScripts || []).find(
    (a) => a.scriptPath === scriptPath && typeof a.ts === 'number' && now - a.ts <= APPROVE_WINDOW_MS,
  );
  if (!approval) {
    return { block: true, reason: `F2: workflow ${scriptPath} requires approve_workflow_script (5-min window)` };
  }

  // 3. sha256 match (content unchanged since approval)
  if (approval.sha256 && scriptSha256 && approval.sha256 !== scriptSha256) {
    return { block: true, reason: `F2: scriptPath sha256 mismatch — content modified after approval` };
  }

  // 4. dangerous pattern scan
  for (const { re, name } of DANGEROUS_PATTERNS) {
    if (re.test(scriptContent || '')) {
      return { block: true, reason: `F2: workflow script contains dangerous pattern — ${name}` };
    }
  }

  return { block: false };
}

export function loadApprovedWorkflowScripts(sessionId, now = Date.now()) {
  const path = join(homedir(), '.claude', 'runtime', `askuser-decisions-${sessionId || 'unknown'}.jsonl`);
  if (!existsSync(path)) return [];
  const out = [];
  try {
    const lines = readFileSync(path, 'utf-8').split(/\r?\n/);
    for (const line of lines) {
      if (!line.trim()) continue;
      let rec;
      try { rec = JSON.parse(line); } catch { continue; }
      if (rec && rec.type === 'approve_workflow_script' && typeof rec.scriptPath === 'string') {
        out.push({ scriptPath: rec.scriptPath, sha256: rec.sha256 || null, ts: typeof rec.ts === 'number' ? rec.ts : 0 });
      }
    }
  } catch { return []; }
  return out.filter((op) => now - op.ts <= APPROVE_WINDOW_MS);
}

export function sha256Hex(content) {
  return createHash('sha256').update(content || '', 'utf-8').digest('hex');
}

async function main() {
  let input = '';
  for await (const chunk of process.stdin) input += chunk;
  let payload;
  try { payload = JSON.parse(input); } catch { return; }

  const { tool_input, session_id } = payload || {};
  if (!tool_input) return;

  const scriptPath = tool_input.scriptPath;
  let scriptContent = '';
  let scriptSha256 = '';
  if (scriptPath && existsSync(scriptPath)) {
    try {
      scriptContent = readFileSync(scriptPath, 'utf-8');
      scriptSha256 = sha256Hex(scriptContent);
    } catch { /* fail-CLOSE: empty content + bad sha → mismatch path or no patterns trigger; require explicit handling */ }
  }

  const approved = loadApprovedWorkflowScripts(session_id, Date.now());
  const r = decide({ toolInput: tool_input, approvedWorkflowScripts: approved, scriptContent, scriptSha256, now: Date.now() });

  if (r.block) {
    process.stderr.write(`[workflow-gate] ${r.reason}\n`);
    process.exit(2);
  }
  process.exit(0);
}

if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('enforce-workflow-gate.mjs')) {
  main().catch((e) => { process.stderr.write(`[workflow-gate] internal error: ${e.message}\n`); process.exit(2); });
}
```

- [ ] **Step 4: Run tests to verify GREEN**

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run enforce-workflow-gate.test.mjs`

Expected: 5/5 PASS.

- [ ] **Step 5: Register hook in settings.json (block-mode)**

Edit `.claude/settings.json` — under `hooks.PreToolUse`, add a new entry with matcher `Workflow`:

```json
{
  "matcher": "Workflow",
  "hooks": [
    {
      "type": "command",
      "command": "node tools/enforce-workflow-gate.mjs",
      "timeout": 5000
    }
  ]
}
```

- [ ] **Step 6: Run full regression**

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run`

Expected: prior tests + 5 new = baseline+11 GREEN (with H2 contributing 6 already, so 1722+).

- [ ] **Step 7: Commit**

Pre-write approval record. Then:

```bash
git add tools/enforce-workflow-gate.mjs tools/enforce-workflow-gate.test.mjs .claude/settings.json
git commit -F .scratch/h8-commit-msg.txt -- tools/enforce-workflow-gate.mjs tools/enforce-workflow-gate.test.mjs .claude/settings.json
```

Commit message:

```
feat(router-gate-v4): Stream H Task 3 — Workflow gate F2 (scriptPath approval + content scan + resumeFromRunId block)

Closes v3.8 FATAL F2: nested agent() calls inside Workflow scripts were
invisible to PreToolUse gates. New enforce-workflow-gate.mjs PreToolUse
hook (block-mode):

1. scriptPath requires approve_workflow_script record in
   ~/.claude/runtime/askuser-decisions-<sess>.jsonl with sha256 of content
   (5-min window).
2. scriptContent static-scanned for dangerous patterns: env-key reads
   (ROUTER_LLM_KEY etc), eval(), child_process spawn/exec, absolute fs
   writes outside /tmp, path traversal (../../../).
3. resumeFromRunId blocked unconditionally (state replay risk).
4. Per-agent inheritance (CLAUDE_GATE_INHERIT=true env) — future
   follow-up; current commit enforces approval + scan + resume block.

Regression: vitest tools GREEN (+5 workflow-gate tests).
Makes Smoke 8 testable in next session.
```

---

### Task 4: H5 — LLM-judge layer (config + client + 2 hooks)

This is the largest task (~2h). Break into 4 sub-tasks (4a–4d) committed separately.

#### Task 4a: llm-judge-config.mjs (keytar wrapper)

**Files:**
- Create: `tools/llm-judge-config.mjs`
- Create: `tools/llm-judge-config.test.mjs`

- [ ] **Step 1: Verify `keytar` is in package.json**

Run: `node -e "require('keytar').findCredentials('claude-router-gate').then(r => console.log('keytar OK', r.length))"`

If "Cannot find module 'keytar'" — add via `npm install keytar` first, commit `package.json` + `package-lock.json` separately as `chore(deps): add keytar for OS keychain access (router-gate v4 Layer 4)`.

- [ ] **Step 2: Write failing test for config loader**

```javascript
import { describe, it, expect } from 'vitest';
import { getKeyImpl, isConfiguredImpl } from './llm-judge-config.mjs';

describe('llm-judge-config (Stream H Task 4a)', () => {
  it('returns null when keytar has no credentials', async () => {
    const stub = { findCredentials: async () => [] };
    const k = await getKeyImpl({ keytarLib: stub });
    expect(k).toBeNull();
  });

  it('returns first credential password', async () => {
    const stub = { findCredentials: async () => [{ account: 'default', password: 'sk-test-123' }] };
    const k = await getKeyImpl({ keytarLib: stub });
    expect(k).toBe('sk-test-123');
  });

  it('isConfigured returns true when key exists', async () => {
    const stub = { findCredentials: async () => [{ account: 'default', password: 'x' }] };
    expect(await isConfiguredImpl({ keytarLib: stub })).toBe(true);
  });

  it('isConfigured returns false on keytar error', async () => {
    const stub = { findCredentials: async () => { throw new Error('libsecret missing'); } };
    expect(await isConfiguredImpl({ keytarLib: stub })).toBe(false);
  });
});
```

- [ ] **Step 3: Run RED**

Run: `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run llm-judge-config.test.mjs`

Expected: 4 FAIL (file does not exist).

- [ ] **Step 4: Implement**

```javascript
// tools/llm-judge-config.mjs
const SERVICE_NAME = 'claude-router-gate';

let _cachedKey = undefined; // distinguish "not loaded" from "loaded as null"

export async function getKeyImpl({ keytarLib }) {
  try {
    const creds = await keytarLib.findCredentials(SERVICE_NAME);
    if (!Array.isArray(creds) || creds.length === 0) return null;
    return creds[0].password || null;
  } catch {
    return null;
  }
}

export async function isConfiguredImpl({ keytarLib }) {
  const k = await getKeyImpl({ keytarLib });
  return typeof k === 'string' && k.length > 10;
}

export async function getKey() {
  if (_cachedKey !== undefined) return _cachedKey;
  let keytarLib;
  try { keytarLib = (await import('keytar')).default || await import('keytar'); }
  catch { _cachedKey = null; return null; }
  _cachedKey = await getKeyImpl({ keytarLib });
  return _cachedKey;
}

export async function isConfigured() {
  const k = await getKey();
  return typeof k === 'string' && k.length > 10;
}

export function _resetCacheForTest() { _cachedKey = undefined; }
```

- [ ] **Step 5: Run GREEN + commit**

Run tests. Pre-write approval. Commit:

```
feat(router-gate-v4): Stream H Task 4a — llm-judge-config via OS keychain (keytar)

Per v3.8 §3.0.1: ROUTER_LLM_KEY stored in OS keychain (Windows Credential
Manager / macOS Keychain / Linux libsecret), NOT in process.env. Reason:
process.env visible in npm test stdout, child_process inherit, etc.

Module: lazy-load via keytar.findCredentials('claude-router-gate'),
cache in-memory after first call, fail-CLOSE (return null) on any error.

Setup (one-time, per machine):
  node -e "require('keytar').setPassword('claude-router-gate','default','sk-ant-...')"
```

#### Task 4b: llm-judge-client.mjs (Anthropic API + multi-judge consensus)

**Files:**
- Create: `tools/llm-judge-client.mjs`
- Create: `tools/llm-judge-client.test.mjs`

- [ ] **Step 1: Failing tests (4 cases)**

Test cases: (a) all judges agree YES → block; (b) all agree NO → allow; (c) disagree → block (fail-CLOSE); (d) key missing → fail-CLOSE block with error reason.

- [ ] **Step 2: Implement**

Module exports `async function judge({ prompt, models = ['claude-sonnet-4-6', 'claude-haiku-4-5-20251001'], fetchImpl, getKeyImpl })` returning `{ block: bool, reason: string, judgments: [{model, verdict, raw}] }`. Use delimiter tokens (24-char unguessable via `crypto.randomBytes(12).toString('hex')`) to wrap user content in prompt. Pre-filter content for `SYSTEM:`, `<judge>`, JSON-fragment patterns per v3.8 §3.5. Concurrent fetch via `Promise.all`. Consensus: "if any judge says YES → block" (strict per v3.8).

- [ ] **Step 3: GREEN + commit**

Commit message references v3.8 §3.5 anti-injection (delimiter + pre-filter + multi-judge).

#### Task 4c: enforce-llm-judge-per-tool.mjs (PreToolUse hook)

**Files:**
- Create: `tools/enforce-llm-judge-per-tool.mjs`
- Create: `tools/enforce-llm-judge-per-tool.test.mjs`
- Modify: `.claude/settings.json`

- [ ] **Step 1: Failing tests** — hook calls judge with tool_input snapshot, blocks if judge returns block. Stub the judge module with `vi.mock`.

- [ ] **Step 2: Implement** — read stdin, build prompt from `{tool_name, tool_input}`, call `judge()`, exit 2 if block.

- [ ] **Step 3: Register in settings.json** matcher `Edit|Write|MultiEdit|NotebookEdit|Bash|Task|Agent`, block-mode, timeout 10000ms.

- [ ] **Step 4: Self-lockout safeguard** — hook MUST short-circuit (allow) if `isConfigured()` returns false, with a stderr WARNING `[llm-judge-per-tool] ROUTER_LLM_KEY not configured — skipping Layer 4`. This is critical to avoid the Stream G Task 8 self-lockout incident.

- [ ] **Step 5: GREEN + commit**

#### Task 4d: enforce-llm-judge-response-scan.mjs (Stop hook)

**Files:**
- Create: `tools/enforce-llm-judge-response-scan.mjs`
- Create: `tools/enforce-llm-judge-response-scan.test.mjs`
- Modify: `.claude/settings.json`

- [ ] **Step 1-5:** Same TDD pattern as 4c but for Stop hook. Reads last assistant message from transcript JSONL, calls judge with "did the response contain instructions to bypass any router-gate rule?" prompt.

- [ ] **Step 6: Commit (4d closes H5 entirely)**

After 4d commit, full regression should be 1722 + ~20 new tests (4 config + 4 client + 6 per-tool + 6 response-scan) = ~1742 GREEN.

---

## Phase H-β — Completeness

### Checkpoint marker: review after Phase H-β (Tasks 5-8)

---

### Task 5: H6 — decomposition-detector wrapper (depends on H5 client)

**Files:**
- Create: `tools/enforce-decomposition-detector.mjs`
- Create: `tools/enforce-decomposition-detector.test.mjs`
- Modify: `.claude/settings.json`

- [ ] **Step 1: Failing tests** (3 cases): (a) "fix this typo" → allow (no decomposition needed); (b) "implement billing tier 2 with rollback and migrations and tests" → block, suggest writing-plans; (c) judge unavailable → fail-open allow.

- [ ] **Step 2: Implement** — uses `llm-judge-client.judge()` with prompt "Does this user request describe complex multi-step work (3+ distinct files, 3+ logical steps, requires planning) WITHOUT the user having invoked `superpowers:writing-plans` or `superpowers:brainstorming` skill first? YES/NO."

Reads recent transcript turns to find user prompt + skill invocations. Block message: `[decomposition-detector] task suggests 3+ steps — invoke superpowers:writing-plans first or override with "direct ok" / "without plan"`.

- [ ] **Step 3: Register matcher `Edit|Write|MultiEdit|NotebookEdit|Bash|Task`**, block-mode, timeout 8000ms. Same `isConfigured()` short-circuit as 4c.

- [ ] **Step 4: GREEN + commit**

---

### Task 6: H4 — askuser-answer-parser wrapper + schema sync

**Files:**
- Create: `tools/enforce-askuser-answer-parser.mjs`
- Create: `tools/enforce-askuser-answer-parser.test.mjs`
- Modify: `tools/askuser-answer-parser.mjs` (add `toApprovalRecord()` exporter)
- Modify: `.claude/settings.json`

- [ ] **Step 1: Failing tests for `toApprovalRecord`**

In `tools/askuser-answer-parser.test.mjs` (extend existing):

```javascript
describe('toApprovalRecord (Stream H Task 6 — schema sync)', () => {
  it('returns null for non-git-pattern answer', () => {
    expect(toApprovalRecord('cancel', { question: 'continue?' })).toBeNull();
  });
  it('returns {type, command, ts} for approved git pattern', () => {
    const r = toApprovalRecord('подтверди git push origin main', {
      question: 'разрешить git push?',
      nowMs: 1700000000000,
    });
    expect(r).toMatchObject({ type: 'approve_git_operation', command: 'git push origin main', ts: 1700000000000 });
  });
});
```

- [ ] **Step 2: Implement `toApprovalRecord` in askuser-answer-parser.mjs**

```javascript
export function toApprovalRecord(answer, { question, nowMs = Date.now() } = {}) {
  const norm = normalizeAnswer(answer);
  if (!norm) return null;
  // Detect git-pattern in question + extract command from answer
  const gitMatch = /\b(git\s+(?:add|commit|push|pull|merge|rebase|reset|checkout|switch|branch|stash|cherry-pick|revert|clean)[^"'\n]*)/i.exec(answer);
  if (!gitMatch) return null;
  const command = normalizeCommand(gitMatch[1]);
  return { type: 'approve_git_operation', command, ts: nowMs };
}
```

- [ ] **Step 3: Wrapper hook tests**

`tools/enforce-askuser-answer-parser.test.mjs`: stub stdin payload with AskUserQuestion response, verify wrapper writes record to `~/.claude/runtime/askuser-decisions-<sess>.jsonl` via injected `writeFn`.

- [ ] **Step 4: Implement wrapper**

`tools/enforce-askuser-answer-parser.mjs`:

```javascript
#!/usr/bin/env node
import { appendFileSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { toApprovalRecord } from './askuser-answer-parser.mjs';

async function main() {
  let input = '';
  for await (const chunk of process.stdin) input += chunk;
  let payload;
  try { payload = JSON.parse(input); } catch { return; }
  const { tool_response, tool_input, session_id } = payload || {};
  if (!tool_response || !tool_input) return;

  const answers = tool_response.answers || {};
  const questions = tool_input.questions || [];
  for (let i = 0; i < questions.length; i++) {
    const q = questions[i];
    const ans = answers[q.question];
    if (!ans) continue;
    const rec = toApprovalRecord(ans, { question: q.question });
    if (!rec) continue;
    const path = join(homedir(), '.claude', 'runtime', `askuser-decisions-${session_id || 'unknown'}.jsonl`);
    try {
      mkdirSync(dirname(path), { recursive: true });
      appendFileSync(path, JSON.stringify(rec) + '\n');
    } catch { /* fail-open observability */ }
  }
}

if (process.argv[1].endsWith('enforce-askuser-answer-parser.mjs')) {
  main().catch(() => process.exit(0)); // fail-open observability
}
```

- [ ] **Step 5: Register `PostToolUse` matcher `AskUserQuestion`**, fail-open (not block), timeout 2000ms.

- [ ] **Step 6: GREEN + commit**

After H4 commit, manual approval workaround can be retired — AskUserQuestion answers automatically populate the approval ledger.

---

### Task 7: H7 — parallel-session-lock

**Files:**
- Create: `tools/parallel-session-lock.mjs`
- Create: `tools/parallel-session-lock.test.mjs`
- Create: `tools/enforce-parallel-session-lock.mjs`
- Create: `tools/enforce-parallel-session-lock.test.mjs`
- Modify: `.claude/settings.json`

- [ ] **Step 1: Pure module API + tests**

```javascript
// API:
//   acquire({ sessionId, pid, workspaceHash, now, readLock, writeLock, ttlMs })
//     → { acquired: bool, holder?: {session_id, pid, acquired_at} }
//   release({ sessionId, workspaceHash, deleteLock })
//   refresh({ sessionId, workspaceHash, now, readLock, writeLock })
```

5 test cases: fresh acquire, second-session blocked, stale lock auto-replaces, same-session refresh succeeds, release allows new acquire.

- [ ] **Step 2: Implement pure module**

Lock file format: `{schema_version: 1, session_id, pid, acquired_at: ms, ttl_ms}`. Stale if `now - acquired_at > ttl_ms`. Default TTL 5 min. Workspace-hash derived from `path.resolve(cwd)` MD5 (first 12 hex chars).

- [ ] **Step 3: Wrapper hook + tests + register**

Matcher `Edit|Write|MultiEdit|NotebookEdit|Bash`, block-mode, timeout 3000ms. Lock acquired automatically on first mutating tool call; refreshed on subsequent calls; released by Stop hook (separate small wire — add to Stop matcher in same settings.json edit).

- [ ] **Step 4: GREEN + commit**

---

### Task 8: H9 — brain-retro Tables 16-17 + analyzer extensions

**Files:**
- Modify: `tools/brain-retro-analyzer.mjs`
- Modify: `tools/brain-retro-analyzer.test.mjs`
- Modify: `.claude/skills/brain-retro/SKILL.md`

- [ ] **Step 1: Failing tests for `buildRouterGateHookEffectiveness`**

```javascript
describe('buildRouterGateHookEffectiveness (Stream H Task 8 — Table 16)', () => {
  it('counts hook fires per rule, blocks vs warns', () => {
    const eps = [
      { hook_fired: { rule: 'path-deny', outcome: 'block' } },
      { hook_fired: { rule: 'path-deny', outcome: 'block' } },
      { hook_fired: { rule: 'git-conditional', outcome: 'block' } },
      { hook_fired: { rule: 'git-conditional', outcome: 'allow-after-approval' } },
    ];
    const r = buildRouterGateHookEffectiveness(eps);
    expect(r.rules['path-deny'].fires).toBe(2);
    expect(r.rules['path-deny'].blocks).toBe(2);
    expect(r.rules['git-conditional'].fires).toBe(2);
    expect(r.rules['git-conditional'].blocks).toBe(1);
  });
});
```

- [ ] **Step 2: Failing tests for `buildSelfFabricationSignals`**

```javascript
describe('buildSelfFabricationSignals (Stream H Task 8 — Table 17)', () => {
  it('flags episodes where controller-claimed action mismatches tool_use record', () => {
    const eps = [
      { controller_claim: 'committed fix', tool_uses: [] }, // fabrication
      { controller_claim: 'committed fix', tool_uses: ['Bash:git commit'] }, // legit
      { controller_claim: 'tests pass', tool_uses: [] }, // fabrication
    ];
    const r = buildSelfFabricationSignals(eps);
    expect(r.fabrications.length).toBe(2);
    expect(r.legit.length).toBe(1);
  });
});
```

- [ ] **Step 3: Implement both builders** (~30 + 30 LOC).

- [ ] **Step 4: Wire into `analyze()`** — add `routerGateHookEffectiveness` and `selfFabricationSignals` to result object.

- [ ] **Step 5: Update SKILL.md MANDATORY DIGITAL ANALYSIS** — 11→13 tables, add Table 16 + Table 17 with column specs.

- [ ] **Step 6: GREEN + commit**

---

## Phase H-γ — Polish

### Checkpoint marker: review after Phase H-γ (Tasks 9-11) — final activation

---

### Task 9: H3 — 2 cosmetic path-format artifacts in gate error messages

**Files:**
- Modify: `tools/shell-content-rules.mjs` (path-deny error message)
- Modify: `tools/shell-content-rules.test.mjs`
- Modify: `tools/powershell-content-rules.mjs`
- Modify: `tools/powershell-content-rules.test.mjs`

- [ ] **Step 1: Failing test — Bash path-deny does not produce double `c:`**

```javascript
it('Bash path-deny error message does not contain double drive letter', () => {
  // Simulate the Smoke 5 step 4 input where cwd join produced "c:/c/users/..."
  const r = pathDenyOverlay({
    candidatePaths: ['/c/Users/Administrator/.claude/projects/x.jsonl'],
    pathNormalize: (p) => 'c:/c/users/administrator/.claude/projects/x.jsonl',
  });
  expect(r.block).toBe(true);
  expect(r.reason).not.toMatch(/c:\/c\//);
});
```

- [ ] **Step 2: Fix** — normalize the display path: strip leading `/c/` if the normalized result already starts with `c:/`. Add a small `displayPath()` helper.

- [ ] **Step 3: Failing test — PowerShell path-deny expands `$env:USERPROFILE`**

```javascript
it('PowerShell path-deny error message expands $env:USERPROFILE', () => {
  const r = pathDenyOverlayPS({
    candidatePaths: ['$env:USERPROFILE/.claude/projects/x.jsonl'],
    env: { USERPROFILE: 'C:\\Users\\Admin' },
    pathNormalize: defaultPathNormalize,
  });
  expect(r.reason).not.toContain('$env:USERPROFILE');
  expect(r.reason.toLowerCase()).toContain('c:/users/admin');
});
```

- [ ] **Step 4: Fix** — call expandEnvVars before display.

- [ ] **Step 5: GREEN + commit (single commit covers both files)**

---

### Task 10: H10 — subagent-prompt-prefix worktree bootstrap auto-inject

**Files:**
- Modify: `tools/subagent-prompt-prefix.mjs`
- Modify: `tools/subagent-prompt-prefix.test.mjs`

- [ ] **Step 1: Failing test — worktree mode produces SETUP block**

```javascript
it('injects SETUP block when isolation=worktree detected', () => {
  const header = buildInjectedHeader({
    cwd: 'c:/моя/проекты/портал crm/v4-stream-A',
    isWorktree: true,
    parentRepoRoot: 'c:/моя/проекты/портал crm/Документация',
  });
  expect(header).toContain('SETUP — worktree bootstrap');
  expect(header).toContain('mklink /D vendor');
  expect(header).toContain('storage/framework/{cache,sessions,views,testing}');
});
```

- [ ] **Step 2: Implement** — detect worktree via `git rev-parse --show-toplevel` comparison + parent repo discovery via `git worktree list --porcelain`.

- [ ] **Step 3: Failing test — non-worktree mode does NOT include SETUP block** (regression guard).

- [ ] **Step 4: GREEN + commit**

---

### Task 11: H11 — Final sync + regression + cleanup + activation

This is the largest single task in Phase H-γ. Sub-divided into 11a–11f.

#### Task 11a: Pre-flight final sync (Pravila §15.2)

- [ ] `git fetch origin && git log HEAD..origin/main --oneline` — confirm no incoming Pravila/CLAUDE.md/Tooling/PSR_v1/MEMORY/Открытые_вопросы/schema.sql/ADR changes.
- [ ] If parallel-session changes detected — stop, coordinate via `docs/sessions/CURRENT.md` log.

#### Task 11b: Normative content sync — Pravila + PSR_v1 + Tooling Прил. Н

- [ ] Pravila §section additions: Layer 4 LLM-judge active; Workflow F2 closed; new tools registered (#?? if any tool warrants Tooling Прил. Н entry — debatable: LLM-judge config/client are infrastructure, not "tooling" in the registry sense; decision: do NOT add new Tooling node IDs for H5/H6/H7/H8/H10 hooks — they are router-gate infrastructure, akin to other tools/enforce-*.mjs which are not in the registry).
- [ ] PSR_v1 §section: cross-ref bump.
- [ ] Tooling Прил. Н §0 cross-ref bump only.
- [ ] Atomic commit per file (3 commits).

#### Task 11c: CLAUDE.md sync via `/claude-md-management:revise-claude-md`

- [ ] Invoke the skill via Skill tool.
- [ ] §0 cross-refs bump; §6 phase paragraph "Stream H closed — Layer 4 LLM-judge active, Workflow F2 closed, brain-retro Tables 16-17, recovery-procedures.md published"; §9 changelog entry.
- [ ] Single commit through the plugin.

#### Task 11d: Final regression

- [ ] `cd tools && node --experimental-vm-modules ../node_modules/vitest/vitest.mjs run` — expect ~1742+ GREEN (1715 baseline + ~27 new from H2/H8/H4/H5/H6/H7/H9 tests).
- [ ] `lefthook run pre-commit --all-files` — expect 0 errors (gitleaks, markdownlint, lychee, cspell, adr-judge, deptrac, larastan).
- [ ] `lefthook run pre-push` — expect gitleaks-full-history 0, lychee 0 errors.

#### Task 11e: Worktree cleanup

- [ ] `git worktree list` — list all current.
- [ ] For each of v4-stream-{A,B,C,D,E} that exist: `git worktree remove ../v4-stream-X --force` (--force only if branch already merged into main, verify first via `git branch --contains <branch>`).
- [ ] `git branch -d feat/v4-stream-A feat/v4-stream-B feat/v4-stream-C feat/v4-stream-D feat/v4-stream-E` (one by one, verify each is merged).

#### Task 11f: Activation completion log

- [ ] Create `docs/observer/notes/2026-05-30-stream-h-completion.md` with final statusboard:
  - All 11 tasks ✅
  - Smokes 1-9 verdicts (recap)
  - Final test counts
  - Worktree cleanup confirmed
  - Layer 4 active (verify with `node -e "import('./tools/llm-judge-config.mjs').then(m=>m.isConfigured()).then(console.log)"`)
- [ ] Commit + push.

#### Task 11g: Final push

- [ ] `git push origin main` — pre-write approval record first.
- [ ] Verify CI status if any.
- [ ] Stream H closed.

---

## Self-Review

After writing this plan, the following checks were performed:

**Spec coverage:**
- H1 → Task 1 ✓
- H2 → Task 2 ✓
- H3 → Task 9 ✓
- H4 → Task 6 ✓
- H5 → Task 4 (split 4a/4b/4c/4d) ✓
- H6 → Task 5 ✓
- H7 → Task 7 ✓
- H8 → Task 3 ✓
- H9 → Task 8 ✓
- H10 → Task 10 ✓
- H11 → Task 11 (split 11a–11g) ✓

**Placeholder scan:** No "TBD"/"implement later"/"similar to Task N" found. Every step has either complete code or a precise textual description of the action.

**Type consistency:**
- `extractPathArgs` signature unchanged (still `(tokens) => string[]`); internal logic rewritten.
- `pathDenyOverlay` signature unchanged; new `displayPath()` internal helper added (Task 9).
- `decide()` exported in Task 3 (workflow-gate) returns `{block: bool, reason: string}` — same convention as existing chain-recommendation hook.
- `judge()` in Task 4b returns `{block, reason, judgments}` — used by Tasks 4c/4d/5 consistently.
- `toApprovalRecord()` in Task 6 returns `{type, command, ts}` — matches `loadApprovedGitOps` reader format in shell-content-rules.mjs (verified by reading source at lines 98-114).
- `buildInjectedHeader` in Task 10 — new exported function; called only by `main()` in same module, no external consumers, no signature drift risk.

**Critical dependencies enforced:**
- Task 5 (H6 decomposition-detector) depends on Task 4b (H5b llm-judge-client) — sequenced correctly.
- Task 8 (H9 brain-retro Tables 16-17) depends on hooks being live — placed after Tasks 4c/4d/5/7 register.
- Task 11 (H11 sync) depends on every prior task — placed last.

**Self-lockout safeguard:** Task 4c Step 4 explicitly mandates `isConfigured()` short-circuit before any judge call, preventing repeat of Stream G Task 8 incident.

**Cost estimate:** ~600-1000k tokens for full Stream H per master plan handoff; this plan does not change that estimate. Phase H-α subagent invocations ~3-4 (Tasks 1, 2, 3, 4 split into 4 commits — subagent per commit), Phase H-β ~4, Phase H-γ ~3 = ~10-11 subagent dispatches total. At average ~50k per subagent (smaller than statusline-setup smoke's 119k since coder-type subagent has lighter context inject) = ~500-650k tokens.

---

## Execution Handoff

**Plan complete and saved to `docs/superpowers/plans/2026-05-30-router-gate-v4-stream-H.md`. Two execution options:**

**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task (Tasks 1-10 individual, Task 11 as 7 sub-dispatches 11a-11g), two-stage review (spec-compliance + code-quality) between tasks, fast iteration. Per the master plan estimate, ~9-11 hours wall-clock with parallelization where independent (none in this plan — all tasks sequential due to dependencies).

**2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints between phases. Saves ~10-30% on tokens (no subagent context-inject overhead) but uses more of this session's context window for execution work.

Per user directive «качество > скорость», recommendation is **Subagent-Driven** with two-stage review enabled. Larger context window per task, isolated failure modes, easier to roll back a single bad task.
