# Gap Tools (testgen, security_review, diff_review, plan_critique) 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:** Add the four highest-value gap tools identified by the 2026-07-01 tool-gap analysis: `testgen` (routed test generation), `security_review` (dedicated security audit), `diff_review` (multi-model diff-aware code review), and `plan_critique` (adversarial red-team of an existing plan).

**Architecture:** Each tool is a thin model-routing wrapper following the existing `defineModelTool` pattern. `testgen` and `security_review` are single-model tools gated on OpenRouter. `diff_review` and `plan_critique` are multi-model panel tools (fan-out to 2–3 lab-diverse models, synthesis by Gemini) gated on Gemini like the existing `jury` tool, sharing a new tiny `runPanel()` helper. No tool writes files; the MCP client does that.

**Tech Stack:** TypeScript (ESM, `.js` import suffixes), Zod schemas, FastMCP registration via `src/tools/registry.ts`, Jest (ts-jest ESM preset), golden contract snapshot in `test/golden/__snapshots__/tool-contracts.json`.

## Global Constraints

- Node >= 22.0.0; ESM — all relative imports end in `.js` even in `.ts` files.
- `defineModelTool` requires top-level `parameters` to be a plain `z.object(...)` — NO `.refine()` / `.superRefine()` / `.transform()` at the top level (produces `ZodEffects`, breaks the factory's `ZodObject` constraint — documented at `src/tools/factory/define-model-tool.ts:54-58`). Validate cross-field requirements at runtime inside `execute` and return an error string.
- Every model-facing system prompt ends with `${FORMAT_INSTRUCTION}` (import from `../utils/format-constants.js`).
- Panel tools: strip panelist output with `stripFormatting()` before synthesis; per-panelist `maxTokens` = 8000 (reasoning-heavy models truncate below that — same rationale as `JUROR_MAX_TOKENS` in `src/tools/jury-tool.ts:21-25`).
- Wrap model calls in `withHeartbeat(fn, reportProgress, 10000)` — the 3rd arg is a 10s keepalive INTERVAL, not a timeout (`src/utils/streaming-helper.ts:80-84`).
- New tools append at the END of their gated block in `registry.ts` — never reorder existing pushes (behavior-preservation contract at the top of `src/tools/registry.ts`).
- Adding a tool changes the emitted `tools/list` contract: every task regenerates the golden snapshot with `UPDATE_GOLDEN=1 npm run test:golden`, then verifies the snapshot diff shows ONLY the new tool.
- Profile booleans for ALL four tools: `true` in `code_focus`, `balanced`, `heavy_coding`, `full`; `false` in `minimal`, `research_power`. Each task bumps the tool-count number in the 4 enabled profiles' `description` strings.
- Expected count progression (verify against `npm run build` build-profiles output each task — trust the build output):
  - after Task 1: code_focus 36, balanced 47, heavy_coding 51, full 58 (minimal 13, research_power 35 unchanged)
  - after Task 2: code_focus 37, balanced 48, heavy_coding 52, full 59
  - after Task 3: code_focus 38, balanced 49, heavy_coding 53, full 60
  - after Task 4: code_focus 39, balanced 50, heavy_coding 54, full 61
- The working tree already contains unrelated uncommitted changes. `git add` ONLY the exact files named in each commit step — never `git add -A`.

## File Structure

- Create: `src/tools/testgen-tool.ts` — testgen tool + exported pure prompt builder
- Create: `src/tools/security-review-tool.ts` — security_review tool + prompt builder
- Create: `src/tools/panel.ts` — shared `runPanel()` fan-out helper (used by Tasks 3–4)
- Create: `src/tools/diff-review-tool.ts` — diff_review panel tool + prompt builders
- Create: `src/tools/plan-critique-tool.ts` — plan_critique panel tool + prompt builders
- Create: `test/tools/testgen.test.ts`, `test/tools/security-review.test.ts`, `test/tools/panel.test.ts`, `test/tools/diff-review.test.ts`, `test/tools/plan-critique.test.ts`
- Modify: `src/tools/registry.ts` (two insertion points), `src/tools/provider-catalog.ts` (catalog lists), `src/profiles/types.ts`, all 6 `src/profiles/*.ts`, `tools.config.json`, `README.md` + `CLAUDE.md` (Task 5 only)

---

### Task 1: `testgen` — routed test generation (S)

**Files:**
- Create: `src/tools/testgen-tool.ts`
- Test: `test/tools/testgen.test.ts`
- Modify: `src/tools/registry.ts` (OpenRouter block, after planner tools, ~line 182)
- Modify: `src/tools/provider-catalog.ts` (OpenRouter provider tool list)
- Modify: `src/profiles/types.ts`, `src/profiles/{minimal,code_focus,research_power,balanced,heavy_coding,full}.ts`, `tools.config.json`

**Interfaces:**
- Consumes: `callOpenRouter(messages, model, temperature, maxTokens)` from `./openrouter-tools.js`; `OpenRouterModel.QWEN3_CODER_NEXT`; `readFilesIntoContext(paths)` from `../utils/file-reader.js`.
- Produces: `export const testgenTool` (tool object), `export function buildTestgenPrompt(args): { system: string; user: string }`, `export function getAllTestgenTools(): [typeof testgenTool]`.

- [ ] **Step 1: Write the failing test**

```typescript
// test/tools/testgen.test.ts
import { testgenTool, buildTestgenPrompt } from "../../src/tools/testgen-tool.js";

describe("testgen tool", () => {
  test("contract: name and parameter keys", () => {
    expect(testgenTool.name).toBe("testgen");
    const keys = Object.keys(testgenTool.parameters.shape);
    expect(keys).toEqual(expect.arrayContaining(["code", "files", "framework", "coverage", "existingTests"]));
  });

  test("prompt builder embeds framework, coverage, and code", () => {
    const { system, user } = buildTestgenPrompt({
      code: "export function add(a: number, b: number) { return a + b; }",
      framework: "jest",
      coverage: "edge",
    });
    expect(system).toContain("jest");
    expect(system).toContain("edge");
    expect(user).toContain("export function add");
  });

  test("execute rejects empty input without a network call", async () => {
    const out = await testgenTool.execute(
      { coverage: "all" } as any,
      { log: () => {}, reportProgress: async () => {} } as any,
    );
    expect(String(out)).toMatch(/provide 'code' or 'files'/i);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npm test -- testgen`
Expected: FAIL — `Cannot find module '../../src/tools/testgen-tool.js'`

- [ ] **Step 3: Write the implementation**

```typescript
// src/tools/testgen-tool.ts
/**
 * testgen — routed test generation.
 * Hands test-writing to a coding-specialized model (Qwen3-Coder-Next) instead
 * of reshaping Claude's own prompt (which is what the test_driven/bdd_spec
 * prompt TECHNIQUES do). Returns runnable test code.
 */
import { z } from "zod";
import { defineModelTool } from "./factory/define-model-tool.js";
import { callOpenRouter, OpenRouterModel } from "./openrouter-tools.js";
import { readFilesIntoContext } from "../utils/file-reader.js";
import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
import { withHeartbeat } from "../utils/streaming-helper.js";

export function buildTestgenPrompt(args: {
  code?: string;
  files?: string[];
  framework?: string;
  coverage?: string;
  existingTests?: string;
}): { system: string; user: string } {
  const coverage = args.coverage || "all";
  const system = `You are Qwen3-Coder-Next, an expert test engineer. Generate RUNNABLE tests for the code provided.

PROCESS (in order):
1. Identify the testing framework: ${args.framework || "infer it from the code/imports; state your inference"}.
2. Enumerate edge cases and failure modes FIRST (empty/null, boundaries, invalid types, error paths, concurrency where relevant).
3. Emit complete, runnable test code targeting uncovered branches. Match the conventions of any existing tests provided.

COVERAGE FOCUS: ${coverage} (edge = boundary/failure cases only; happy = main paths; regression = lock current behavior; all = everything).

OUTPUT: a single test file's contents, then a short list of cases deliberately NOT covered and why. ${FORMAT_INSTRUCTION}`;

  const fileContext = args.files?.length
    ? `\n\nSOURCE FILES:\n${readFilesIntoContext(args.files)}`
    : "";
  const existing = args.existingTests
    ? `\n\nEXISTING TESTS (match these conventions):\n${args.existingTests}`
    : "";
  const user = `CODE UNDER TEST:\n${args.code || "(see SOURCE FILES)"}${fileContext}${existing}`;
  return { system, user };
}

export const testgenTool = defineModelTool({
  name: "testgen",
  description:
    "Generate runnable tests with a coding-specialized model (Qwen3-Coder-Next). Enumerates edge cases first, then emits test code. Provide 'code' or 'files'.",
  parameters: z.object({
    code: z.string().optional().describe("The code to generate tests for (or use 'files')"),
    files: z.array(z.string()).optional().describe("File paths to read as code-under-test. Supports line ranges: 'src/foo.ts:100-200'."),
    framework: z.string().optional().describe("Test framework (e.g. jest, vitest, pytest). Omit to infer."),
    coverage: z.enum(["edge", "happy", "regression", "all"]).optional().default("all").describe("Coverage focus"),
    existingTests: z.string().optional().describe("Paste existing tests so generated ones match conventions"),
  }),
  execute: async (args, { reportProgress }: any) => {
    if (!args.code && !args.files?.length) {
      return "Error: provide 'code' or 'files' — there is nothing to generate tests for.";
    }
    const { system, user } = buildTestgenPrompt(args);
    return withHeartbeat(
      () =>
        callOpenRouter(
          [
            { role: "system", content: system },
            { role: "user", content: user },
          ],
          OpenRouterModel.QWEN3_CODER_NEXT,
          0.3,
          12000,
        ),
      reportProgress,
      10000,
    );
  },
});

export function getAllTestgenTools() {
  return [testgenTool] as const;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `npm test -- testgen`
Expected: PASS (3 tests)

- [ ] **Step 5: Register the tool**

In `src/tools/registry.ts`, inside the `if (isOpenRouterAvailable())` block, AFTER the planner tools push (after the line `...([plannerMakerTool, plannerRunnerTool, listPlansTool] as unknown as RegistryTool[]),` and its closing `);`), add:

```typescript
    // testgen — routed test generation (Qwen3-Coder-Next) — gated on OpenRouter.
    const { testgenTool } = await import("./testgen-tool.js");
    tools.push(testgenTool as unknown as RegistryTool);
```

In `src/tools/provider-catalog.ts`: find the OpenRouter provider's tool-name list (it contains `"qwen_coder"`) and append `"testgen"`.

- [ ] **Step 6: Profile schema + profiles + config**

1. `src/profiles/types.ts` — add `testgen: boolean;` to `ToolsConfig` (place beside the other OpenRouter coding tools, e.g. after `qwen_coder`).
2. All 6 profiles — add the key (compiler enforces): `testgen: true,` in `code_focus.ts`, `balanced.ts`, `heavy_coding.ts`, `full.ts`; `testgen: false,` in `minimal.ts`, `research_power.ts`.
3. Bump the count inside the `description` string of the 4 enabled profiles: code_focus "35"→"36", balanced "46"→"47", heavy_coding "50"→"51", full "57"→"58".
4. `tools.config.json` — under `availableTools`, append `"testgen"` to the array that contains `"qwen_coder"`.

- [ ] **Step 7: Build and verify counts**

Run: `npm run build`
Expected: build-profiles output reports `code_focus.json (36 tools)`, `balanced.json (47 tools)`, `heavy_coding.json (51 tools)`, `full.json (58 tools)`, minimal 13 / research_power 35 unchanged. If output differs, fix the profile booleans before proceeding.

- [ ] **Step 8: Regenerate golden contract intentionally**

Run: `UPDATE_GOLDEN=1 npm run test:golden`
Then: `git diff test/golden/__snapshots__/tool-contracts.json`
Expected: the diff adds ONLY the `testgen` entry — no other tool's schema changed.

- [ ] **Step 9: Full test suite**

Run: `npm test`
Expected: all suites PASS.

- [ ] **Step 10: Commit**

```bash
git add src/tools/testgen-tool.ts test/tools/testgen.test.ts src/tools/registry.ts src/tools/provider-catalog.ts src/profiles/types.ts src/profiles/minimal.ts src/profiles/code_focus.ts src/profiles/research_power.ts src/profiles/balanced.ts src/profiles/heavy_coding.ts src/profiles/full.ts profiles/ test/golden/__snapshots__/tool-contracts.json
git commit -m "feat(tools): add testgen — routed test generation via Qwen3-Coder-Next"
```

---

### Task 2: `security_review` — dedicated security audit (S)

**Files:**
- Create: `src/tools/security-review-tool.ts`
- Test: `test/tools/security-review.test.ts`
- Modify: `src/tools/registry.ts` (OpenRouter block, directly after the Task 1 testgen push)
- Modify: `src/tools/provider-catalog.ts`, `src/profiles/types.ts`, all 6 profiles, `tools.config.json`

**Interfaces:**
- Consumes: `callOpenRouter`, `OpenRouterModel.DEEPSEEK_V4_PRO`, `readFilesIntoContext`, `FORMAT_INSTRUCTION`, `withHeartbeat` — same imports as Task 1.
- Produces: `export const securityReviewTool`, `export function buildSecurityReviewPrompt(args): { system: string; user: string }`, `export function getAllSecurityReviewTools()`.

- [ ] **Step 1: Write the failing test**

```typescript
// test/tools/security-review.test.ts
import { securityReviewTool, buildSecurityReviewPrompt } from "../../src/tools/security-review-tool.js";

describe("security_review tool", () => {
  test("contract: name and parameter keys", () => {
    expect(securityReviewTool.name).toBe("security_review");
    const keys = Object.keys(securityReviewTool.parameters.shape);
    expect(keys).toEqual(expect.arrayContaining(["code", "diff", "files", "language", "context", "standard"]));
  });

  test("prompt builder embeds taint-analysis framing, standard, and trust context", () => {
    const { system, user } = buildSecurityReviewPrompt({
      code: "app.get('/u', (req,res) => db.query(`SELECT * FROM users WHERE id=${req.query.id}`))",
      standard: "owasp",
      context: "public internet-facing API",
    });
    expect(system).toMatch(/taint|untrusted input/i);
    expect(system).toContain("OWASP");
    expect(user).toContain("db.query");
    expect(user).toContain("public internet-facing API");
  });

  test("execute rejects empty input without a network call", async () => {
    const out = await securityReviewTool.execute(
      { standard: "both" } as any,
      { log: () => {}, reportProgress: async () => {} } as any,
    );
    expect(String(out)).toMatch(/provide 'code', 'diff', or 'files'/i);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npm test -- security-review`
Expected: FAIL — `Cannot find module '../../src/tools/security-review-tool.js'`

- [ ] **Step 3: Write the implementation**

```typescript
// src/tools/security-review-tool.ts
/**
 * security_review — dedicated security audit for code you own/are authorized
 * to review. Unlike openai_code_review's generic reviewer with a `security`
 * focusArea, this carries a real attacker mental model: taint/data-flow,
 * OWASP/CWE framing, exploit sketch + concrete fix per finding.
 */
import { z } from "zod";
import { defineModelTool } from "./factory/define-model-tool.js";
import { callOpenRouter, OpenRouterModel } from "./openrouter-tools.js";
import { readFilesIntoContext } from "../utils/file-reader.js";
import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
import { withHeartbeat } from "../utils/streaming-helper.js";

export function buildSecurityReviewPrompt(args: {
  code?: string;
  diff?: string;
  files?: string[];
  language?: string;
  context?: string;
  standard?: string;
}): { system: string; user: string } {
  const standard = args.standard || "both";
  const standardLine =
    standard === "owasp"
      ? "Map findings to OWASP Top 10 (2025) categories."
      : standard === "cwe"
        ? "Assign a CWE id to every finding."
        : "Map findings to OWASP Top 10 (2025) categories AND assign a CWE id to every finding.";

  const system = `You are a principal application-security engineer performing an AUTHORIZED defensive review of the owner's own code. Think like an attacker; report like an engineer.

METHOD:
1. TAINT / DATA-FLOW — trace every untrusted input (params, headers, files, env, DB reads of user data) to its sinks (queries, exec, deserialization, file paths, templates, redirects).
2. AUTHN/AUTHZ — missing checks, confused-deputy, IDOR, privilege boundaries.
3. SECRETS & CRYPTO — hardcoded credentials, weak primitives, misused randomness.
4. INJECTION & DESERIALIZATION — SQL/NoSQL/command/template injection, unsafe eval/deserialize.
5. DENIAL & ABUSE — unbounded loops/allocations from user input, missing rate limits (flag only; do not design attacks).

${standardLine}

PER FINDING: [SEVERITY critical|high|medium|low] [CWE/OWASP ref] — location (file:line if derivable), why it's exploitable (1-2 sentence sketch, no weaponized payloads), and the CONCRETE FIX (code-level).
END WITH: a severity-ordered summary table and an overall risk verdict.
If the code is clean in an area you checked, say so explicitly — absence of findings must be an assertion, not an omission. ${FORMAT_INSTRUCTION}`;

  const parts: string[] = [];
  if (args.context) parts.push(`DEPLOYMENT/TRUST CONTEXT: ${args.context}`);
  if (args.language) parts.push(`LANGUAGE/FRAMEWORK: ${args.language}`);
  if (args.diff) parts.push(`DIFF UNDER REVIEW:\n${args.diff}`);
  if (args.code) parts.push(`CODE UNDER REVIEW:\n${args.code}`);
  if (args.files?.length) parts.push(`SOURCE FILES:\n${readFilesIntoContext(args.files)}`);
  return { system, user: parts.join("\n\n") };
}

export const securityReviewTool = defineModelTool({
  name: "security_review",
  description:
    "Dedicated security audit (DeepSeek V4 Pro): taint/data-flow analysis, OWASP/CWE-mapped findings with severity, exploitability sketch, and concrete fixes. For code you are authorized to review. Provide 'code', 'diff', or 'files'.",
  parameters: z.object({
    code: z.string().optional().describe("Code to audit (or use 'diff'/'files')"),
    diff: z.string().optional().describe("Unified diff to audit (scopes the review to the change)"),
    files: z.array(z.string()).optional().describe("File paths to read server-side. Supports line ranges: 'src/foo.ts:100-200'."),
    language: z.string().optional().describe("Language/framework hint (e.g. 'TypeScript/Express')"),
    context: z.string().optional().describe("Trust boundaries & deployment context (e.g. 'internal-only service behind VPN')"),
    standard: z.enum(["owasp", "cwe", "both"]).optional().default("both").describe("Finding-mapping standard"),
  }),
  execute: async (args, { reportProgress }: any) => {
    if (!args.code && !args.diff && !args.files?.length) {
      return "Error: provide 'code', 'diff', or 'files' — there is nothing to audit.";
    }
    const { system, user } = buildSecurityReviewPrompt(args);
    return withHeartbeat(
      () =>
        callOpenRouter(
          [
            { role: "system", content: system },
            { role: "user", content: user },
          ],
          OpenRouterModel.DEEPSEEK_V4_PRO,
          0.2,
          12000,
        ),
      reportProgress,
      10000,
    );
  },
});

export function getAllSecurityReviewTools() {
  return [securityReviewTool] as const;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `npm test -- security-review`
Expected: PASS (3 tests)

- [ ] **Step 5: Register the tool**

In `src/tools/registry.ts`, directly after the Task 1 `testgen` push (still inside `if (isOpenRouterAvailable())`):

```typescript
    // security_review — dedicated security audit (DeepSeek V4 Pro) — gated on OpenRouter.
    const { securityReviewTool } = await import("./security-review-tool.js");
    tools.push(securityReviewTool as unknown as RegistryTool);
```

In `src/tools/provider-catalog.ts`: append `"security_review"` to the same OpenRouter list as Task 1.

- [ ] **Step 6: Profile schema + profiles + config**

Same recipe as Task 1, key `security_review`: `types.ts` + 6 profiles (`true` in code_focus/balanced/heavy_coding/full, `false` in minimal/research_power), description counts code_focus "36"→"37", balanced "47"→"48", heavy_coding "51"→"52", full "58"→"59"; append `"security_review"` to the same `tools.config.json` array.

- [ ] **Step 7: Build and verify counts**

Run: `npm run build`
Expected: code_focus 37, balanced 48, heavy_coding 52, full 59.

- [ ] **Step 8: Regenerate golden contract**

Run: `UPDATE_GOLDEN=1 npm run test:golden` then `git diff test/golden/__snapshots__/tool-contracts.json`
Expected: diff adds ONLY `security_review`.

- [ ] **Step 9: Full test suite**

Run: `npm test`
Expected: all suites PASS.

- [ ] **Step 10: Commit**

```bash
git add src/tools/security-review-tool.ts test/tools/security-review.test.ts src/tools/registry.ts src/tools/provider-catalog.ts src/profiles/types.ts src/profiles/minimal.ts src/profiles/code_focus.ts src/profiles/research_power.ts src/profiles/balanced.ts src/profiles/heavy_coding.ts src/profiles/full.ts profiles/ test/golden/__snapshots__/tool-contracts.json
git commit -m "feat(tools): add security_review — OWASP/CWE security audit via DeepSeek V4 Pro"
```

---

### Task 3: `runPanel()` helper + `diff_review` — multi-model diff review (M)

**Files:**
- Create: `src/tools/panel.ts`
- Create: `src/tools/diff-review-tool.ts`
- Test: `test/tools/panel.test.ts`, `test/tools/diff-review.test.ts`
- Modify: `src/tools/registry.ts` (Gemini block, after the `juryTool` push, ~line 129)
- Modify: `src/tools/provider-catalog.ts`, `src/profiles/types.ts`, all 6 profiles, `tools.config.json`

**Interfaces:**
- Consumes: `callOpenRouter` + `OpenRouterModel.{KIMI_K2_7_CODE,DEEPSEEK_V4_PRO}`; `callOpenAI(messages, model?, temperature?, maxTokens?, reasoningEffort?)` from `./openai-tools.js`; `callGemini(prompt, model?, systemPrompt?, temperature?)` from `./gemini-tools.js`; `hasOpenAIApiKey`, `hasOpenRouterApiKey` from `../utils/api-keys.js`; `stripFormatting` from `../utils/format-stripper.js`.
- Produces: `export interface Panelist { key: string; label: string; call: (q: string) => Promise<string> }`; `export async function runPanel(panelists: Panelist[], prompt: string): Promise<{ label: string; text: string }[]>` (drops throwing panelists); `export const diffReviewTool`; `export function buildDiffReviewerPrompt(args): string`; `export function buildDiffJudgePrompt(perspectives, args): string`.

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

```typescript
// test/tools/panel.test.ts
import { runPanel, type Panelist } from "../../src/tools/panel.js";

describe("runPanel", () => {
  test("collects successful panelists and drops throwing ones", async () => {
    const panel: Panelist[] = [
      { key: "a", label: "A", call: async () => "alpha says yes" },
      { key: "b", label: "B", call: async () => { throw new Error("offline"); } },
      { key: "c", label: "C", call: async () => "gamma says no" },
    ];
    const out = await runPanel(panel, "question");
    expect(out.map((r) => r.label)).toEqual(["A", "C"]);
    expect(out[0].text).toContain("alpha");
  });

  test("passes the same prompt to every panelist", async () => {
    const seen: string[] = [];
    const panel: Panelist[] = [
      { key: "a", label: "A", call: async (q) => { seen.push(q); return "x"; } },
      { key: "b", label: "B", call: async (q) => { seen.push(q); return "y"; } },
    ];
    await runPanel(panel, "the-prompt");
    expect(seen).toEqual(["the-prompt", "the-prompt"]);
  });
});
```

```typescript
// test/tools/diff-review.test.ts
import { diffReviewTool, buildDiffReviewerPrompt, buildDiffJudgePrompt } from "../../src/tools/diff-review-tool.js";

const SAMPLE_DIFF = `--- a/src/pay.ts
+++ b/src/pay.ts
@@ -10,3 +10,3 @@
-  const total = items.reduce((s, i) => s + i.price, 0);
+  const total = items.reduce((s, i) => s + i.price * i.qty, 0);`;

describe("diff_review tool", () => {
  test("contract: name and parameter keys", () => {
    expect(diffReviewTool.name).toBe("diff_review");
    const keys = Object.keys(diffReviewTool.parameters.shape);
    expect(keys).toEqual(expect.arrayContaining(["diff", "intent", "files", "focus", "severityFloor"]));
  });

  test("reviewer prompt scopes to the diff and carries intent + focus", () => {
    const p = buildDiffReviewerPrompt({ diff: SAMPLE_DIFF, intent: "charge quantity, not unit price", focus: "correctness" });
    expect(p).toMatch(/changed .*lines|only .*changed/i);
    expect(p).toContain("charge quantity");
    expect(p).toContain(SAMPLE_DIFF);
    expect(p).toContain("correctness");
  });

  test("judge prompt includes every perspective and the severity floor", () => {
    const p = buildDiffJudgePrompt(
      [{ label: "Kimi (SWE)", text: "off-by-one in reduce" }, { label: "DeepSeek", text: "missing null check" }],
      { diff: SAMPLE_DIFF, severityFloor: "major" },
    );
    expect(p).toContain("Kimi (SWE)");
    expect(p).toContain("missing null check");
    expect(p).toContain("major");
  });

  test("execute rejects a missing diff without a network call", async () => {
    const out = await diffReviewTool.execute(
      { focus: "all", severityFloor: "nit" } as any,
      { log: () => {}, reportProgress: async () => {} } as any,
    );
    expect(String(out)).toMatch(/'diff' is required/i);
  });
});
```

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

Run: `npm test -- panel && npm test -- diff-review`
Expected: both FAIL — modules not found.

- [ ] **Step 3: Write `src/tools/panel.ts`**

```typescript
// src/tools/panel.ts
/**
 * Tiny fan-out helper for multi-model panel tools (diff_review, plan_critique).
 * Same resilience contract as the jury: a panelist whose call throws (missing
 * key, provider outage) is DROPPED — its error text must never leak into
 * synthesis. Output is stripped so the judge sees plain prose.
 */
import { stripFormatting } from "../utils/format-stripper.js";

export interface Panelist {
  key: string;
  label: string;
  call: (q: string) => Promise<string>;
}

export async function runPanel(
  panelists: Panelist[],
  prompt: string,
): Promise<{ label: string; text: string }[]> {
  const settled = await Promise.all(
    panelists.map(async (p) => {
      try {
        return { label: p.label, text: stripFormatting(await p.call(prompt)) };
      } catch {
        return null;
      }
    }),
  );
  return settled.filter((r): r is { label: string; text: string } => r !== null);
}
```

- [ ] **Step 4: Run the panel test**

Run: `npm test -- panel`
Expected: PASS (2 tests)

- [ ] **Step 5: Write `src/tools/diff-review-tool.ts`**

```typescript
// src/tools/diff-review-tool.ts
/**
 * diff_review — multi-model, diff-AWARE code review.
 * Differs from openai_code_review (whole-file, single model) and jury (free-
 * text, no code structure): reviewers are scoped to the changed lines, then a
 * Gemini judge dedupes and severity-ranks into ONE actionable list.
 * Gated on Gemini (judge); panelists self-drop when their key is missing.
 */
import { z } from "zod";
import { defineModelTool } from "./factory/define-model-tool.js";
import { callOpenRouter, OpenRouterModel } from "./openrouter-tools.js";
import { callOpenAI } from "./openai-tools.js";
import { callGemini } from "./gemini-tools.js";
import { hasOpenAIApiKey, hasOpenRouterApiKey } from "../utils/api-keys.js";
import { readFilesIntoContext } from "../utils/file-reader.js";
import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
import { withHeartbeat } from "../utils/streaming-helper.js";
import { runPanel, type Panelist } from "./panel.js";

const PANELIST_MAX_TOKENS = 8000; // same rationale as JUROR_MAX_TOKENS (jury-tool.ts)

export function buildDiffReviewerPrompt(args: {
  diff: string;
  intent?: string;
  files?: string[];
  focus?: string;
}): string {
  const focus = args.focus || "all";
  const fileContext = args.files?.length
    ? `\n\nSURROUNDING CODE (context only — do NOT review unchanged code):\n${readFilesIntoContext(args.files)}`
    : "";
  return `Review this diff. Flag issues ONLY on changed lines and lines directly adjacent to/affected by the change — do not review the rest of the file.

${args.intent ? `STATED INTENT OF THE CHANGE: ${args.intent}\nAlso flag any way the diff does NOT accomplish this intent.\n` : ""}FOCUS: ${focus} (security | perf | correctness | style | all).

FOR EACH ISSUE: severity (blocker | major | minor | nit), file:line from the diff hunks, what breaks and the concrete input/state that triggers it, suggested fix (one line).
Look specifically for: regressions the change introduces, missed edge cases in the new logic, security implications of new data flows, and behavior the intent implies but the diff doesn't implement.
If you find nothing at a severity, say so explicitly.

DIFF:
${args.diff}${fileContext}`;
}

export function buildDiffJudgePrompt(
  perspectives: { label: string; text: string }[],
  args: { diff: string; severityFloor?: string },
): string {
  const floor = args.severityFloor || "nit";
  const body = perspectives
    .map((p, i) => `=== REVIEWER ${i + 1}: ${p.label} ===\n${p.text}`)
    .join("\n\n");
  return `You are the presiding reviewer. Below are independent reviews of the SAME diff.

MERGE THEM INTO ONE LIST:
1. Deduplicate findings that describe the same underlying issue (keep the clearest wording; note "flagged by N/${perspectives.length} reviewers").
2. Discard findings that misread the diff (verify each against the DIFF below).
3. Rank by severity: blocker > major > minor > nit. OMIT everything below severity floor: ${floor}.
4. Every finding keeps its file:line anchor and one-line fix.

END WITH: verdict line — "MERGEABLE", "MERGEABLE WITH FIXES", or "DO NOT MERGE", plus the single most important fix.

DIFF:
${args.diff}

${body}`;
}

function buildPanel(): Panelist[] {
  const panel: Panelist[] = [];
  if (hasOpenRouterApiKey()) {
    panel.push({
      key: "kimi",
      label: "Kimi K2.7-Code (SWE regressions)",
      call: (q) =>
        callOpenRouter(
          [
            { role: "system", content: `You are Kimi K2.7-Code, an SWE-specialized reviewer. Hunt regressions and missed edge cases in diffs. ${FORMAT_INSTRUCTION}` },
            { role: "user", content: q },
          ],
          OpenRouterModel.KIMI_K2_7_CODE,
          0.3,
          PANELIST_MAX_TOKENS,
        ),
    });
    panel.push({
      key: "deepseek",
      label: "DeepSeek V4 Pro (correctness & security)",
      call: (q) =>
        callOpenRouter(
          [
            { role: "system", content: `You are DeepSeek V4 Pro reviewing a diff. Rigorously verify correctness of the new logic and security of new data flows. ${FORMAT_INSTRUCTION}` },
            { role: "user", content: q },
          ],
          OpenRouterModel.DEEPSEEK_V4_PRO,
          0.2,
          PANELIST_MAX_TOKENS,
        ),
    });
  }
  if (hasOpenAIApiKey()) {
    panel.push({
      key: "gpt",
      label: "GPT-5.5 (intent & API-contract)",
      call: (q) =>
        callOpenAI(
          [
            { role: "system", content: `You review diffs for intent mismatches and API-contract breaks (types, error paths, backward compatibility). ${FORMAT_INSTRUCTION}` },
            { role: "user", content: q },
          ],
          undefined,
          0.3,
          PANELIST_MAX_TOKENS,
          "high",
        ),
    });
  }
  return panel;
}

export const diffReviewTool = defineModelTool({
  name: "diff_review",
  description:
    "Multi-model diff-aware code review: 2-3 lab-diverse reviewers (Kimi K2.7-Code, DeepSeek V4 Pro, GPT-5.5) scoped to the changed lines, deduplicated and severity-ranked by a Gemini judge. Provide the unified diff in 'diff'.",
  parameters: z.object({
    diff: z.string().describe("Unified diff to review (git diff output) — REQUIRED"),
    intent: z.string().optional().describe("What the change is SUPPOSED to do (enables intent-mismatch detection)"),
    files: z.array(z.string()).optional().describe("File paths for surrounding context. Supports line ranges: 'src/foo.ts:100-200'."),
    focus: z.enum(["security", "perf", "correctness", "style", "all"]).optional().default("all").describe("Review focus"),
    severityFloor: z.enum(["blocker", "major", "minor", "nit"]).optional().default("nit").describe("Omit findings below this severity"),
  }),
  execute: async (args, { reportProgress }: any) => {
    if (!args.diff?.trim()) {
      return "Error: 'diff' is required — paste the unified diff (e.g. `git diff` output).";
    }
    const panel = buildPanel();
    if (panel.length === 0) {
      return "Error: no reviewers available — diff_review needs OPENROUTER_API_KEY and/or OPENAI_API_KEY in addition to the Gemini key. Run `doctor` for setup status.";
    }
    const reviewerPrompt = buildDiffReviewerPrompt(args);
    const perspectives = await withHeartbeat(
      () => runPanel(panel, reviewerPrompt),
      reportProgress,
      10000,
    );
    if (perspectives.length === 0) {
      return "Error: all reviewers failed (provider outage or quota). Try again or run `doctor`.";
    }
    const judgePrompt = buildDiffJudgePrompt(perspectives, args);
    const verdict = await withHeartbeat(
      () =>
        callGemini(
          judgePrompt,
          undefined,
          `You are Gemini 3 Pro, the presiding code reviewer synthesizing a panel review of one diff. Be decisive; keep only verified findings. ${FORMAT_INSTRUCTION}`,
          0.3,
        ),
      reportProgress,
      10000,
    );
    const roster = perspectives.map((p) => p.label).join(", ");
    return `DIFF REVIEW (${perspectives.length} reviewers: ${roster})\n\n${verdict}`;
  },
});

export function getAllDiffReviewTools() {
  return [diffReviewTool] as const;
}
```

- [ ] **Step 6: Run the diff-review tests**

Run: `npm test -- diff-review`
Expected: PASS (4 tests)

- [ ] **Step 7: Register the tool**

In `src/tools/registry.ts`, inside `if (isGeminiAvailable())`, AFTER the `tools.push(juryTool as unknown as RegistryTool);` line, add:

```typescript
    // diff_review — multi-model diff review with Gemini judge — gated on Gemini
    // (panelists self-drop when OpenRouter/OpenAI keys are missing).
    const { diffReviewTool } = await import("./diff-review-tool.js");
    tools.push(diffReviewTool as unknown as RegistryTool);
```

In `src/tools/provider-catalog.ts`: append `"diff_review"` to the Gemini provider's tool list (the one containing `"jury"` or `"gemini_analyze_text"`).

- [ ] **Step 8: Profile schema + profiles + config**

Same recipe, key `diff_review`: `true` in code_focus/balanced/heavy_coding/full, `false` in minimal/research_power; counts code_focus "37"→"38", balanced "48"→"49", heavy_coding "52"→"53", full "59"→"60"; append `"diff_review"` to the `tools.config.json` array that contains `"jury"` (or the Gemini group).

- [ ] **Step 9: Build, golden, full suite**

Run: `npm run build` — expect code_focus 38, balanced 49, heavy_coding 53, full 60.
Run: `UPDATE_GOLDEN=1 npm run test:golden` then `git diff test/golden/__snapshots__/tool-contracts.json` — diff adds ONLY `diff_review`.
Run: `npm test` — all suites PASS.

- [ ] **Step 10: Commit**

```bash
git add src/tools/panel.ts src/tools/diff-review-tool.ts test/tools/panel.test.ts test/tools/diff-review.test.ts src/tools/registry.ts src/tools/provider-catalog.ts src/profiles/types.ts src/profiles/minimal.ts src/profiles/code_focus.ts src/profiles/research_power.ts src/profiles/balanced.ts src/profiles/heavy_coding.ts src/profiles/full.ts profiles/ test/golden/__snapshots__/tool-contracts.json
git commit -m "feat(tools): add diff_review — multi-model diff-aware review with Gemini judge"
```

---

### Task 4: `plan_critique` — adversarial plan red-team (M)

**Files:**
- Create: `src/tools/plan-critique-tool.ts`
- Test: `test/tools/plan-critique.test.ts`
- Modify: `src/tools/registry.ts` (Gemini block, directly after the Task 3 diff_review push)
- Modify: `src/tools/provider-catalog.ts`, `src/profiles/types.ts`, all 6 profiles, `tools.config.json`

**Interfaces:**
- Consumes: `runPanel`/`Panelist` from `./panel.js` (Task 3); `callOpenRouter` + `OpenRouterModel.DEEPSEEK_V4_PRO`; `callGrok(messages, model?, temperature?, maxTokens?)` from `./grok-tools.js`; `callOpenAI`; `callGemini`; `hasGrokApiKey`, `hasOpenAIApiKey`, `hasOpenRouterApiKey` from `../utils/api-keys.js`.
- Produces: `export const planCritiqueTool`, `export function buildPlanCritiquePrompt(args): string`, `export function buildPlanCritiqueJudgePrompt(perspectives, args): string`.

- [ ] **Step 1: Write the failing test**

```typescript
// test/tools/plan-critique.test.ts
import { planCritiqueTool, buildPlanCritiquePrompt, buildPlanCritiqueJudgePrompt } from "../../src/tools/plan-critique-tool.js";

const SAMPLE_PLAN = "1. Add OAuth login\n2. Migrate users table\n3. Ship to prod Friday";

describe("plan_critique tool", () => {
  test("contract: name and parameter keys", () => {
    expect(planCritiqueTool.name).toBe("plan_critique");
    const keys = Object.keys(planCritiqueTool.parameters.shape);
    expect(keys).toEqual(expect.arrayContaining(["plan", "goal", "constraints", "files"]));
  });

  test("critique prompt carries pre-mortem framing, the plan, and the goal", () => {
    const p = buildPlanCritiquePrompt({ plan: SAMPLE_PLAN, goal: "secure login without downtime" });
    expect(p).toMatch(/failed|failure/i);          // pre-mortem framing
    expect(p).toMatch(/assumption/i);              // hidden-assumption audit
    expect(p).toContain("Migrate users table");
    expect(p).toContain("secure login without downtime");
  });

  test("judge prompt includes every critic and demands ranked risks", () => {
    const p = buildPlanCritiqueJudgePrompt(
      [{ label: "DeepSeek", text: "step 2 has no rollback" }, { label: "Grok", text: "Friday deploy risk" }],
      { plan: SAMPLE_PLAN },
    );
    expect(p).toContain("no rollback");
    expect(p).toMatch(/rank/i);
  });

  test("execute rejects a missing plan without a network call", async () => {
    const out = await planCritiqueTool.execute(
      {} as any,
      { log: () => {}, reportProgress: async () => {} } as any,
    );
    expect(String(out)).toMatch(/'plan' is required/i);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npm test -- plan-critique`
Expected: FAIL — module not found.

- [ ] **Step 3: Write the implementation**

```typescript
// src/tools/plan-critique-tool.ts
/**
 * plan_critique — adversarial red-team of an EXISTING plan (any source:
 * hand-written, planner_maker output, a design doc). planner_maker BUILDS
 * plans and planner_runner EXECUTES them; nothing critiques a plan you
 * already hold. Panel of diverse critics + Gemini synthesis, pre-mortem first.
 * Gated on Gemini (judge); critics self-drop when their key is missing.
 */
import { z } from "zod";
import { defineModelTool } from "./factory/define-model-tool.js";
import { callOpenRouter, OpenRouterModel } from "./openrouter-tools.js";
import { callGrok } from "./grok-tools.js";
import { callOpenAI } from "./openai-tools.js";
import { callGemini } from "./gemini-tools.js";
import { OPENAI_MODELS } from "../config/model-constants.js";
import { hasGrokApiKey, hasOpenAIApiKey, hasOpenRouterApiKey } from "../utils/api-keys.js";
import { readFilesIntoContext } from "../utils/file-reader.js";
import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
import { withHeartbeat } from "../utils/streaming-helper.js";
import { runPanel, type Panelist } from "./panel.js";

const PANELIST_MAX_TOKENS = 8000;

export function buildPlanCritiquePrompt(args: {
  plan: string;
  goal?: string;
  constraints?: string;
  files?: string[];
}): string {
  const fileContext = args.files?.length
    ? `\n\nRELEVANT CODE/DOCS:\n${readFilesIntoContext(args.files)}`
    : "";
  return `Red-team this plan. Assume it was executed and FAILED — work backwards.

1. PRE-MORTEM — the 5 most plausible ways this plan failed, most likely first, each with the step that caused it.
2. HIDDEN ASSUMPTIONS — every unstated assumption the plan depends on (environment, data, ordering, people); mark which are UNVERIFIED.
3. STRUCTURE — missing steps, mis-ordered steps, steps with no acceptance criterion or no rollback.
4. RISKS RANKED — likelihood x impact, each with a concrete mitigation that could be added to the plan.
Do NOT rewrite the plan; critique it. Be specific to THIS plan — no generic project-management advice.

${args.goal ? `STATED GOAL: ${args.goal}\nAlso flag anything in the plan that does not serve this goal (scope creep) and any goal aspect no step covers (gap).\n` : ""}${args.constraints ? `CONSTRAINTS: ${args.constraints}\n` : ""}
PLAN UNDER REVIEW:
${args.plan}${fileContext}`;
}

export function buildPlanCritiqueJudgePrompt(
  perspectives: { label: string; text: string }[],
  args: { plan: string },
): string {
  const body = perspectives
    .map((p, i) => `=== CRITIC ${i + 1}: ${p.label} ===\n${p.text}`)
    .join("\n\n");
  return `You are synthesizing independent red-team critiques of the SAME plan into one actionable review.

1. Merge duplicate concerns (note "raised by N/${perspectives.length} critics" — convergence signals real risk).
2. Discard critiques that misread the plan (verify against the PLAN below).
3. Output: (a) TOP RISKS ranked by likelihood x impact with mitigations; (b) UNVERIFIED ASSUMPTIONS to check before starting; (c) CONCRETE PLAN EDITS — numbered, minimal, each tied to a risk; (d) VERDICT — "SOUND", "SOUND WITH EDITS", or "RETHINK", one sentence why.

PLAN:
${args.plan}

${body}`;
}

function buildCriticPanel(): Panelist[] {
  const panel: Panelist[] = [];
  if (hasOpenRouterApiKey()) {
    panel.push({
      key: "deepseek",
      label: "DeepSeek V4 Pro (logical soundness)",
      call: (q) =>
        callOpenRouter(
          [
            { role: "system", content: `You are DeepSeek V4 Pro red-teaming a plan. Attack its logical soundness: ordering, dependencies, unstated preconditions. ${FORMAT_INSTRUCTION}` },
            { role: "user", content: q },
          ],
          OpenRouterModel.DEEPSEEK_V4_PRO,
          0.3,
          PANELIST_MAX_TOKENS,
        ),
    });
  }
  if (hasGrokApiKey()) {
    panel.push({
      key: "grok",
      label: "Grok (operational reality)",
      call: (q) =>
        callGrok(
          [
            { role: "system", content: `You are a pragmatic staff engineer red-teaming a plan. Attack its operational reality: deploy risk, rollback, timing, human factors. Be blunt. ${FORMAT_INSTRUCTION}` },
            { role: "user", content: q },
          ],
          undefined,
          0.5,
          PANELIST_MAX_TOKENS,
        ),
    });
  }
  if (hasOpenAIApiKey()) {
    panel.push({
      key: "gpt",
      label: "GPT-5.5 (edge cases & scope)",
      call: (q) =>
        callOpenAI(
          [
            { role: "system", content: `You red-team plans for edge cases, scope gaps, and missing acceptance criteria. ${FORMAT_INSTRUCTION}` },
            { role: "user", content: q },
          ],
          OPENAI_MODELS.DEFAULT, // explicit: `undefined` falls back to INSTANT (gpt-5.4-mini), contradicting the GPT-5.5 label
          0.4,
          PANELIST_MAX_TOKENS,
          "high",
        ),
    });
  }
  return panel;
}

export const planCritiqueTool = defineModelTool({
  name: "plan_critique",
  description:
    "Adversarial red-team of an existing plan (from any source): multi-model pre-mortem, hidden-assumption audit, ranked risks with mitigations, concrete plan edits, verdict. Complements planner_maker (builds) and planner_runner (executes).",
  parameters: z.object({
    plan: z.string().describe("The plan to critique (paste it) — REQUIRED"),
    goal: z.string().optional().describe("The goal the plan is supposed to achieve (enables scope-creep and gap detection)"),
    constraints: z.string().optional().describe("Hard constraints (deadline, budget, compliance, team size)"),
    files: z.array(z.string()).optional().describe("Relevant code/doc paths for grounding. Supports line ranges: 'src/foo.ts:100-200'."),
  }),
  execute: async (args, { reportProgress }: any) => {
    if (!args.plan?.trim()) {
      return "Error: 'plan' is required — paste the plan you want red-teamed.";
    }
    const panel = buildCriticPanel();
    if (panel.length === 0) {
      return "Error: no critics available — plan_critique needs OPENROUTER_API_KEY, GROK_API_KEY, or OPENAI_API_KEY in addition to the Gemini key. Run `doctor` for setup status.";
    }
    const criticPrompt = buildPlanCritiquePrompt(args);
    const perspectives = await withHeartbeat(
      () => runPanel(panel, criticPrompt),
      reportProgress,
      10000,
    );
    if (perspectives.length === 0) {
      return "Error: all critics failed (provider outage or quota). Try again or run `doctor`.";
    }
    const judgePrompt = buildPlanCritiqueJudgePrompt(perspectives, args);
    const verdict = await withHeartbeat(
      () =>
        callGemini(
          judgePrompt,
          undefined,
          `You are Gemini 3 Pro, synthesizing a red-team panel's critiques of one plan. Convergent concerns are signal; be decisive. ${FORMAT_INSTRUCTION}`,
          0.3,
        ),
      reportProgress,
      10000,
    );
    const roster = perspectives.map((p) => p.label).join(", ");
    return `PLAN CRITIQUE (${perspectives.length} critics: ${roster})\n\n${verdict}`;
  },
});

export function getAllPlanCritiqueTools() {
  return [planCritiqueTool] as const;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `npm test -- plan-critique`
Expected: PASS (4 tests)

- [ ] **Step 5: Register the tool**

In `src/tools/registry.ts`, directly after the Task 3 `diff_review` push (still inside `if (isGeminiAvailable())`):

```typescript
    // plan_critique — adversarial plan red-team with Gemini judge — gated on Gemini.
    const { planCritiqueTool } = await import("./plan-critique-tool.js");
    tools.push(planCritiqueTool as unknown as RegistryTool);
```

In `src/tools/provider-catalog.ts`: append `"plan_critique"` to the same Gemini list as Task 3.

- [ ] **Step 6: Profile schema + profiles + config**

Same recipe, key `plan_critique`: `true` in code_focus/balanced/heavy_coding/full, `false` in minimal/research_power; counts code_focus "38"→"39", balanced "49"→"50", heavy_coding "53"→"54", full "60"→"61"; append `"plan_critique"` to the same `tools.config.json` array as `diff_review`.

- [ ] **Step 7: Build, golden, full suite**

Run: `npm run build` — expect code_focus 39, balanced 50, heavy_coding 54, full 61.
Run: `UPDATE_GOLDEN=1 npm run test:golden` then `git diff test/golden/__snapshots__/tool-contracts.json` — diff adds ONLY `plan_critique`.
Run: `npm test` — all suites PASS.

- [ ] **Step 8: Commit**

```bash
git add src/tools/plan-critique-tool.ts test/tools/plan-critique.test.ts src/tools/registry.ts src/tools/provider-catalog.ts src/profiles/types.ts src/profiles/minimal.ts src/profiles/code_focus.ts src/profiles/research_power.ts src/profiles/balanced.ts src/profiles/heavy_coding.ts src/profiles/full.ts profiles/ test/golden/__snapshots__/tool-contracts.json
git commit -m "feat(tools): add plan_critique — adversarial multi-model plan red-team"
```

---

### Task 5: Docs & counts settlement

**Files:**
- Modify: `README.md` (badge line 8, headline line 13, "57 AI Tools" feature line ~99, anchor texts referencing "57", profile table lines ~112-119)
- Modify: `CLAUDE.md` (profile-counts line ~96, "Adding New Tools" stays valid)

**Interfaces:**
- Consumes: final build-profiles output from Task 4 (minimal 13, research_power 35, code_focus 39, balanced 50, heavy_coding 54, full 61).
- Produces: consistent public counts. NOTE: the tachibot.com landing repo (`../landing/tachibot-landing`) also claims "57 tools" — flag it in the commit message; it is updated separately, ideally together with the next npm release so the site never claims tools users can't install yet.

- [ ] **Step 1: Update README.md**

Replace every count that the Task-4 build output invalidates:
- Badge: `tools-57_active` → `tools-61_active`; anchor `#-tool-ecosystem-57-tools` → match the (renamed) section heading.
- Headline: `**57 AI tools. 12 providers. One protocol.**` → `**61 AI tools. 12 providers. One protocol.**`
- Features: `**57 AI Tools** across 12 providers` → `**61 AI Tools** across 12 providers`
- Profile table rows: Minimal 13, Research Power 35, Code Focus 39, Balanced 50, Heavy Coding 54, Full (default) 61.
- Add the four tools to the tool-ecosystem section in their provider groups (testgen + security_review under OpenRouter-gated tools; diff_review + plan_critique beside jury).

- [ ] **Step 2: Update CLAUDE.md**

Counts line → `minimal (13), code_focus (39), research_power (35), balanced (50), heavy_coding (54), full (61 — every tool)`. Add one line to the tool table/notes: `diff_review`/`plan_critique` are Gemini-gated panel tools reusing the jury pattern; `testgen`/`security_review` are OpenRouter-gated single-model tools.

- [ ] **Step 3: Verify no stale counts remain**

Run: `grep -rn "57" README.md CLAUDE.md | grep -vi "1957\|2057"`
Expected: no tool-count references to 57 remain (model names/dates are fine).

- [ ] **Step 4: Full suite one last time**

Run: `npm run build && npm test`
Expected: build clean, all suites PASS.

- [ ] **Step 5: Commit**

```bash
git add README.md CLAUDE.md
git commit -m "docs: settle tool counts at 61 after gap tools (landing repo 57-claims to update with next release)"
```

---

### Task 6: `/review` and `/redteam` skills

**Files:**
- Create: `skills/review/SKILL.md`
- Create: `skills/redteam/SKILL.md`

**Interfaces:**
- Consumes: the `diff_review` (Task 3) and `plan_critique` (Task 4) MCP tools with the parameter schemas defined there.
- Produces: two Claude Code skills. `scripts/install-skills.sh` derives its list from the `skills/` directory (fixed 2026-07-01), so no script change is needed — the count just becomes 14.

- [ ] **Step 1: Write `skills/review/SKILL.md`**

Look at an existing bundled skill first (e.g. `skills/algo/SKILL.md`) and match its frontmatter format exactly (name/description keys). Content:

```markdown
---
name: review
description: Multi-model diff review — panel of lab-diverse reviewers (Kimi K2.7-Code, DeepSeek V4 Pro, GPT-5.5) scoped to your changed lines, deduplicated and severity-ranked by a Gemini judge
---

# /review — Multi-Model Diff Review

Review the user's current change with the `diff_review` tool.

## Steps

1. Collect the diff. If the user gave one, use it. Otherwise run `git diff` (unstaged), falling back to `git diff HEAD` (all uncommitted) — if both are empty, use `git diff main...HEAD` (branch changes). If still empty, tell the user there is nothing to review and stop.
2. Infer the intent: from the user's words, or from the branch name and recent commit messages (`git log --oneline -5`).
3. Call `diff_review` with: `diff` (the collected diff), `intent`, `focus` if the user named one (security | perf | correctness | style), and `files` for any file the diff touches heavily (use `path:start-end` ranges around the hunks).
4. Relay the verdict. Lead with the MERGEABLE / MERGEABLE WITH FIXES / DO NOT MERGE line, then the findings at severity `major` and above; mention how many minor/nit findings were omitted.
5. Offer to apply the top fix.

## Requirements

Needs GEMINI (judge) plus OPENROUTER and/or OPENAI keys (reviewers self-drop). If the tool returns a "no reviewers available" error, show the user its message — it names the missing keys.
```

- [ ] **Step 2: Write `skills/redteam/SKILL.md`**

```markdown
---
name: redteam
description: Adversarial plan red-team — multi-model pre-mortem, hidden-assumption audit, ranked risks with mitigations, and concrete edits for any plan you paste or point at
---

# /redteam — Adversarial Plan Critique

Red-team a plan with the `plan_critique` tool.

## Steps

1. Collect the plan. If the user pasted it, use it verbatim. If they pointed at a file (a plan doc, an issue, a PLAN.md), read it and use its contents. If neither, ask for the plan and stop.
2. Extract the goal and constraints from the user's message or the plan's own header; pass them as `goal` and `constraints` when present.
3. Call `plan_critique` with `plan`, plus `goal`/`constraints`/`files` when available (`files` = code the plan touches, for grounding).
4. Relay the verdict. Lead with SOUND / SOUND WITH EDITS / RETHINK, then the top risks with mitigations and the numbered plan edits. Note which concerns multiple critics converged on — convergence is signal.
5. Offer to apply the plan edits to the plan document.

## Requirements

Needs GEMINI (judge) plus OPENROUTER, GROK, or OPENAI keys (critics self-drop). If the tool returns a "no critics available" error, show the user its message.
```

- [ ] **Step 3: Verify the install script picks both up**

Run: `bash -n scripts/install-skills.sh && HOME=$(mktemp -d) bash scripts/install-skills.sh | grep -E "review|redteam"`
Expected: both `/review` and `/redteam` appear in the advertised list (script derives from `skills/`).

- [ ] **Step 4: Commit**

```bash
git add skills/review/SKILL.md skills/redteam/SKILL.md
git commit -m "feat(skills): add /review (diff_review) and /redteam (plan_critique) skills"
```

---

## Self-Review Notes

- Spec coverage: all four recommended tools from the gap analysis have tasks; the "do NOT build" list (refactor/consensus/dependency-graph/tracker/tracer) is intentionally excluded.
- Type consistency: `runPanel`/`Panelist` defined in Task 3 and consumed with identical signatures in Task 4; `buildDiffReviewerPrompt`/`buildDiffJudgePrompt` and `buildPlanCritiquePrompt`/`buildPlanCritiqueJudgePrompt` names match between test and implementation steps; all call-helper signatures verified against source (`callGemini` gemini-tools.ts:34, `callGrok` grok-tools.ts:64, `callOpenAI` openai-tools.ts:152, `callOpenRouter` openrouter-tools.ts:91).
- Known judgment calls: (1) diff_review/plan_critique live in the Gemini gate because the judge is Gemini — same trade-off the jury makes; panelists self-drop. (2) `.default()` on optional enums means `execute` may still receive `undefined` when called directly in tests (FastMCP parses defaults at the wire layer) — prompt builders therefore fall back internally (`args.focus || "all"`). (3) Counts in profile descriptions are asserted against build output each task, not assumed.
