---
name: prd-spec-generator
version: 0.6.1
description: Action-driven PRD generation. The MCP server runs a stateless 20-step pipeline reducer (11 PRD-generation steps + 9 opt-in implementation steps behind a human gate); the host (Claude Code) executes each substantive action and feeds the result back via submit_action_result, looping until done. Multi-judge verification combines genius reasoning patterns with zetetic team subagents. Grounded in the ai-architect ecosystem (automatised-pipeline MCP, Cortex MCP, zetetic-team-subagents).
dependencies: node>=20
prd_contexts: proposal, feature, bug, incident, poc, mvp, release, cicd
mcp_tools:
  - start_pipeline
  - submit_action_result
  - get_pipeline_state (read-only diagnostic; does not advance the pipeline)
  - plan_section_verification
  - plan_document_verification
  - conclude_verification
mcp_tools_legacy:
  - get_config, read_skill_config
  - check_health, get_prd_context_info, list_available_strategies
  - validate_prd_section, validate_prd_document
  - get_quality_history, get_strategy_effectiveness
  - coordinate_context_budget, map_failure_to_retrieval
ecosystem:
  - automatised-pipeline (Rust MCP) — codebase indexing, graph queries, impact, semantic-diff
  - cortex (Python MCP) — persistent memory, recall, methodology, narrative
  - zetetic-team-subagents — engineer, code-reviewer, test-engineer, security-auditor, dba, ...
  - genius reasoning patterns — liskov, dijkstra, fermi, popper, ...
---

# AI PRD Generator (v0.6.1) — Dispatcher Protocol

You (the host) drive a loop:

1. Call `start_pipeline` → receive an envelope with `{ run_id, messages, action, ... }`.
2. Display every entry in `messages` to the user.
3. Execute `action` per the dispatch table below.
4. Call `submit_action_result(run_id, result)` → receive next envelope.
5. Repeat until `action.kind === "done"` or `action.kind === "failed"`.

**`emit_message` is never returned to the host as `action`.** The runner coalesces all status messages into the `messages` array; the `action` field always carries something the host actually has to do.

---

## ENVELOPE SHAPE

Every response from `start_pipeline` and `submit_action_result` has this shape:

```json
{
  "run_id": "run_abc_123",
  "current_step": "context_detection",
  "messages": [
    { "text": "🟢 PRD Spec Generator — TRIAL TIER\n...", "level": "info" }
  ],
  "action": { "kind": "ask_user" | "call_pipeline_tool" | ... , ... },
  "state_summary": { "sections": [...], "clarification_rounds": 3, "errors": 0 }
}
```

- `run_id` — handle for `submit_action_result` and `get_pipeline_state`. Caputre once from the first response and reuse.
- `messages` — banners/status lines collected while the runner advanced internally to reach `action`. May be empty. Display each `text` at the given `level` (default `info`) before executing `action`.
- `action` — what you must execute. **Never `emit_message`.** Always one of: `ask_user`, `call_pipeline_tool`, `call_cortex_tool`, `spawn_subagents`, `write_file`, `done`, `failed`.

---

## TERMINOLOGY (read this before the dispatch table)

| Term | Means |
|---|---|
| **the host** | Claude Code (or another MCP-aware client) running this dispatcher loop |
| **the project directory** | The codebase root where `.mcp.json` lives — distinct from "the host" |
| **the runner** | The MCP server's stateless reducer behind the pipeline tools |
| **the result** (always typed) | The `ActionResult` value the host passes to `submit_action_result` — exactly one of four `kind`s |
| **agent output** | The raw text returned by an Agent tool call to a subagent — appears in `subagent_batch_result.responses[i].raw_text` |
| **a judge** (`purpose: "judge"`) | A spawned subagent invocation that returns a `JudgeVerdict` JSON object. The same word also names the `judge` field inside `JudgeVerdict`, which holds the `AgentIdentity` of the agent that rendered the verdict. |
| **routing token** | One of `correlation_id`, `invocation_id`, `batch_id`, `question_id` — opaque strings the host MUST echo back unchanged |
| **`messages`** | The array of `{ text, level }` entries the runner collected while advancing internally to the substantive `action`. Display each `text` at its `level` before executing `action`. |
| **substantive action** | Any `action` kind that requires host execution and a submitted result: `ask_user`, `call_pipeline_tool`, `call_cortex_tool`, `spawn_subagents`, `write_file`. Plus terminal kinds `done` and `failed`. (`emit_message` is NEVER returned to the host — the runner coalesces it.) |
| **coalescing** | The runner's behavior of collecting status signals (which would otherwise be individual `emit_message` actions) into the `messages` array, so each response carries exactly one substantive action plus its accumulated message context. |

---

## DISPATCH TABLE — ONE ROW PER ACTION KIND

### `ask_user`

**Action shape:**
```json
{
  "kind": "ask_user",
  "question_id": "prd_context",
  "header": "Which kind of PRD?",
  "description": "...",
  "options": [{ "label": "feature", "description": "..." }] | null,
  "multi_select": false
}
```

**Execute:** Invoke `AskUserQuestion` with `header`, `description`, and `options` (if non-null). If `options === null`, accept freeform.

**Submit:**
```json
{
  "kind": "user_answer",
  "question_id": "<echo action.question_id verbatim>",
  "selected": ["<chosen option label string>"],
  "freeform": "<text answer if any>"
}
```

`AskUserQuestion` returns the chosen option's `label` string directly. Place that string in `selected[0]`. If the user typed a freeform answer, place it in `freeform` and leave `selected` empty.

---

### `call_pipeline_tool`

**Action shape:**
```json
{
  "kind": "call_pipeline_tool",
  "tool_name": "index_codebase",
  "arguments": { "path": "/abs/path", "output_dir": "...", "language": "auto" },
  "correlation_id": "input_analysis_index"
}
```

**Execute:** Call the **automatised-pipeline MCP** tool named `tool_name` with `arguments` exactly as provided. The tool prefix is `mcp__<server-key>__<tool_name>` where `<server-key>` is the registration key in your project's `.mcp.json` for the automatised-pipeline server. The default registration key is `automatised-pipeline` (per the automatised-pipeline plugin's `.mcp.json`), so the prefix is `mcp__plugin_automatised-pipeline_automatised-pipeline__<tool_name>`. Inspect your `.mcp.json` to confirm.

**If the automatised-pipeline MCP is not registered in your project**, submit a tool_result with `success: false`. The pipeline will halt with a `failed` action; the user must register the dependency before re-running.

**Submit (success):**
```json
{
  "kind": "tool_result",
  "correlation_id": "<echo action.correlation_id verbatim>",
  "success": true,
  "data": <the JSON the tool returned, unmodified>
}
```

**Submit (failure):**
```json
{
  "kind": "tool_result",
  "correlation_id": "<echo action.correlation_id verbatim>",
  "success": false,
  "error": "<error string>"
}
```

---

### `call_cortex_tool`

**Action shape:** identical to `call_pipeline_tool` but `tool_name` refers to a **Cortex MCP** tool. Tool prefix convention: `mcp__plugin_hypermnesia-mcp_cortex__<tool_name>` or `mcp__cortex__<tool_name>` per your `.mcp.json`.

> The plugin-scoped prefix carries **both** the plugin name and the MCP server key: `mcp__plugin_<plugin-name>_<server-key>__<tool>`. Cortex renamed the plugin `cortex` -> `hypermnesia-mcp` in v4.15.0 (community-directory name collision) while keeping the server key `cortex`, so the prefix is `mcp__plugin_hypermnesia-mcp_cortex__`, not `mcp__plugin_cortex_cortex__`. An unresolvable MCP tool name is dropped silently by the host, so the old prefix fails with no error. <!-- mcp-prefix-allow-legacy: this line quotes the dead spelling on purpose -->


The handlers currently emit `tool_name: "recall"` for section-context retrieval. Other Cortex tools may be added by future handlers; do not assume a fixed list.

**Submit:** same `tool_result` shape as `call_pipeline_tool`. Pass `data` through unmodified — the runner's parser expects the standard Cortex `{ results: [{ content, score, ... }] }` shape.

---

### `spawn_subagents`

**Action shape:**
```json
{
  "kind": "spawn_subagents",
  "purpose": "draft" | "judge" | "review",
  "batch_id": "self_check_verify",
  "invocations": [
    {
      "invocation_id": "self_check_judge_0001",
      "subagent_type": "zetetic-team-subagents:genius:liskov",
      "description": "Judge FR-001 (genius:liskov)",
      "prompt": "<full self-contained prompt — DO NOT modify>",
      "isolation": "none"
    }
  ]
}
```

`purpose` is an observability label — your dispatch logic does NOT branch on it.

`subagent_type` is an opaque string. Pass it verbatim to the Agent tool's `subagent_type` parameter. Do not parse, normalize, or construct it yourself.

**Execute:** Issue **all** invocations in a **single message** with **multiple parallel Agent tool calls**. (Sequential dispatch produces a correct result but multiplies wall-clock time by N — for self-check batches with many judges this can exceed practical timeouts.) Each Agent call: `subagent_type = invocation.subagent_type`, `description = invocation.description`, `prompt = invocation.prompt`. **Never modify the prompt** — judge prompts contain JSON-output instructions that the runner's parser depends on.

**Submit:**
```json
{
  "kind": "subagent_batch_result",
  "batch_id": "<echo action.batch_id verbatim>",
  "responses": [
    {
      "invocation_id": "<echo invocation.invocation_id verbatim>",
      "raw_text": "<the agent's full final reply text>"
    },
    {
      "invocation_id": "<echo invocation.invocation_id verbatim>",
      "error": "<set this if the agent failed; raw_text optional>"
    }
  ]
}
```

For a successful agent: include `raw_text` (the full reply string). The runner extracts JSON from it.
For a failed agent: include `error`. The runner records an `INCONCLUSIVE` verdict for that claim.

---

### `write_file`

**Action shape:**
```json
{ "kind": "write_file", "path": "prd-output/abc12345/01-prd.md", "content": "..." }
```

**Execute:** Write `content` to `path` (relative to the project directory). Create parent directories as needed.

**Submit:**
```json
{ "kind": "file_written", "path": "<echo action.path verbatim>", "bytes": <UTF-8 byte length of content> }
```

`bytes` is the UTF-8 byte length of `content` (not character count). Use `Buffer.byteLength(content, 'utf8')` or equivalent.

---

### `done`

**Action shape:**
```json
{ "kind": "done", "summary": "...", "artifacts": ["overview: passed", ...] }
```

**Execute:** Display `messages` (if any), then `summary`, then the `artifacts` list. **Stop the loop.**

---

### `failed`

**Action shape:**
```json
{ "kind": "failed", "reason": "...", "step": "input_analysis" }
```

**Execute:** Display `messages` (if any), then `reason` and the failing `step`. **Stop the loop.** If the user wants more detail, call `get_pipeline_state(run_id, format: "full")` and show `state.errors[]`. The `full` response is bounded to the 100,000-char MCP budget and degrades least-relevant detail first (grounding → oldest clarifications → section content); a `__bounded` block records anything omitted/elided, and `errors[]` is never shed. To pull the full code-graph grounding that `full` sheds first, call `get_pipeline_state(run_id, format: "grounding")`.

---

## ERROR TRANSPORT

`submit_action_result` itself can return `isError: true` from the MCP layer. Two cases:

| Cause | Recovery |
|---|---|
| Concurrent submission rejected — you called `submit_action_result` for the same `run_id` while a previous call was still in flight | The host implementation has a bug. Wait for the in-flight call to return, then submit ONCE. Do not retry blindly. |
| Unknown `run_id` | The MCP server process restarted and lost in-memory state. The pipeline cannot be resumed — call `start_pipeline` to begin a new run. |

In both cases, display the `error` field from the response to the user and stop.

---

## HARD RULES

1. **Never modify `prompt` in any `spawn_subagents.invocations[i]`.** Forward it verbatim. Modifying judge prompts breaks the runner's JSON parser; modifying drafter prompts produces sections that fail validation.

2. **Never alter routing tokens.** `correlation_id`, `invocation_id`, `batch_id`, and `question_id` are opaque values emitted by the runner; the host echoes them back unchanged on the corresponding result. The runner uses them to route results back to the handler that is waiting.

3. **Always issue `spawn_subagents` invocations in parallel.** One message; many Agent tool calls. (Performance, not correctness — but production-relevant.)

4. **Never call `start_pipeline`, `submit_action_result`, or `get_pipeline_state` from inside an Agent tool call.** Subagents return their text; the host (not the subagent) submits the batch result.

5. **Stop the loop on `done` or `failed`.** Do not re-call `submit_action_result` afterwards.

6. **One pipeline = one `run_id`.** Do not interleave actions from different runs.

7. **Never call `submit_action_result` concurrently for the same `run_id`.** The runner's in-flight guard rejects with `isError: true`.

8. **`subagent_type` is opaque.** Pass it verbatim to the Agent tool. Do not construct, normalize, or guess it.

---

## VERIFICATION TAXONOMY (judge-facing — host does not enforce)

This table describes constraints baked into judge prompts. The host does not enforce verdicts; the runner does, via the `concludeDocument` consensus engine.

| Verdict | Meaning |
|---|---|
| PASS | Structurally complete + verifiable from the document |
| SPEC-COMPLETE | Test method specified; needs runtime to confirm |
| NEEDS-RUNTIME | Cannot verify at design time |
| INCONCLUSIVE | Depends on unresolved OQ-XXX or external factor |
| FAIL | Structurally invalid or contradicts other claims |

NFR claims (latency, throughput, fps, storage) MUST NOT receive PASS — judges receive this constraint in their prompts. The host does not inspect or filter verdicts.

---

## ECOSYSTEM BOUNDARY (informational — for project setup, not host dispatch)

| Concern | Owner |
|---|---|
| Codebase indexing, graph queries, impact, semantic diff | **automatised-pipeline MCP** |
| Persistent memory, recall, narrative, methodology | **Cortex MCP** |
| Reasoning roles (engineer, reviewer, dba, ...) | **zetetic-team-subagents** |
| Genius reasoning patterns | **zetetic-team-subagents/genius** |
| PRD generation, multi-judge verification, hard-output rules, file export | **prd-spec-generator** (this) |

A correctly configured project has all four ecosystem dependencies registered in `.mcp.json`. Missing dependencies surface as `tool_result.success: false` from `call_pipeline_tool` or `call_cortex_tool`, which causes the pipeline to emit `failed`.

---

## CORTEX MEMORY HOOKS (what gets remembered + recalled across sessions)

Cortex is the persistent memory engine. The pipeline uses it on two surfaces — **recall during section generation** (already covered in the `call_cortex_tool` dispatch row above) and **storage of decisions/lessons that future sessions benefit from**. The host does not orchestrate this storage; Cortex's hooks (`SessionStart`, `UserPromptSubmit`, `PostToolUse`, `SessionEnd`, `Notification`) capture it automatically when the Cortex plugin is installed.

### What recall pulls IN (during section generation)

Each section's `pending → retrieving` transition emits `call_cortex_tool({ tool_name: "recall", arguments: { query: "<section-specific>", max_results: 8 } })`. The query template is per-section-type (see `packages/orchestration/src/section-plan.ts:SECTION_RECALL_TEMPLATES`):

| Section type | Recall query shape | What's recalled |
|---|---|---|
| `requirements` | `requirements decisions for <feature>` | Past FR decisions, rejected approaches, scope boundaries |
| `technical_specification` | `architecture pattern <feature> ports adapters` | Past architecture choices, port/adapter conventions, framework rejections |
| `data_model` | `data model <feature> tables relationships migrations` | Schema decisions, migration history, FK conventions |
| `acceptance_criteria` | `acceptance criteria <feature> Given When Then` | Past AC patterns, traceability conventions |
| `security_considerations` | `security threats <feature> auth authz STRIDE` | Threat model decisions, auth choices, secret-handling patterns |
| `performance_requirements` | `performance NFR <feature> latency throughput SLA` | Past p95/p99 targets, measurement methods, capacity decisions |
| `testing` | `testing strategy <feature> coverage e2e integration` | Test patterns, coverage decisions, anti-patterns to avoid |

The recall results land in the engineer subagent's prompt as `<codebase_context>...</codebase_context>` (truncated to 800 chars per result; max 8 results per section — see `RECALL_MAX_RESULTS_INCLUDED` in `section-generation.ts`). The engineer is instructed to weight recall content but not blindly copy it.

### What's worth storing OUT (for future PRDs)

When this PRD reaches `done`, the Cortex `SessionEnd` hook captures session content automatically. The most valuable artifacts to store:

1. **Clarification answers.** The Q&A turns in `state.clarifications` — these are the user's load-bearing decisions. Tag: `decision`, `feature:<feature_description>`.
2. **Section-failure violations.** Each entry in `state.errors` with `error_kind: "section_failure"` represents a validator violation the engineer couldn't fix in 3 attempts. These are anti-patterns worth remembering. Tag: `lesson`, `failure-mode:<rule>`.
3. **Strategy effectiveness.** Each `state.strategy_executions` entry records `(strategy, claim_characteristics, wasCompliant, actualConfidenceGain)`. This is the closed feedback loop — recorded automatically via `EffectivenessTracker.recordExecution` into the project's `EvidenceRepository` (separate from Cortex; SQLite-backed). Cortex is NOT the storage for this signal — it lives in `~/.prd-gen/evidence.db`.
4. **Multi-judge verdict patterns.** `done.verification.distribution` per claim. A `distribution_suspicious=true` flag is itself worth remembering — it indicates the panel was likely too soft on this kind of claim. Tag: `lesson`, `verification:confirmatory-bias`.

The host does NOT explicitly call `cortex.remember(...)` from the dispatch loop — Cortex's `PostToolUse` and `SessionEnd` hooks observe the `done` action's `summary` + `verification` fields and store what's structured enough to retrieve later.

### Verification of recall freshness

If a section's recall returns content stale enough to mislead the engineer, the validator catches it via `fr_traceability` / `architecture` / etc., the section retries, and a new draft emerges without the stale guidance. The retry budget (`MAX_ATTEMPTS=3`) is the safety bound on bad-recall propagation.

---

## MINIMAL WORKED LOOP (illustrative; not exhaustive)

```
host: start_pipeline({ feature_description: "build OAuth login", codebase_path: "/abs/path" })

  → envelope { run_id, messages: [<welcome banner>, <PRD context detected: Feature>],
              action: { kind: "call_pipeline_tool", tool_name: "index_codebase",
                        arguments: { path: "/abs/path", output_dir: "...", language: "auto" },
                        correlation_id: "input_analysis_index" } }

host: displays the 2 messages, then calls mcp__<your-pipeline-key>__index_codebase(...)
host: submit_action_result(run_id, { kind: "tool_result", correlation_id: "input_analysis_index",
                                       success: true, data: { graph_path: "...", ... } })

  → envelope { messages: [<Codebase indexed: ...>, <Scope acceptable. Proceeding to clarification.>],
              action: { kind: "spawn_subagents", purpose: "draft", batch_id: "clarification_compose_1",
                        invocations: [ { invocation_id: "...", subagent_type: "zetetic-team-subagents:engineer",
                                         prompt: "<compose clarification question>", ... } ] } }

host: spawns the engineer subagent in parallel, collects the JSON-formatted question
host: submit_action_result(run_id, { kind: "subagent_batch_result", batch_id: "clarification_compose_1",
                                      responses: [ { invocation_id: "...", raw_text: "..." } ] })

  → envelope { messages: [],
              action: { kind: "ask_user", question_id: "clarification_answer",
                        header: "Round 1: <question>", ... } }

... loop continues until action.kind === "done" or action.kind === "failed"
```

The runner collects status messages (welcome banner, PRD-context-detected, codebase-indexed, scope-decision, ...) into `messages`. The host displays them and proceeds to `action`. There is never a "void" round-trip where the host has nothing to submit.
