| name | moplan:plan |
| description | Plan development work with a local-first interactive marimo notebook. Generates a YAML plan, populates slices and questions, runs dual-agent critique, opens the notebook for human review (approvals persist to .moplan/moplan.db), and finalizes the plan YAML as the deliverable. Use when asked to plan, create a development plan, or build a feature with design clarity first. |
Moplan: CRISPI Plan Skill
Use this skill when asked to plan development work, organize tasks, build a feature with human review before implementation, or structure a design before writing code.
Trigger keywords: create plan, development plan, plan tasks, organize work, task breakdown, crispi, interactive plan, plan with review, design and build, plan this feature, review before building, generate plan, scaffold plan
What This Skill Does
Generates a structured YAML plan, populates it with vertical slices, design questions with recommendations, and dual-agent critique. Opens the marimo notebook for interactive human review — approvals persist into .moplan/moplan.db on every click. On finalize, the YAML is rewritten with the approved state and handed back to the user as the deliverable. No work-tracking CLI is invoked. The YAML file is the output.
Architecture (moplan-native, no external tools required):
| Artifact | Location | Written by | Read by |
|---|
| Plan content | .moplan/plans/plan-<id>.yaml | the plan agent (Step 2) | the notebook + critique agents + humans |
| Approval state | .moplan/moplan.db (SQLite) | the notebook (on every click) | the notebook (on reload) + finalize flow |
| Static archive | .moplan/plans/plan-<id>.html | marimo export html (Step 6) | humans, PR reviewers |
The .moplan/ directory is bootstrapped automatically by MoplanStore (plugin/notebooks/moplan_store.py) the first time /plan runs. It walks up from CWD until it finds an existing .moplan/ or a git root, and creates the store at the git root. This behaviour is logged clearly to stderr — it never silently no-ops.
Step 0: Figure out what you are planning
Before launching the notebook, spend a few minutes with the user clarifying:
- What is being built or changed?
- Why now? What does "done" look like?
- Is this a greenfield plan, or are you planning changes to existing code?
If the request is vague, ask clarifying questions first. A vague plan is worse than no plan.
Step 1: Research (Parallel Agents)
Spawn research agents in a single message — do not proceed until both complete:
Agent(description="Understand current codebase state", subagent_type="plan-researcher",
prompt="Investigate [area]. Answer: current state, key files, existing patterns, constraints.")
Agent(description="Check for prior plans", subagent_type="Explore",
prompt="Search .moplan/plans/ for any existing plan YAMLs related to [area]. Read each
one and report: plan ID, title, status (draft/review/finalized), and a one-line summary.")
Research must answer:
- Current state of the codebase in this area
- Desired end state (from the request)
- Open design questions (choices that affect the architecture)
- Candidate vertical slices (end-to-end, not horizontal layers)
- Real dependencies between slices
- Prior art or existing patterns to leverage
Skip research only for: trivial changes (<10 lines), bug fixes with known root cause, documentation-only changes.
Step 2: Generate the plan YAML
Generate an 8-hex plan id (e.g. 9ae3f59b) and write the plan directly to:
.moplan/plans/plan-<id>.yaml
If .moplan/ does not yet exist, MoplanStore.discover() will bootstrap it the first time the notebook runs — but the agent can also create the directory up-front with mkdir -p .moplan/plans.
YAML schema
Every plan YAML MUST follow this exact structure:
meta:
id: plan-<hex8>
track_id: ""
title: "<plan title>"
description: >
One paragraph describing what this plan designs and why.
created_at: "YYYY-MM-DD"
status: draft
created_by: claude-opus
design:
problem: >
What is the current state? What's wrong? Why does this matter?
Include measurements, evidence, or user impact.
goals:
- "**Goal 1** — measurable outcome"
- "**Goal 2** — measurable outcome"
constraints:
- "Constraint 1 — why it exists"
- "Constraint 2 — why it exists"
approved: false
comment: ""
slices:
- id: feat-<hex8>
num: 1
title: "<slice title>"
what: >
What exactly will be implemented. Be specific — name functions,
files, APIs. An agent reading this should know what to build.
why: >
Why this slice exists. What problem does it solve? What breaks
without it? Link to the design goals.
files:
- path/to/file1.py
- path/to/file2.py
deps: []
done_when:
- "Acceptance criterion 1 — testable, concrete"
- "Acceptance criterion 2 — testable, concrete"
effort: S
risk: Low
tests: |
Unit: specific test description with expected input/output
Integration: specific integration test
Regression: what existing tests must still pass
approved: false
comment: ""
questions:
- id: q-<kebab-name>
text: "The design question in plain English?"
description: >
Context paragraph explaining WHY this question matters, what
tradeoffs are involved, and what evidence exists for each option.
recommended: <option-key>
options:
- key: option-a
label: "A: Short name — Full description and implications"
- key: option-b
label: "B: Short name — Full description and implications"
answer: null
critique: null
Quality requirements
Slices — ALL fields are MANDATORY:
what: Specific enough that an agent can implement it without asking questions
why: Links to a design goal or explains the business need
files: At least one file path
done_when: At least two concrete, testable acceptance criteria
tests: At least one unit test and one integration/regression test
effort: S (<50 lines), M (50-200 lines), L (>200 lines)
risk: Low (pure addition), Med (modifies existing), High (changes hot path or shared interfaces)
Questions — ALL fields are MANDATORY:
description: At least 2 sentences on context and tradeoffs
recommended: Agent MUST pick a default — the human overrides only where they disagree
options: At least 2 options, each with a descriptive label
Design — ALL subsections are MANDATORY:
problem: Evidence-based description of current state
goals: Measurable outcomes (not vague aspirations)
constraints: Real limitations that affect design choices
Step 3: Validate the YAML structure
Before launching the notebook, validate the YAML programmatically:
import yaml
plan = yaml.safe_load(open(".moplan/plans/plan-<id>.yaml").read())
assert plan["meta"]["id"].startswith("plan-")
assert plan["meta"]["status"] in ("draft", "review", "finalized")
assert plan.get("design", {}).get("problem")
assert plan.get("design", {}).get("goals")
assert plan.get("design", {}).get("constraints")
for s in plan.get("slices", []):
for field in ("what", "why", "files", "done_when", "tests", "effort", "risk"):
assert s.get(field), f"slice {s['num']} missing {field}"
for q in plan.get("questions", []):
assert q.get("description")
assert q.get("recommended")
assert len(q.get("options", [])) >= 2
print("plan structurally valid")
Fix any assertion failure before moving on.
Step 4: Critique (Parallel Agents)
Trigger: Always run critique for plans with >= 3 slices. Skip for trivial plans.
Dispatch two critique agents in parallel. Each reads the plan YAML at .moplan/plans/plan-<id>.yaml and produces structured findings. The agents themselves are defined in this plugin at plugin/agents/plan-critic.md and plugin/agents/feasibility-reviewer.md.
Agent(description="Design critique", subagent_type="plan-critic",
prompt="Read .moplan/plans/plan-<id>.yaml. Produce structured critique per the
template in your system prompt.")
Agent(description="Feasibility critique", subagent_type="feasibility-reviewer",
prompt="Read .moplan/plans/plan-<id>.yaml. Verify claims against the actual
codebase and produce structured critique per the template in your system prompt.")
What critique agents produce
Each agent emits text in this shape (parsed by the orchestrator into the plan YAML):
ASSUMPTIONS:
A1: [VERIFIED|PLAUSIBLE|UNVERIFIED|QUESTIONABLE|FALSIFIED] text | evidence
SLICE_ASSESSMENTS:
S1: assessment with specific concerns
RISKS:
R1: risk description | severity: Low/Medium/High | mitigation
GAPS:
- What the plan doesn't address
ALTERNATIVES:
- Different approaches worth considering
PRIOR_ART:
- Relevant existing solutions or patterns (feasibility critic only)
Writing critique into the YAML
The orchestrator parses both outputs and merges them into the critique section of the same YAML file, then re-validates. Use PyYAML's dump with sort_keys=False to preserve order.
Step 4b: Address critique findings (MANDATORY before review)
For each FALSIFIED assumption or HIGH severity risk, update the plan in place before handing it to the human:
- Update affected slice
what / done_when to incorporate the falsification or mitigation
- Add new constraints to
design.constraints when the finding affects the whole plan
- Add new questions when the critic surfaces a choice the plan didn't articulate
Re-write the YAML. Commit the revision (git add .moplan/plans/plan-<id>.yaml && git commit) so the human reviews the critique-informed version, not the stale original.
Skip Step 4b only if all assumptions are verified/plausible AND no HIGH-severity risks exist.
Step 5: Open the marimo notebook for human review (PAUSE HERE)
Launch the plan review notebook:
cd ${CLAUDE_PLUGIN_ROOT}/notebooks && uv run --script plan_notebook.py -- --plan plan-<id>
This is exactly what /plan plan-<id> does — the slash command and skill use the same invocation. uv run --script reads the PEP 723 header inside plan_notebook.py and installs marimo, pyyaml, anywidget, traitlets, and anthropic into an isolated environment on first run.
Tell the human:
Plan ready for review. I'll launch the notebook now.
In the marimo interface you will see:
1. Design Discussion — problem, goals, constraints. Approve or comment.
2. Slice Cards — one per slice. Click to expand; approve each one.
3. Open Questions — pick an option per question (defaults highlighted).
4. AI Critique — read the findings. Optionally chat with Claude in the
sidebar to propose amendments; accepted amendments are applied on
finalize.
5. Feedback Summary — progress bar showing completion.
6. Finalize — click when all sections are approved.
Every click persists to .moplan/moplan.db so you can close and
reopen the notebook without losing progress.
I will wait until you finalize before moving on.
STOP. Do not proceed until the human finalizes the plan.
Step 5b: Review flow — how approvals persist
This is the contract between the notebook surface and the MoplanStore. It exists so the agent running this skill knows exactly what the notebook is doing on the human's behalf — no notebook-native hand-waving.
Store discovery. On startup, the notebook calls MoplanStore.discover() which walks up from CWD until it finds .moplan/ or a git root. If neither is found, it bootstraps .moplan/ at the git root (or CWD if not a git repo) and logs the action to stderr. The store exposes store.root, store.db_path, store.plans_dir, and store.project_dir (the parent of .moplan/).
Plan selection. The notebook reads mo.cli_args().get("plan"). The value may be either a bare plan id (plan-9ae3f59b) or a full path. If empty, the notebook shows store.list_plans() as a dropdown. There is no MOPLAN_PLAN_ID environment variable — the CLI arg is the single source of truth.
Approve/reject for slices and design. Each click writes a row via store.persist_feedback(plan_id, section, action, value). Concretely:
- Approving the design section:
section="design", action="approve", value="true"
- Approving slice
feat-abc: section="slice", action="approve", value="true", question_id="feat-abc"
- Leaving a comment:
section="design" or section="slice", action="comment", value=<text>
Answering questions. section="question", action="answer", value=<option-key>, question_id=<q-id>. The recommended option is pre-selected; the human overrides only where they disagree.
Amendments from chat. The chat sidebar streams Claude via ClaudeChatBackend(plan_context=..., store=store, plan_id=...). Claude can emit AMEND slice-N: ... directives which the notebook parses and stores via store.persist_amendment(plan_id, amendment_dict, amendment_id). Each amendment starts as proposed. The human accepts or rejects via a dropdown beside the amendment; state transitions via store.update_amendment_status.
Finalize emits the plan artifact. When the human clicks Finalize, plan_persistence.finalize_plan() runs:
- Reads all current approvals, question answers, and accepted amendments via the store.
- Applies accepted amendments to the in-memory plan dict.
- Sets
meta.status = "finalized", design.approved = true, and slice.approved for each approved slice.
- Writes the updated YAML back to
.moplan/plans/plan-<id>.yaml.
- Runs
marimo export html to produce plan-<id>.html alongside the YAML as a static archive.
- Returns a markdown summary announcing "Plan Finalized" and pointing at the YAML file.
Integrate the finalized plan with your own task tracker. The deliverable of this skill is .moplan/plans/plan-<id>.yaml with meta.status=finalized, a full CRISPI design, approved slices, and seeded task rows in .moplan/moplan.db (see store.get_approvals() and the section="task" feedback rows). Downstream orchestration is out of scope for this skill.
Step 6: Read the finalized plan
After the notebook exits (the human has clicked Finalize):
import yaml
plan = yaml.safe_load(open(".moplan/plans/plan-<id>.yaml").read())
assert plan["meta"]["status"] == "finalized"
approved_slices = [s for s in plan["slices"] if s.get("approved")]
rejected_slices = [s for s in plan["slices"] if not s.get("approved")]
decisions = {q["id"]: q.get("answer") for q in plan.get("questions", [])}
Parse the results and report back to the user:
Plan finalized: .moplan/plans/plan-<id>.yaml
Approved slices (N of M):
feat-XXXX Slice 1 title
feat-XXXX Slice 2 title
Rejected slices:
Slice 3 title — excluded
Design decisions:
Q1 (Migration caching): schema-version (recommended, accepted)
Q2 (Error handling): metric-counter (overrode recommendation: structured-log)
Next steps are up to the caller — the plan YAML is the finalized deliverable.
Step 7: (Optional) Hand off to an executor
If the user has an execution workflow installed that consumes plan YAMLs, point it at the file:
.moplan/plans/plan-<id>.yaml
Moplan itself does not ship an executor — it ships the plan. Keeping those concerns separate is the point.
Agent type selection
| Agent | When to use |
|---|
plan-researcher | Step 1 — investigate codebase state, constraints, and prior art |
plan-enricher | Step 2 Path A — expand a skeleton plan section into full CRISPI |
plan-critic | Design review critique — challenge assumptions, identify gaps |
feasibility-reviewer | Feasibility critique — verify claims against actual code, find prior art |
All four agents are bundled with moplan. No external tooling is required.
Key rules
- Plans are YAML files at
.moplan/plans/ — not SQLite, not an external service
- Approval state lives in
.moplan/moplan.db — atomic SQLite writes, survives restarts
- All slice fields are mandatory — what, why, files, done_when, tests, effort, risk
- All questions need context — description + recommended option
- Design has three subsections — problem, goals, constraints
- Critique challenges assumptions — not just verifies code
- Finalize is explicit — the human clicks the button inside the notebook
- The YAML is the deliverable — moplan does not create work-tracker entries on its own
- No silent no-op — if the store can't find
.moplan/, it bootstraps and logs; it never fails silently