---
name: Product Manager Interview
slug: product-manager-interview
category: Writing
description: "Product Manager Interview guides users through a Socratic interview to produce a Product Requirements Document (PRD). Use it when you need to create a PRD or PM document, triggered by commands like 'ooo pm' or 'prd'."
github: "https://github.com/Q00/ouroboros/tree/main/skills/pm"
language: Python
stars: 5431
forks: 550
install: "npx degit https://github.com/Q00/ouroboros/tree/main/skills/pm ~/.claude/skills/pm"
installs_to: ~/.claude/skills/pm
source_path: skills/pm/SKILL.md
collection_size: 25
category_size: 1012
collection_url: "https://dirskills.com/collections/Q00/ouroboros"
added: 2026-08-16T07:00:01.704Z
last_synced: 2026-08-16T07:00:01.704Z
canonical_url: "https://dirskills.com/skills/product-manager-interview"
---

# Product Manager Interview

Product Manager Interview guides users through a Socratic interview to produce a Product Requirements Document (PRD). Use it when you need to create a PRD or PM document, triggered by commands like 'ooo pm' or 'prd'.

**Install:**

```bash
npx degit https://github.com/Q00/ouroboros/tree/main/skills/pm ~/.claude/skills/pm
```

## README

# /ouroboros:pm

PM-focused Socratic interview that produces a Product Requirements Document.

## Instructions

### Step 0: Version Check (runs before the PM interview)

Before starting the PM interview, check if a newer version is available:

```bash
# Fetch latest release tag from GitHub (timeout 3s to avoid blocking)
curl -s --max-time 3 https://api.github.com/repos/Q00/ouroboros/releases/latest | grep -o '"tag_name": "[^"]*"' | head -1
```

Compare the result with the current version in the active runtime's local plugin metadata (for Claude installs this is `.claude-plugin/plugin.json`).
- If a newer version exists, ask the user through the active runtime's `ask_user` capability:
  ```json
  {
    "questions": [{
      "question": "Ouroboros <latest> is available (current: <local>). Update before starting?",
      "header": "Update",
      "options": [
        {"label": "Update now", "description": "Update plugin to latest version (restart required to apply)"},
        {"label": "Skip, start PM interview", "description": "Continue with current version"}
      ],
      "multiSelect": false
    }]
  }
  ```
  - If "Update now":
    - On Claude-plugin installs only:
      1. Run `claude plugin marketplace update ouroboros` via the active runtime's `run_shell` capability (refresh marketplace index). If this fails, tell the user "⚠️ Marketplace refresh failed, continuing…" and proceed.
      2. Run `claude plugin update ouroboros@ouroboros` via the active runtime's `run_shell` capability (update plugin/skills). If this fails, inform the user and stop — do NOT proceed to the package-manager step.
    - On non-Claude runtimes, skip Claude plugin commands and proceed directly to the package-manager step for `ouroboros-ai`; do not require Claude-only commands or tools.
    3. Detect the user's Python package manager and upgrade the MCP server:
       - Check which tool installed `ouroboros-ai` by running these in order:
         - `uv tool list 2>/dev/null | grep "^ouroboros-ai "` → if found, use `uv tool upgrade ouroboros-ai`
         - `pipx list 2>/dev/null | grep "^  ouroboros-ai "` → if found, use `pipx upgrade ouroboros-ai`
         - Otherwise, print: "Also upgrade the MCP server: `pip install --upgrade ouroboros-ai`" (do NOT run pip automatically)
    4. Tell the user: "Updated! Restart your session to apply, then run `ooo pm` again."
  - If "Skip": proceed immediately.
- If versions match, the check fails (network error, timeout, rate limit 403/429), or parsing fails/returns empty: **silently skip** and proceed.

### Step 1: Load MCP Tool

```
tool discovery query: "+ouroboros pm_interview"
```

**CRITICAL — deferred-schema guard (prevents "Invalid tool parameters"):**
This is a multi-turn loop and each turn runs in a fresh tool context. A deferred
tool's schema loaded on one turn is NOT guaranteed to still be loaded on the next.
Calling `ouroboros_pm_interview` while its schema is unloaded in the **current**
turn makes the runtime reject it with **"Invalid tool parameters"** every message.
Therefore **re-run `tool discovery query: "+ouroboros pm_interview"` immediately before
EVERY `ouroboros_pm_interview` call** below (idempotent — a no-op if already
loaded). If the load ever returns no matching tool (and the tool is not already callable — an empty load for an already-exposed tool is an expected no-op, not absence), follow the not-found diagnosis
below instead of retrying the failing call.

If not found → fail closed without inspecting or mutating
`~/.claude/mcp.json`. Standalone Claude SDK setup requires MCP 1.x and cannot
activate the Ouroboros MCP 2 server with its configured backend. Explain:

```
The PM interview MCP tool is unavailable in this runtime.

Configure a supported CLI-backed host with:
  ouroboros setup --runtime <codex|opencode|kiro|copilot|hermes>

Then restart that host and retry ooo pm. Claude SDK profiles ([claude] and
[claude-sdk]) stay on MCP 1.x; the separate [mcp] server uses [claude-cli]. Do
not combine both MCP majors or add a direct Python fallback.
```

Stop.

### Step 2: Start Interview

```
Tool: ouroboros_pm_interview
Arguments:
  initial_context: <user's topic or idea>
  cwd: <current working directory>
```

**This response carries the first question, so Step 3 applies to it** — including
the fan-out in 3-A2. The first question is the one most likely to be answered
from memory, so it is the last one to skip evidence on.

### Step 3: Loop

Apply this to **every** MCP response that carries a question, including the one
Step 2 returned and any question re-shown on resume.

**A. Show alerts** (if present in `meta`):
- `meta.deferred_this_round` → print `[DEV → deferred] "question"`
- `meta.decide_later_this_round` → print `[DEV → decide-later] "question"`
- `meta.pending_reframe` → print `ℹ️ Reframed from technical question.`

**A2. Fan out the evidence lanes — required before you ask the user anything.**

**You do not look at the repositories yourself. Ever.** This skill has no
code-answer path: there is no step where you run Read/Glob/Grep or a docs MCP
to answer a PM question, and finding the answer quickly on your own is the
failure, not a shortcut past it. Evidence the PM cannot trace back to a lane is
evidence the record does not contain — it is not bound to the question, not
bounded by the roster, and not checked against the answer contract. This skill
is self-contained: everything you need is here and in the tool response, so do
not go looking for exploration rules in another skill's file.

**When `meta.question_advisory_subagents` is present you MUST fan out.** Show
the question text to the user first, then treat each entry as a spawn-ready
payload with `title`, `agent`, `prompt`, and `context`, and dispatch every
payload through your host's native subagent mechanism — Claude Code → **one
Task/Agent call per payload in one parallel batch**; Codex → one native Codex
subagent per payload; a runtime with no parallel primitive → process them
sequentially. Pass each payload's `prompt` unchanged rather than rewriting it.

This holds regardless of dispatch mode: **the payloads themselves are the spawn
signal.** `meta.question_advisory_host_action=spawn_subagents` is a reinforcing
cue, not a prerequisite. The only time you skip spawning is when the host has no
subagent primitive at all.

**Say what is running.** Same shape the regular interview uses: after the
question, set off by a divider, one line naming how many perspectives are running
and what they are — then what arrives when they finish.

```
---
While you answer this question, two perspectives are reviewing in parallel
(code context / data measurement). When they return I will put what they found
next to the question as grounds.
```

Two things differ from the interview's line, and both follow from this tool
having two lanes instead of six:

- Name the perspectives in the user's terms, not by lane id. `code_context` is
  an identifier for the fan-out, not something the reader needs.
- **End at "grounds", never at "options".** The interview can promise to
  organise the results into answer choices because it runs a lane that produces
  them. This tool does not, and a promise the synthesis cannot keep trains the
  user to expect the one thing the lanes must never hand them.

Write the line in the language the user is speaking.

**Do not go to step A3 or B while the lanes are still running.** Step B is where
you ask the user, and asking before the evidence arrives is the exact failure
this mechanism exists to prevent: the PM decides without the two things they
could not have looked up themselves. Waiting is for lanes still in flight: one
that came back empty, broke its contract, or could not be spawned has returned.

**Submitting results back.** Correlate by
`meta.question_advisory_result_correlation_key` (`context.lane_id`) and call
`ouroboros_submit_fanout_results` with `meta.question_advisory_fanout_id`,
passing `session_id` explicitly. Submit **every lane you hold**, not only the
new ones: a lane that ran and found nothing still submits its output, and a lane
you could not spawn at all is submitted as
`{ "key": <lane id>, "undispatched": true }` — the literal `true`, with no
`content` beside it. Never invent output for a lane you did not run; a
fabricated finding is worse than a missing one.

**Reading the reply.** With a `contract_id`, synthesize from the outputs you
hold. Without one, read `missing_required_keys` and `contract_violations`, then
resubmit once carrying every lane. Still without one — go to B with what
survived, or with none, saying in one line that the investigation did not come
back. The interview does not wait on this.

**Two lanes never reach the block, whatever the reply says.** Leave out a lane
named in `contract_violations`, and leave out a lane you submitted as
`undispatched` — a lane that did not run has an empty place, not a clear one,
and a reply can be accepted while one of them never ran. Where the block would
have carried that lane, write that it did not run.

There are two lanes and both are required: `code_context` and `data_context`.
A `code_context` lane that carries a policy returns `answer_prefix:
"[from-code]"` and a `user_confirmation_prompt` — that is a step, described in
A3 below, not the answer. `data_context` has no prefix at all: measurements are
shown beside the question and the answer is the user's own words. Never skip
asking the user because a lane answered clearly.

**Synthesize into the evidence block.** This is what
`synthesis_contract.output_shape = "evidence_beside_question"` means, and it is a
fixed shape so the same session twice looks the same twice. Print it immediately
above the question, then ask the question unchanged:

```
Evidence (examined: billing-api, storefront)

What the code does today
  · [billing-api] src/billing/lapse.py  — access continues to period end
  · [storefront]  src/checkout.ts       — access is revoked immediately
    ! These two repositories implement this differently.

Measured — active subscriptions by plan, last 90 days
  · standard 12,480 / premium 3,120
```

Write the block in the language the user is speaking. The labels above are
placeholders for its shape, not text to copy.

**What this block is not.** It carries no answer options, no recommendation, no
ranking, and no "therefore …" sentence. The moment it proposes an answer it has
stopped being evidence — that is the whole difference between this tool and the
regular interview, which does synthesize options. A PRD asks what the system
*should* do, and everything above says what it *does*.

Rules for building it:

- **Each claim keeps its repository.** Never merge two repositories' claims into
  one line, and never present a disagreement as one policy with an exception —
  flag it, as above. That contradiction is the most useful thing the PM can be
  shown, and it is the first thing a tidy summary destroys.
- **Always print the examined scope.** `examined` has one entry per repository
  the lane read, and every claim sits inside its own entry; "found nothing"
  across two of five repositories means something different from across all
  five. An entry whose `policy_claims` is empty was read and had nothing — say
  so. A repository with no entry was never opened, and must never be printed as
  clean.
- **Carry measurements as reported** — the lane's `metric`, its groups, its
  numbers. Do not re-scale, combine, or round; you did not run the read.
- **A no-op lane gets at most one line, and often none.** Read the reason as a
  statement about the lane rather than about the user's system:
  `not_a_policy_question` / `not_a_measurement` → print nothing, this question
  simply is not that kind. `no_repository_in_roster` /
  `roster_repository_not_readable` / `store_described_but_not_callable` → yours
  to handle (nothing registered, a path did not open, a store did not answer);
  do not relay any of them as "your system has no such policy/data".
- **Drop a finding the user has already answered past.** If they answered while
  the lanes were still running, do not re-open a settled decision with it. There
  is nowhere to put it: a finding takes the round it was fetched for, and that
  round is spent.
- **Evidence from outside the roster is a suggestion, not evidence.** It is
  rejected at submission, so relaying it as a finding would show the user
  something the record does not contain. Offer to register the repository
  instead, so the next question can be answered against it.

**A3. Record a confirmed finding — its own turn, before you prompt for an answer.**

Only when some `code_context` entry came back carrying a `policy_claims` item.
That answer also carries `answer_prefix: "[from-code]"` and a
`user_confirmation_prompt`, and there is no prefix that skips this step:
`[from-code][auto-confirmed]` is not a value the contract can hold, so a lane
cannot declare itself pre-confirmed.

1. Show the evidence block (A2) and ask the lane's `user_confirmation_prompt`
   through `AskUserQuestion`. Ask it as it is: the user is confirming that this
   is what the code does, not deciding what it should do.
2. If they say it is wrong, or they would rather just answer, **send nothing**
   and go to B. A question answered with no finding recorded is an accurate
   account of how that decision was made, not a degraded one.
3. If they confirm, send it as the answer with its prefix, composed from the
   `examined` entries so every claim keeps the repository it was read in:

```
Tool: ouroboros_pm_interview
Arguments:
  session_id: <meta.session_id>
  last_question: <meta.question>
  answer: |
    [from-code] billing-api src/billing/lapse.py: access continues to period end.
    [from-code] storefront src/checkout.ts: access is revoked immediately.
```

Send `meta.question`, never the response text — the text carries the fan-out
directive. Omitting it files the finding under a placeholder question, and
since the answer slot is withheld by design, the finding is then lost from
both slots.

4. The server records it as an **adopted fact**: the round is marked
   `observation`, requirement extraction reads a withheld-note in its place, and
   it does not count toward the decisions that complete the interview. The
   response carries the **next** question, generated with the finding in view —
   that is the question the user answers in their own words, from step 3-A.

**Why it takes the round rather than riding beside the answer.** It was built
the other way first, with a second parameter for findings. Two entrances meant
two sets of rules for one payload, and they stopped agreeing: the same class of
silent loss reappeared at a new address for six review rounds. One entrance is
what closed it, and it is what `ouroboros_interview` always did.

**What this does not license.** The finding is never sent unconfirmed to save a
turn. What holds if you do is downstream and weaker than the user's eyes: the
`[from-code]` prefix keeps it out of requirement extraction and out of the
completion count, so an unconfirmed forward costs the user a question turn and
puts nothing false in the PRD. That is a floor, not a permission.

**B. Show content + get user input** (once A2's lanes have returned):

The question text was already shown in A2 and the user may answer it at any
point; what waits here is your formal prompt, not the person.

Print the MCP content text to the user first, with the lane findings beside it.

Tell users they do not need to invent speculative answers. If a question is
unknown, stakeholder-dependent, too broad, or safer to decide later, route it
through the existing assumptions / decide-later / deferred mechanisms instead
of presenting it as a confirmed requirement.

Then check: does `meta.ask_user_question` exist?

- **YES** → Pass it directly to `AskUserQuestion`:
  ```
  AskUserQuestion(questions=[meta.ask_user_question])
  ```
  Do NOT modify it. Do NOT add options. Do NOT rephrase the question.

- **NO** → This is an interview question. Use `AskUserQuestion` with `meta.question`.
  - If `meta.skip_eligible == true`: add a skip option based on `meta.classification`:
    - `classification == "decide_later"` → add option `{"label": "Decide later", "description": "Skip — will be recorded as an open item in the PRD"}`
    - `classification == "deferred"` → add option `{"label": "Defer to dev", "description": "Skip — this technical decision will be deferred to the development phase"}`
  - Generate 2-3 suggested answers as the other options. Include a non-speculative
    uncertainty option when appropriate, such as `Not sure yet — record as an
    assumption or decide-later item`.

**C. Relay answer back:**

If the user chose "Decide later" → send `answer="[decide_later]"`.
If the user chose "Defer to dev" → send `answer="[deferred]"`.
Otherwise → send the user's answer through the Refine gate below.

**Refine gate — structure it, mark whose it is, then have the user confirm.**

Always structure the answer, including when the user only picked an option. The
text you send is MCP's only context for the next question and for what the PRD
records as decided, so a bare label loses everything around the decision. What
makes structuring safe is not restraint — it is the two things below.

**Mark whose each section is.** A section carrying the user's own words is
labelled `(user-stated)`. A section that is your reading of their answer is left
unmarked, and a reader can tell them apart at a glance:

```
[from-user][refined]
Decision: <what they decided, in their words>

Reasoning:
- <your reading of why, drawn from what they said in this session>

Constraints (user-stated):
- <constraints they stated>

Out of scope (user-stated):
- <what they put out of scope>
```

Omit any section you have nothing for. An empty `Constraints (user-stated)` is
better absent than filled with something plausible, and `Reasoning` drawn from
nothing they said is the failure this labelling exists to make visible.

**No codebase-context section, and no lane findings.** The regular interview
adds one, because there the main session inspects code itself. Here it does not.
A lane's finding is recorded by step A3, on its own turn, as an adopted fact.
Putting it in this payload would record it as part of the user's decision.

**Then confirm — this is the gate, and it is what licenses the structuring
above.** One `AskUserQuestion` before sending:

```json
{
  "questions": [{
    "question": "I structured your answer as follows before sending it:\n\n<payload>\n\nIs anything missing or misrepresented?",
    "header": "Refine — preserve your answer",
    "options": [
      {"label": "Send as-is", "description": "The structure captures my answer faithfully"},
      {"label": "Fix the reasoning", "description": "That is not why I decided it"},
      {"label": "Add to Constraints", "description": "I want to add a constraint I forgot"},
      {"label": "Let me rewrite it", "description": "I will restate the answer myself"}
    ],
    "multiSelect": false
  }]
}
```

`Fix the reasoning` is there because the unmarked section is the one you wrote.
Append `[refined]` only after this confirmation: an unconfirmed structure carries
your reading of the answer under the user's name, and the PRD cannot tell the
difference later.

**Send it as the answer, on the one parameter every answer uses.**

```
Tool: ouroboros_pm_interview
Arguments:
  session_id: <meta.session_id>
  last_question: <meta.question>
  <meta.response_param>: <the refined answer, or "[decide_later]" / "[deferred]">
```

Ignored while the server holds the question unanswered; required otherwise —
plugin mode never persists the child's questions, and an answer with no pending
question is refused without it.

There is no second parameter carrying findings. The tool has exactly the fields
the regular interview has, and a finding travels the same way any adopted fact
travels there — see **A3. Re
