| name | prd-spec-generator |
| version | 3.2.0 |
| description | Action-driven PRD generation. The MCP server runs a stateless 9-step pipeline reducer; 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 (v3.1.0) — Dispatcher Protocol
You (the host) drive a loop:
- Call
start_pipeline → receive an envelope with { run_id, messages, action, ... }.
- Display every entry in
messages to the user.
- Execute
action per the dispatch table below.
- Call
submit_action_result(run_id, result) → receive next envelope.
- 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:
{
"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 kinds |
| 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:
{
"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:
{
"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:
{
"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):
{
"kind": "tool_result",
"correlation_id": "<echo action.correlation_id verbatim>",
"success": true,
"data": <the JSON the tool returned, unmodified>
}
Submit (failure):
{
"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_cortex_cortex__<tool_name> or mcp__cortex__<tool_name> per your .mcp.json.
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:
{
"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:
{
"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:
{ "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:
{ "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:
{ "kind": "done", "summary": "...", "artifacts": ["overview: passed", ...] }
Execute: Display messages (if any), then summary, then the artifacts list. Stop the loop.
failed
Action shape:
{ "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
-
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.
-
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.
-
Always issue spawn_subagents invocations in parallel. One message; many Agent tool calls. (Performance, not correctness — but production-relevant.)
-
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.
-
Stop the loop on done or failed. Do not re-call submit_action_result afterwards.
-
One pipeline = one run_id. Do not interleave actions from different runs.
-
Never call submit_action_result concurrently for the same run_id. The runner's in-flight guard rejects with isError: true.
-
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:
- Clarification answers. The Q&A turns in
state.clarifications — these are the user's load-bearing decisions. Tag: decision, feature:<feature_description>.
- 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>.
- 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.
- 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.