A dedicated, read-only planner persona for pi. It produces a Ralph-format prd.json that downstream executor agents (e.g. ralph.sh) consume. This is the plan half of plan-and-execute — the execute half stays in Ralph, untouched.
pi --plan # enter planner, then just chat
pi --plan "add a notifications system" # optional: seed the first turnFrom there you describe your feature, the agent clarifies the decision points interactively (and records every decision as you chat). Run /emit-plan at any time: it generates the PRD from the chat history — if no draft exists yet the planner drafts one from the conversation, it is validated against the full compile checklist (failures are bounced back to the planner for correction), and tasks/prd-<branch>.md is written. Review the markdown, then compile it into the executor handoff with /compile-prd.
One chat session produces one PRD — a bunch of small, ordered user stories. /explore is an optional deep-dive helper, never a prerequisite.
| File | Role |
|---|---|
index.ts |
Extension entry: --plan flag, persona injection, tool/command gating, /explore, /decisions, /emit-plan, /compile-prd, ask_decision + record_decision tools, UI status, decision persistence |
prompts.ts |
PLANNER_PERSONA system prompt (clarify-then-draft workflow, compile self-check, example artifacts), exploreKickoff() message generator |
schema.ts |
Dependency-free PRD contract: types (Prd, UserStory, Decision), validatePlan() (Ralph checklist), extractPrdMarkdown() (chat draft → markdown), parsePrdMarkdown() (markdown → Prd), renderPrdMarkdown() (Prd → tasks/prd-<branch>.md), toPrdJson() (Prd → prd.json) |
schema.test.ts |
8 unit tests covering the full /emit-plan pipeline |
bash-allowlist.ts |
isSafeCommand() — blocks destructive bash patterns, allows read-only commands only |
User describes feature
│
▼
Interactive clarification (every turn)
• agent presents option forks via ask_decision → interactive picker → recorded
• free-form commitments in chat → record_decision → recorded
• /explore (OPTIONAL) → structured briefing: Interpretation, Options,
Implications, Impact, Decisions needed
│
▼
┌─ /emit-plan ─┐ USER command (not the agent) — runnable at ANY time
│ find draft │ → scans assistant messages for # PRD: block;
│ │ if none exists, asks the planner to DRAFT it from the
│ │ chat history and auto-resumes when it responds
│ parse + │ → parsePrdMarkdown() → Prd
│ validate │ → validatePlan() — must pass ALL compile conditions;
│ │ on failure, errors are sent back to the planner,
│ │ it redrafts, the emit auto-retries (bounded)
│ write │ → tasks/prd-<branch>.md (human review gate)
└───────────────┘
│
▼
User reviews (and optionally hand-edits) tasks/prd-<branch>.md
│
▼
┌─ /compile-prd ─┐ USER command
│ re-parse │ → tasks/prd-<branch>.md → Prd
│ re-validate │ → validatePlan() (same checklist, always passes)
│ write │ → prd.json (Ralph handoff)
└─────────────────┘
│
▼
ralph.sh reads prd.json, executes stories, flips passes: true
When you enter planner mode, the agent adopts a read-only persona. You just chat: describe the feature, react to questions, commit to directions.
- Option forks are interactive — when the agent hits a decision point with concrete options, it calls
ask_decision, which pops an interactive picker (your options plus a free-text "Other"). Your pick is recorded automatically. - Free-form commitments are recorded too — when you commit to a direction in plain chat, the agent calls
record_decisionimmediately. /decisions— view all recorded decisions at any time./explore— optional deep-dive helper, never required:/explore— structured briefing: Interpretation, Options, Implications, Impact/changes, Decisions needed/explore <focus>— same, focused on a specific concern
Recorded decisions are flushed into the PRD's Decisions from Exploration section when you run /emit-plan.
Once decisions are recorded, you ask the agent to draft the plan. The agent converts the agreed direction into a Ralph-format PRD and presents it as a fenced ```markdown code block in chat. It follows an exact template:
```markdown
# PRD: <project name>
**Branch:** `ralph/<kebab-case-branch>`
<one-paragraph description>
## User Stories
### US-001: <title>
**Description:** As a <user>, I want <feature> so that <benefit>
**Acceptance Criteria:**
- [ ] <verifiable criterion>
- [ ] Typecheck passes
```Story order = priority order. Every story must include "Typecheck passes". UI stories must include "Verify in browser using dev-browser skill". The planner is read-only — it cannot write files.
Emitting is an explicit user command, not something the agent does. A pre-existing draft is not required — /emit-plan generates the PRD from whatever is in the chat history:
/emit-plan —
- Scans the conversation for the most recent assistant message containing a
# PRD:markdown block. - If none exists, asks the planner to draft the PRD from the conversation so far (an
[EMIT-PLAN]message). When the planner responds, the emit resumes automatically (agent_endhook, bounded retries). - Parses the draft into a structured
PrdviaparsePrdMarkdown(). - Runs
validatePlan()— the exact same Ralph checklist that/compile-prdenforces. If any compile condition fails (missingralph/prefix, no stories, duplicate IDs, missing "Typecheck passes",passes !== false, etc.), nothing is written and the error list is sent back to the planner, which redrafts — the emit then auto-retries. - On success, renders and writes
tasks/prd-<branch>.md— a normalized markdown document including project metadata, recorded decisions, ordered user stories, and any validation warnings.
If you want changes, ask the agent to revise the draft, then run /emit-plan again. No decisions are required to emit — if none were recorded, the Decisions section simply reads _none_.
- Review
tasks/prd-<branch>.md. Edit by hand if needed. /compile-prd— compile the reviewed markdown intoprd.json:/compile-prd <branch>— compiletasks/prd-<branch>.md/compile-prd path/to/file.md— compile a specific file/compile-prd— pick interactively from alltasks/prd-*.mdfiles
The command re-parses the markdown, re-runs schema + Ralph-checklist validation, asks for confirmation, then writes prd.json. Because it parses the file on disk, any manual edits you made are picked up.
| File | Produced by | Purpose |
|---|---|---|
tasks/prd-<branch>.md |
/emit-plan |
Human-readable PRD — the review gate |
prd.json |
/compile-prd |
Machine handoff consumed by ralph.sh |
Adjust PRD_JSON_PATH / prdMdPath in index.ts if your Ralph setup expects the JSON elsewhere.
The validatePlan() function in schema.ts enforces Ralph's story-quality checklist on every /emit-plan and /compile-prd — a plan that cannot compile cannot be emitted:
branchNamemust start withralph/- At least one user story required
- No duplicate story IDs
passesmust befalseon emit (executor flips it)- Every story must have at least one acceptance criterion
- Every story must include
Typecheck passesas a criterion
- ID should follow
US-001pattern notesshould be empty on emit- UI-related stories should include
Verify in browser using dev-browser skill - Duplicate priorities (ordering may be ambiguous)
Priority follows dependency order: schema/migrations → backend logic → UI → aggregate views. Stories are sorted by priority in the output.
| Guardrail | Mechanism |
|---|---|
| Read-only | write and edit tools are blocked in planner mode — the agent can only draft the PRD in chat |
| Safe bash only | bash-allowlist.ts matches destructive patterns (rm, mv, npm install, git commit, etc.) and blocks them; only read-only commands pass |
| Human emits, not the agent | The PRD only lands on disk when the USER runs /emit-plan — the agent never calls a write tool |
| Compile conditions gate emission | /emit-plan runs the exact same validatePlan() checklist as /compile-prd; drafts with errors are bounced back to the planner for correction, nothing is written |
| Bounded auto-retry | the draft-then-emit loop on agent_end has a fixed retry budget (EMIT_ATTEMPTS) — it can never ping-pong with the planner indefinitely |
| Human review always | /emit-plan only writes tasks/prd-<branch>.md; prd.json is written exclusively by /compile-prd after review |
| Decision persistence | Decisions are saved to the session via pi.appendEntry and restored on session_start / session_tree, so resuming a session keeps its grounding |
When planner mode is active, the extension provides:
- Status bar — shows
📋 planner · N decision(s)once decisions exist, or a muted📋 plannerbefore any - Widget —
plan-decisionspanel listing all recorded decisions - Interactive pickers —
ask_decisionrenders decision options as actx.ui.selectdialog (with a free-text "Other") - Notifications — info/warning/error messages for guidance and validation results
--plan is a persona, not a mode you flip mid-session. The planner is a different agent with a read-only contract and a single deliverable (prd.json). Booting into it keeps planning sessions clean and isolated from execution.
Unlike Ralph's PRD clarifying questions (which extract intent from the user), /explore flows the other way: the agent builds the user's understanding of their own request. It's an interactive advisory loop — the agent investigates the codebase and helps you see options, implications, and decisions you may not have considered. It is entirely optional: normal chat already records decisions and is enough to draft and emit a PRD.
# PRD: Notification System
**Branch:** `ralph/notification-system`
Add in-app notifications so users see activity on their content.
## Decisions from Exploration
- **Delivery mechanism?** → In-app only (no email) _(scope kept small)_
## User Stories
### US-001: Notification schema
**Description:** As a developer, I want a notifications table so that events can be stored
**Acceptance Criteria:**
- [ ] Migration creates notifications table
- [ ] Typecheck passes
### US-002: Notification bell UI
**Description:** As a user, I want a bell icon with unread count so that I notice new activity
**Acceptance Criteria:**
- [ ] Bell shows unread count
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
---
_Review this document, edit by hand if needed, then run `/compile-prd` to produce `prd.json` for the executor._{
"project": "Notification System",
"branchName": "ralph/notification-system",
"description": "Add in-app notifications so users see activity on their content.",
"userStories": [
{
"id": "US-001",
"title": "Notification schema",
"description": "As a developer, I want a notifications table so that events can be stored",
"acceptanceCriteria": ["Migration creates notifications table", "Typecheck passes"],
"priority": 1,
"passes": false,
"notes": ""
},
{
"id": "US-002",
"title": "Notification bell UI",
"description": "As a user, I want a bell icon with unread count so that I notice new activity",
"acceptanceCriteria": [
"Bell shows unread count",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 2,
"passes": false,
"notes": ""
}
]
}schema.ts is dependency-free (no TypeBox, no pi imports), so the entire emit pipeline is unit-testable directly with Node's built-in test runner:
node --test schema.test.ts8 tests, all passing:
| Test | What it covers |
|---|---|
extractPrdMarkdown — fenced block |
Pulls the PRD out of a ```markdown fenced chat draft |
extractPrdMarkdown — unfenced fallback |
Falls back to an unfenced # PRD: heading |
extractPrdMarkdown — no PRD |
Returns null when no PRD is present in the text |
parsePrdMarkdown round-trip |
Reconstructs the full Ralph contract (ids, priorities, criteria) |
| Valid draft → no errors | A well-formed draft meets all compile conditions |
| Compile conditions fail | Catches: missing Typecheck, bad branch prefix, no stories, duplicate IDs |
| Full pipeline | chat draft → extract → parse → validate → render tasks/prd-<branch>.md → re-parse → prd.json matching Ralph's contract |
| Priority ordering | toPrdJson() sorts stories by priority regardless of input order |
# project-local
mkdir -p .pi/extensions && cp -r plan-mode .pi/extensions/
# global
cp -r plan-mode ~/.pi/agent/extensions/
# quick test
pi -e ./plan-mode/index.ts --plan "your feature"The planner and executor share exactly one contract:
{
"project": "string",
"branchName": "ralph/kebab-case",
"description": "string",
"userStories": [
{
"id": "US-001",
"title": "Short descriptive name",
"description": "As a [user], I want [feature] so that [benefit]",
"acceptanceCriteria": ["Typecheck passes", "..."],
"priority": 1,
"passes": false,
"notes": ""
}
]
}The executor (ralph.sh) reads prd.json, processes stories in priority order, flips passes to true as it completes them, and fills in notes.