# Writing Techniques + blog_writer 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 a `writing` technique family (3 techniques) that closes the structure/draft/revise hole in the library, surface it on the landing site, and add a `blog_writer` tool that runs the researched-blogpost pipeline in one call.

**Architecture:** Techniques are data + a template function, exactly like the existing 85 — a catalogue entry in `prompt-technique-tools.ts` and a handler in `prompt-engineer-lite.ts`. The landing site regenerates from the catalogue, so it needs only a category label. `blog_writer` is a `defineModelTool` in the `testgen` mould that composes an existing research tool with the `storm` and `re3` templates rather than carrying its own copies of them.

**Tech Stack:** TypeScript, zod, FastMCP, jest. Landing: Next.js 16, Tailwind 4.

**Spec:** `~/Downloads/tachibot-prompt-gap-2026_11.html`, "Writing & Content" tab. Its argument: the library has creative *thinking* (`what_if`, `innovate`, `alt_view`, `creative_use`) and zero creative *writing*. Mapped against a 7-stage pipeline it finds stage 4 (structure) and stage 5 (draft) EMPTY, stage 6 (revise) served by the wrong tool (`reflexion` optimises toward "correct"; prose revision is cutting, rhythm and specificity), and stage 3 (research) lacking discipline for what comes back.

---

## Global Constraints

- **Counts are derived, never typed.** `TECHNIQUE_COUNT = FLAT_TECHNIQUES.length` and `CORE_COUNT = CORE_TECHNIQUES.length` already exist. Do not write `88` anywhere. The literal `37` survived two expansions of this catalogue and `~9 core` survived until it meant 18 — do not add a third.
- **Every catalogue entry needs all five prose fields:** `name`, `alias`, `description`, `useWhen`, `example`. `useWhen` and `example` are non-optional in the `Technique` interface, so omitting them is a compile error.
- **`description` ends with the paper reference in exactly the form `(arXiv:NNNN.NNNNN)`** when one exists, because the landing generator splits it off with `/\(((?:arXiv:\d{4}\.\d{4,5}(?:,\s*)?)+)\)\s*$/` and renders it as a link. A reference in any other position stays inline as raw text.
- **Never hand-edit `landing/tachibot-landing/src/lib/techniques.generated.ts`.** Regenerate it.
- **JSX escaping is mandatory in JSX text** or the landing build fails: `&mdash;` `&ndash;` `&quot;` `&apos;`. The opposite applies inside TypeScript string literals — use real `—` and `"` there, because an entity in a string renders as the literal characters `&mdash;`.
- **Techniques register in `src/tools/registry.ts`, never `src/server.ts`** — that file is a thin bin dispatcher with zero static imports.
- **The golden snapshot must change by exactly the intended amount.** After any tool change run `UPDATE_GOLDEN=1 npm run test:golden` and inspect the diff before committing.
- **Provider helpers RETURN bracketed error strings, never throw.** `blog_writer` must follow this.

---

### Task 1: The `writing` family — 3 techniques

**Files:**
- Modify: `src/tools/prompt-technique-tools.ts` (add `writing` category to `TECHNIQUES`, add `"writing"` to the `filter` enum)
- Modify: `src/prompt-engineer-lite.ts` (3 Map entries + 3 alias entries)
- Test: `test/tools/writing-techniques.test.ts` (create)

**Interfaces:**
- Consumes: nothing from earlier tasks.
- Produces: technique names `storm`, `re3`, `de_generic` resolvable via `PromptEngineerLite.applyTechnique(tool, name, query)`; category id `writing` present in `FLAT_TECHNIQUES`. Task 2 renders these; Task 3 reuses the `storm` and `re3` template text.

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

Create `test/tools/writing-techniques.test.ts`:

```ts
import { PromptEngineerLite } from "../../src/prompt-engineer-lite.js";
import { listPromptTechniquesTool } from "../../src/tools/prompt-technique-tools.js";

describe("writing family", () => {
  const pe = new PromptEngineerLite();

  test("all three writing techniques resolve to real templates", () => {
    for (const name of ["storm", "re3", "de_generic"]) {
      const out = pe.applyTechnique("grok_reason", name, "TESTQ");
      expect(out.trim()).not.toBe("TESTQ");
      expect(out.length).toBeGreaterThan(200);
    }
  });

  test("aliases resolve to the same template as their canonical name", () => {
    const pairs: Record<string, string> = {
      research_first: "storm",
      four_pass: "re3",
      fix_boring: "de_generic",
    };
    for (const [alias, canonical] of Object.entries(pairs)) {
      expect(pe.applyTechnique("grok_reason", alias, "q")).toBe(
        pe.applyTechnique("grok_reason", canonical, "q"),
      );
    }
  });

  test("storm refuses to start writing before research", () => {
    const out = pe.applyTechnique("grok_reason", "storm", "q");
    expect(out).toMatch(/do not start writing/i);
    expect(out).toContain("[SEARCH]");
  });

  test("re3 keeps structural and line editing in separate passes", () => {
    const out = pe.applyTechnique("grok_reason", "re3", "q");
    expect(out).toMatch(/structure only/i);
    expect(out).toMatch(/line only/i);
  });

  test("de_generic forbids lengthening", () => {
    const out = pe.applyTechnique("grok_reason", "de_generic", "q");
    expect(out).toMatch(/do not lengthen/i);
  });

  test("the writing category is listed and filterable", async () => {
    const out = await listPromptTechniquesTool.execute({ filter: "writing" });
    expect(out).toContain("storm");
    expect(out).toContain("re3");
    expect(out).toContain("de_generic");
  });
});
```

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

Run: `npm test -- test/tools/writing-techniques.test.ts`
Expected: FAIL. `applyTechnique` returns the query unchanged for unknown names, so `expect(out.trim()).not.toBe("TESTQ")` fails first.

- [ ] **Step 3: Add the three engine templates**

In `src/prompt-engineer-lite.ts`, add these three entries to the `this.t` Map (the Map literal starting at line 30). Match the existing entry style: `['name', (q) => \`...\`],`.

```ts
    ['storm', (q) => `Write about "${q}". Do NOT start writing yet — research first.

STAGE 1 — PERSPECTIVES. Identify 4 people who would each care about this topic for a
DIFFERENT reason: a practitioner, a sceptic, a buyer, and someone burned by it before.
Name what each one actually wants to know.

STAGE 2 — INTERVIEW. For each perspective write the 4 questions they would ask,
including the awkward one that usually gets left out. Answer each, marking it:
  [KNOWN]   confident, no source needed
  [SEARCH]  needs a real source — say what would settle it
  [OPINION] genuinely contested — say who disagrees and why

STAGE 3 — WHAT IS ACTUALLY INTERESTING. Across all 16 answers, which 3 would surprise
a reader who already knows the basics? Those 3 are the piece; everything else is
context. If nothing surprises you, say so — the topic may not warrant writing up.

STAGE 4 — OUTLINE around those 3, with a WORD BUDGET per section. Budgets must be
uneven; equal budgets mean you have not decided what matters.`],

    ['re3', (q) => `Produce a long-form piece on "${q}" in four separate passes. Structural
rewriting and line editing fight each other — asking for both at once gets you neither,
because the model tidies sentences in a piece whose architecture is wrong.

PASS 1 — PLAN. No prose yet. Produce:
  - the ONE thing the reader should take away
  - the promise the opening makes (the piece must pay it off)
  - a section list, each with purpose, word budget, and the specific point it lands
  - the strongest objection a smart reader will have, and where you deal with it
Budgets must be UNEVEN. If they are all equal, the plan is lazy.

PASS 2 — DRAFT. Write to the plan. Do not polish, do not self-edit, do not stop to
improve a sentence. Hit the budgets.

PASS 3 — REWRITE, STRUCTURE ONLY. Ignore sentence quality entirely. Only ask:
  - does each section do its stated job?
  - is anything in the wrong order?
  - what can be CUT wholesale? Name at least one thing — "nothing" is not an
    acceptable answer.
  - does the ending pay off the opening's promise?
Output the restructured draft, not notes about it.

PASS 4 — EDIT, LINE ONLY. Structure is now fixed: do not move or delete sections. Only:
  - cut every word that earns nothing
  - break up any sentence you have to re-read
  - replace abstractions with specifics
  - vary sentence length; read for rhythm
  - delete every hedge that is not load-bearing

Report at the end: word count per pass, and what you cut in Pass 3. If the piece never
got shorter, Pass 3 did not happen.`],

    ['de_generic', (q) => `The following piece is competent and dull. Do NOT "improve" it —
improving fluency is what made it dull. Attack these three things only.

1. ORIGINALITY — find every sentence a competent writer would produce by default on
   this topic. Quote each one, then replace it with something only someone who
   actually knows this subject would say. If you cannot, mark it CUT.

2. ELABORATION — find every claim that is asserted and then abandoned. Each needs ONE
   of: a concrete example, a number, a named case, or a mechanism explaining why it is
   true. Generic support ("this improves efficiency") counts as abandoned.

3. SPECIFICITY SWEEP — replace every one of these:
   - "many/several/various"        → the actual number
   - "significantly/dramatically"  → by how much
   - "experts say"                 → which experts
   - "can be challenging"          → the specific failure
   - any noun phrase that would fit an article on a completely different topic

Then answer: could a competitor publish this under their name with no changes? If yes,
say which parts made that possible.

Do NOT lengthen it. Trade word for word.

PIECE: ${q}`],
```

- [ ] **Step 4: Add the three aliases**

In `src/prompt-engineer-lite.ts`, in the `techniqueMap` object (starts line 695), add under the "New technique aliases" section:

```ts
    research_first: 'storm',
    four_pass: 're3',
    fix_boring: 'de_generic',
```

- [ ] **Step 5: Add the catalogue entries**

In `src/tools/prompt-technique-tools.ts`, add a new `writing` key to the `TECHNIQUES` object. Place it immediately after the `creative` category, because the spec's argument is that `creative` is ideation and `writing` is the making half:

```ts
  writing: [
    {
      name: "storm",
      alias: "research_first",
      description: "Interview four different personas about the topic, mark each answer KNOWN/SEARCH/OPINION, then build the piece around only the 3 answers that would surprise a reader who knows the basics. (arXiv:2402.14207)",
      useWhen: "Any researched piece — article, doc, report — where you would otherwise start drafting from what you already know.",
      example: "Generic writing happens because the model answers the obvious questions. The sceptic's awkward question is the one that makes the piece worth reading.",
      tier: 'core',
      kind: 'contract',
    },
    {
      name: "re3",
      alias: "four_pass",
      description: "Plan, draft, restructure, then line-edit — as four separate passes, never combined. (arXiv:2210.06774)",
      useWhen: "Anything past a few hundred words that has to hold together.",
      example: "Ask for structure and sentence polish at once and you get neither: the model tidies prose in a piece whose architecture is wrong. If the draft never got shorter, the restructure pass did not happen.",
      tier: 'core',
      kind: 'contract',
    },
    {
      name: "de_generic",
      alias: "fix_boring",
      description: "Attack originality, elaboration and specificity directly instead of asking for a better draft — fluency is the part that was already fine. (arXiv:2309.14556)",
      useWhen: "A draft that reads competently and says nothing.",
      example: "\"Improve this\" gets you more fluency, which is what made it dull. The test is whether a competitor could publish it unchanged under their own name.",
    },
  ],
```

- [ ] **Step 6: Add `writing` to the filter enum**

In `src/tools/prompt-technique-tools.ts`, the `filter` parameter enum lists every category. Add `"writing"`:

```ts
    filter: z.enum(["all", "creative", "writing", "research", "analytical", "reflective", "reasoning", "verification", "meta", "debate", "judgment", "engineering", "research_advanced", "decision", "structured_coding", "retrieval", "security", "few_shot", "tabular"])
      .optional()
      .describe("Show one category in full (implies all techniques in it)"),
```

- [ ] **Step 7: Add the short labels**

`getTechniqueDescription()` in `src/prompt-engineer-lite.ts:883` holds a `desc` map of
terse labels, used by workflow status lines (`src/visualizer-lite.ts:27`,
`PromptEnhancementHandler.ts:103`). It falls back to the raw technique name
(`return desc[key] || technique;` at line 999), so omitting this is cosmetic rather
than breaking — but the label is what a reader sees in a running workflow.

Add to the `desc` map:

```ts
      // Writing
      'storm': 'Research-first',
      're3': 'Four-pass',
      'de_generic': 'De-generic',
```

- [ ] **Step 8: Build and run the tests**

Run: `npm run build && npm test -- test/tools/writing-techniques.test.ts`
Expected: PASS, 6 tests.

- [ ] **Step 9: Verify counts moved and nothing else broke**

Run:
```bash
node -e "import('./dist/src/tools/prompt-technique-tools.js').then(m=>{const f=m.FLAT_TECHNIQUES;console.log('techniques',f.length,'core',f.filter(t=>t.tier==='core').length,'cats',new Set(f.map(t=>t.category)).size)})"
npm test
```
Expected: `techniques 88 core 20 cats 18`. Full suite passes (34 suites).

- [ ] **Step 10: Update the golden snapshot**

The `list_prompt_techniques` description embeds `${CORE_COUNT}` and `${TECHNIQUE_COUNT}`, and the `filter` enum gained a value, so the tool contract changes.

Run: `UPDATE_GOLDEN=1 npm run test:golden`
Then: `git diff test/golden/__snapshots__/tool-contracts.json`
Expected: exactly two changes — the counts in the description (18→20, 85→88) and `"writing"` added to the filter enum. Anything else means something unintended changed.

- [ ] **Step 11: Commit**

```bash
git add src/tools/prompt-technique-tools.ts src/prompt-engineer-lite.ts \
        test/tools/writing-techniques.test.ts test/golden/__snapshots__/tool-contracts.json
git commit -m "feat(prompts): add the writing family — storm, re3, de_generic

The library had creative THINKING (what_if, innovate, alt_view, creative_use)
and zero creative WRITING. Against a 7-stage pipeline, stages 4 (structure) and
5 (draft) were empty, and stage 6 (revise) was served by reflexion, which
optimises toward correct — prose revision is cutting, rhythm and specificity.

storm      research before drafting; build the piece around the 3 surprising
           answers, not the obvious ones
re3        plan / draft / restructure / line-edit as SEPARATE passes, because
           structural and line editing fight each other
de_generic attack originality, elaboration and specificity — never fluency,
           which is the part that was already fine"
```

---

### Task 2: Surface the writing family on the landing site

**Files:**
- Modify: `landing/tachibot-landing/src/components/docs/PromptTechniqueCatalog.tsx` (`CATEGORY_BLURBS`)
- Modify: `landing/tachibot-landing/scripts/generate-techniques.mjs` (`CATEGORY_LABELS`)
- Regenerate: `landing/tachibot-landing/src/lib/techniques.generated.ts`

**Interfaces:**
- Consumes: category id `writing` and technique names `storm`, `re3`, `de_generic` from Task 1, read out of `tachibot-mcp/dist`.
- Produces: nothing later tasks depend on.

- [ ] **Step 1: Add the human label for the new category**

In `landing/tachibot-landing/scripts/generate-techniques.mjs`, `CATEGORY_LABELS` maps category ids to display names. Unmapped ids fall back to title-case, so this is what stops it rendering as "Writing" without intent:

```js
  creative: 'Ideas & Creativity',
  writing: 'Writing & Long-form',
```

- [ ] **Step 2: Add the family blurb**

In `landing/tachibot-landing/src/components/docs/PromptTechniqueCatalog.tsx`, `CATEGORY_BLURBS` explains what each family is FOR before the names arrive. Add:

```tsx
  writing:
    'Making the thing, not thinking it up. The creative family generates angles; this one structures, drafts and revises. Prose revision is a different job from being correct — cutting, rhythm and specificity rather than accuracy.',
```

- [ ] **Step 3: Regenerate from the built server**

The generator reads `tachibot-mcp/dist`, so Task 1 must be built first.

Run:
```bash
cd tachibot-mcp && npm run build
cd ../landing/tachibot-landing && node scripts/generate-techniques.mjs
```
Expected output: `88 techniques, 18 categories` and `every technique resolved to a real template`.

- [ ] **Step 4: Verify the new family reaches the rendered page**

Run:
```bash
npm run build
H=.next/server/app/docs/prompt-techniques.html
for s in "Writing &amp; Long-form" storm re3 de_generic; do
  printf "%-22s %s\n" "$s" "$(grep -o "$s" $H | wc -l | tr -d ' ')"
done
```
Expected: the label appears twice (filter row + section heading); each technique name appears at least once. Zero for any of them means the regeneration did not take.

- [ ] **Step 5: Commit**

```bash
cd landing/tachibot-landing
git add src/ scripts/
git commit -m "feat(docs): surface the writing family

Adds the Writing & Long-form label and family blurb, and regenerates the
catalogue from the server: 85 -> 88 techniques, 17 -> 18 categories.

The blurb states the distinction the spec turns on: the creative family
generates angles, this one makes the thing. Prose revision is not the same
job as being correct."
```

---

### Task 3: The `blog_writer` tool

**Files:**
- Create: `src/tools/blog-writer-tool.ts`
- Modify: `src/tools/registry.ts` (register in the OpenRouter gate)
- Modify: `src/tools/provider-catalog.ts` (catalogue entry)
- Modify: `src/profiles/types.ts` (interface boolean)
- Modify: `src/profiles/full.ts`, `balanced.ts`, `research_power.ts`, `heavy_coding.ts`, `code_focus.ts`, `minimal.ts`
- Modify: `tools.config.json` (gitignored — edit on disk)
- Test: `test/tools/blog-writer.test.ts` (create)

**Interfaces:**
- Consumes: the `storm` and `re3` template text from Task 1, via `new PromptEngineerLite().applyTechnique()` — the tool must NOT carry its own copy of those prompts, or they drift.
- Produces: MCP tool `blog_writer` with parameters `{ topic: string; wordCount?: number; research?: boolean; audience?: string }`.

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

Create `test/tools/blog-writer.test.ts`:

```ts
import { buildBlogWriterPrompt } from "../../src/tools/blog-writer-tool.js";

describe("blog_writer prompt construction", () => {
  test("uses the storm template rather than a private copy", () => {
    const p = buildBlogWriterPrompt({ topic: "MCP tool bloat", research: true });
    expect(p).toContain("STAGE 1 — PERSPECTIVES");
    expect(p).toContain("[SEARCH]");
  });

  test("uses the re3 template for the writing half", () => {
    const p = buildBlogWriterPrompt({ topic: "MCP tool bloat", research: true });
    expect(p).toContain("PASS 3 — REWRITE, STRUCTURE ONLY");
  });

  test("skips the research stage when research is false", () => {
    const p = buildBlogWriterPrompt({ topic: "x", research: false });
    expect(p).not.toContain("STAGE 1 — PERSPECTIVES");
    expect(p).toContain("PASS 1 — PLAN");
  });

  test("carries the word budget into the prompt when given", () => {
    const p = buildBlogWriterPrompt({ topic: "x", wordCount: 1200 });
    expect(p).toContain("1200");
  });

  test("names the audience when given", () => {
    const p = buildBlogWriterPrompt({ topic: "x", audience: "backend engineers" });
    expect(p).toContain("backend engineers");
  });
});
```

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

Run: `npm test -- test/tools/blog-writer.test.ts`
Expected: FAIL, "Cannot find module '../../src/tools/blog-writer-tool.js'".

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

Create `src/tools/blog-writer-tool.ts`. Model it on `src/tools/testgen-tool.ts` — same imports, same `defineModelTool` shape, same `withHeartbeat` wrapper.

```ts
/**
 * blog_writer — researched long-form in one call.
 *
 * Composes the `storm` and `re3` techniques rather than copying their text. Those
 * templates live in PromptEngineerLite and are the single source of truth; a private
 * copy here would drift the moment either is edited, and the drift would be silent
 * because both paths still produce well-formed prose.
 */
import { z } from "zod";
import { defineModelTool } from "./factory/define-model-tool.js";
import { callOpenRouter, OpenRouterModel } from "./openrouter-tools.js";
import { withHeartbeat } from "../utils/streaming-helper.js";
import { PromptEngineerLite } from "../prompt-engineer-lite.js";

const engine = new PromptEngineerLite();

export interface BlogWriterArgs {
  topic: string;
  wordCount?: number;
  research?: boolean;
  audience?: string;
}

export function buildBlogWriterPrompt(args: BlogWriterArgs): string {
  const { topic, wordCount, research = true, audience } = args;

  const parts: string[] = [];

  if (research) {
    parts.push(engine.applyTechnique("blog_writer", "storm", topic));
    parts.push(
      "Having completed the research above, now write the piece using the four-pass method below. Use only the 3 surprising answers as the spine.",
    );
  }

  parts.push(engine.applyTechnique("blog_writer", "re3", topic));

  if (wordCount) {
    parts.push(
      `TOTAL LENGTH: about ${wordCount} words. Distribute this across the sections unevenly — the section that carries the argument gets the most.`,
    );
  }
  if (audience) {
    parts.push(
      `AUDIENCE: ${audience}. Assume they know the basics; do not explain what they already know.`,
    );
  }

  return parts.join("\n\n---\n\n");
}

export const blogWriterTool = defineModelTool({
  name: "blog_writer",
  description:
    "Write a researched long-form post: interview four personas for the angle, then plan/draft/restructure/line-edit in separate passes. Put the SUBJECT in the 'topic' parameter.",
  parameters: z.object({
    topic: z.string().describe("What the post is about (REQUIRED - put your subject here)"),
    wordCount: z.number().optional().describe("Target length in words"),
    research: z.boolean().optional().default(true).describe("Run the persona-interview research stage first (default true)"),
    audience: z.string().optional().describe("Who it is for, e.g. 'backend engineers'"),
  }),
  execute: async (args: BlogWriterArgs, { reportProgress }: any) => {
    const prompt = buildBlogWriterPrompt(args);
    return withHeartbeat(
      () =>
        callOpenRouter(prompt, {
          model: OpenRouterModel.KIMI,
          maxTokens: 8000,
        }),
      reportProgress,
      "blog_writer",
    );
  },
});
```

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

Run: `npm run build && npm test -- test/tools/blog-writer.test.ts`
Expected: PASS, 5 tests.

- [ ] **Step 5: Add blog_writer to the technique executor's allowlist**

`executeTargetTool()` in `src/tools/prompt-technique-tools.ts:1202` holds a HARDCODED
`toolMappings` dict of the only 16 tools `execute_prompt_technique` can call. Line 1281
throws `Tool "X" is not supported for prompt techniques` for anything absent. Without
this step, `execute_prompt_technique({ technique: "re3", tool: "blog_writer" })` fails
even though both exist.

Add beside the other entries, matching their shape:

```ts
    'blog_writer': async () => {
      const { blogWriterTool } = await import('./blog-writer-tool.js');
      return blogWriterTool.execute({ topic: prompt }, context);
    },
```

Note for whoever touches this later: this dict is a third hardcoded technique-to-tool
wiring point, alongside the catalogue and the engine. It is the concrete form of the
"how do techniques reach the tools" question, and it currently answers it for 16 of 66
tools with no test asserting the list stays in sync with the registry.

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

In `src/tools/registry.ts`, append at the END of the OpenRouter gate, beside the `testgen` registration around line 205:

```ts
    // blog_writer — researched long-form via Kimi — gated on OpenRouter.
    const { blogWriterTool } = await import("./blog-writer-tool.js");
    server.addTool(blogWriterTool);
```

- [ ] **Step 7: Add to the provider catalogue**

In `src/tools/provider-catalog.ts`, add `"blog_writer"` to the OpenRouter tool-name array (the one containing `"testgen"` at line 116).

- [ ] **Step 8: Add the profile flag to the interface and all six profiles**

In `src/profiles/types.ts`, beside the `testgen` line (93):

```ts
  blog_writer: boolean;  // Researched long-form writing via Kimi
```

Then set it in each of the six profile files. Enable where long-form belongs, disable elsewhere:

```ts
// src/profiles/full.ts, balanced.ts, research_power.ts
  blog_writer: true,

// src/profiles/minimal.ts, code_focus.ts, heavy_coding.ts
  blog_writer: false,
```

Bump the tool count in each enabled profile's description string.

- [ ] **Step 9: Add to tools.config.json**

`tools.config.json` is gitignored, so edit it on disk. Add `"blog_writer"` to the `availableTools` array.

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

Run: `npm run build`
Expected: the profile builder prints `full.json (67 tools)` — one more than before. If `full` still says 66, the profile entry did not take.

- [ ] **Step 11: Update the golden snapshot and inspect the diff**

Run: `UPDATE_GOLDEN=1 npm run test:golden`
Then: `git diff test/golden/__snapshots__/tool-contracts.json`
Expected: exactly one added tool object, `blog_writer`. Any other change means something unintended moved.

- [ ] **Step 12: Run the full suite**

Run: `npm test`
Expected: 35 suites pass.

- [ ] **Step 13: Update the docs**

- `README.md` — tool count badge and the Tool Ecosystem section
- `docs/TOOLS_REFERENCE.md` — a `### blog_writer` section with the parameter table
- `CLAUDE.md` — the tool count in the Project Overview line

- [ ] **Step 14: Commit**

```bash
git add src/ test/ docs/ README.md CLAUDE.md
git commit -m "feat(tools): add blog_writer — researched long-form in one call

Composes the storm and re3 techniques rather than copying their text. A private
copy would drift the moment either template is edited, and silently, because
both paths still emit well-formed prose.

research:true runs the persona interview first and feeds the 3 surprising
answers into the four-pass write. research:false goes straight to the passes.

66 -> 67 tools. Enabled in full, balanced and research_power; off in minimal,
code_focus and heavy_coding."
```

---

## Spec coverage — what this plan does NOT cover

The spec maps 7 stages. This plan closes stages 3, 4, 5 and 6. Two are deliberately left:

- **Stage 1 (have an idea)** — already covered. The spec's only complaint is mode
  collapse, and `verbalized_sampling` (added in v2.31.0) is named as the fix. Nothing
  to do.
- **Stage 7 (judge if it's any good)** — the spec rates `council_of_experts` "partial:
  works, but no craft rubric — it'll tell you the argument holds, not that paragraph
  three is limp." It identifies the gap but proposes no technique for it, and inventing
  a craft rubric with no source would be exactly the unsourced-specifics defect these
  entries are meant to avoid. Left open deliberately. If it is wanted later, the
  honest starting point is the "Art or Artifice?" evaluation criteria
  (arXiv:2309.14556), which is already the basis for `de_generic`.

All three arXiv identifiers in this plan were verified against the papers
(STORM 2402.14207, Re3 2210.06774, Art or Artifice 2309.14556). The source report
names the papers but cites no identifiers, so these were looked up, not remembered.

## Notes for the executor

**Task 1 and 2 are shippable without Task 3.** If the tool turns out not to earn its place, stopping after Task 2 leaves the library complete and the site correct.

**Task 3 was chosen against a recommendation.** The techniques alone already give a researched blog pipeline via `execute_prompt_technique({ technique: "storm", tool: "perplexity_reason", query: "..." })`, and this server is at 66 tools where published guidance puts tool-selection degradation past 20–30. The tool is being built because a single call is more discoverable than a three-parameter composition. If tool-selection accuracy is later measured as a problem, this is a candidate for removal — which is why Step 3 composes the templates instead of copying them.

**The `research: true` path does not itself search.** It instructs the model to mark claims `[SEARCH]`. Genuine grounding still requires routing through a search-capable tool. Do not describe `blog_writer` as performing research in the docs; describe it as producing a piece that names what needs sourcing.
