CFN Decide Skill (MegaPlan DECISIONS Phase)
Purpose: Surface the tradeoff forks hidden in a spec and resolve them BEFORE the plan locks. Every meaningful fork (auth method, sync vs async, storage, build-vs-buy, data model, UX pattern, third-party vs native) gets named, classified, and either recommended or pushed to the user. Resolved decisions are written to the decision log so future plans stop re-litigating settled questions.
Phase: DECISIONS. DAG level 3. Runs in parallel with cfn-pseudo. Always active (not conditional). Consumes the spec, feeds cfn-data, cfn-arch, and /write-plan.
Closes the loop: the decision log is currently queried at plan entry (cfn-megaplan Step 0, cfn-plan-review Phase 1) but never written during planning. This phase is the writer.
When to Use
- Any MegaPlan run (orchestrator spawns it at L3, always).
- Standalone: a spec exists and you need to lock tradeoff decisions before writing the plan.
- Re-run when the spec changes a fork (new storage target, new auth requirement).
Skip only if the orchestrator's tier profile sets decide to skip (it does not in any shipped tier).
Input
planning/<slug>/SPEC_<slug>.md (required). Read it fully before extracting forks. If multiple SPEC_*.md exist, use the one whose slug matches; never regenerate the slug differently.
Optional context the orchestrator may pass: prior decision-log hits for these entities (inject as "prior resolution" rows, status RESOLVED, so you do not re-ask a settled fork).
Output
planning/<slug>/DECISIONS_<slug>.md— the decision register (table + any alternatives panel).- Structured records written to the decision-log SQLite store via
record.shfor every RESOLVED decision (see Phase 5).
Tier behavior (from orchestrator Directive)
| Tier | Directive | Behavior |
|---|---|---|
| mvp | light |
Blocking forks ONLY. Drop the alternatives panel and all non-blocking forks. Register holds blocking rows only. |
| beta | full |
All forks, blocking and non-blocking. No alternatives panel. |
| enterprise | full + extras [alternatives_panel] |
All forks PLUS Phase 4 alternatives panel on the single biggest design fork. |
The orchestrator passes Directive: and Include extras:. Honor them. drops may list alternatives_panel and non_blocking_forks for light.
Global rule (non-negotiable)
Decisions surfaced via AskUserQuestion (the orchestrator makes the actual call; you produce the payloads) MUST:
- Be ONE decision per question. Never bundle two forks into one question.
- Use plain English. No jargon, no internal acronyms in the question or options.
- Flag the best long-term-maintenance option
(Recommended). Best for maintenance, not the quick fix. - Only surface forks with MEANINGFUL consequence. Do not ask "should I commit" or "tabs or spaces".
- Order of implementation is NOT a user decision. You decide it. Never surface it.
Protocol
Phase 1: Fork extraction
Scan the spec for decision points. Walk these categories explicitly:
| Category | Look for in spec | Tie-in |
|---|---|---|
| Auth method | login, session, token, role, permission FRs/NFRs | floor: auth_boundaries |
| Sync vs async | "immediately", queue, background job, webhook, long-running | concurrency design (cfn-data/arch) |
| Storage choice | persisted state, table, cache, blob, file | cfn-data |
| Build vs buy / lib vs hand-roll | parsing, crypto, date math, validation, any "system SHALL compute" | cfn-arch build ladder (YAGNI -> reuse -> stdlib -> native -> installed dep -> one line -> new dep last) |
| Data model | entity shape, relationships, normalization, enum vs lookup | cfn-data |
| UX pattern | how a user performs an action, control choice, flow shape | cfn-ux affordance map |
| Third-party vs native | external API vs platform primitive, vendor lock-in | build ladder + cost/provider rules |
For each fork found, classify:
- BLOCKING — the plan cannot proceed without resolving it. Different choices produce materially different architecture, data model, or component contracts. cfn-data / cfn-arch would build the wrong thing if left open.
- non-blocking — a default is safe; the choice is reversible cheaply later. Record it RESOLVED with your recommendation as the rationale; do not ask the user.
Blocking litmus (mechanical). Classify BLOCKING iff reversing the choice later would require ANY of:
- a schema migration,
- a change to an API contract another component consumes,
- edits in 3+ files,
- re-doing work from a downstream phase (data/arch/ux).
If none apply, it is non-blocking: pick the recommendation and RESOLVE it yourself.
Per-category completeness. Emit one register row per category in the Phase 1 table even when the row is no fork found. A category with no row means it was not walked, and the register is rejected.
Zero-fork sanity check. A spec with db: yes or frontend: yes build flags that yields zero forks is suspicious; re-walk the category table before returning.
Security carve-out: a build-vs-buy fork touching crypto, auth, token/JWT parsing, or input sanitization is NOT a real fork. The audited dependency wins. Record it RESOLVED, rationale "security floor, never hand-roll", do not surface.
Phase 2: Decision register
Artifact location. Every artifact of one plan lives in that plan's own directory, planning/<slug>/. Under /cfn-megaplan, /cfn-megaplan-lite, or /cfn-spa-plan the orchestrator hands you the exact path plus a Plan dir: line — write there, and read the input paths it gives you verbatim. Invoked standalone, read with $HOME/.claude/skills/cfn-megaplan/lib/plan-paths.sh resolve <slug> <basename> (per-plan dir first, legacy flat planning/ second) and write to planning/<slug>/. Never split one plan across two locations.
Write the register as a table in planning/<slug>/DECISIONS_<slug>.md:
| id | decision | options | tradeoff (plain English) | recommendation | status | rationale |
|---|
Rules:
id=D-1,D-2, ... stable across re-runs.status=OPEN(blocking, needs user) orRESOLVED(you decided, or user answered, or prior-log hit).- Every RESOLVED row MUST have a non-empty
rationale. A resolution with no rationale is rejected. recommendationnames one option and why it is best for long-term maintenance.- Blocking forks start
OPEN. Non-blocking forks startRESOLVEDwith recommendation = choice. - In
lighttier, non-blocking rows are dropped entirely (not written).
Phase 3: User surfacing (produce AskUserQuestion payloads)
For every OPEN (blocking) decision, emit one AskUserQuestion payload. The orchestrator batches and calls them; you only build the payloads.
Payload per decision:
question: "<plain-English fork, no jargon>"
header: "<short label, e.g. Storage>"
options:
- label: "<option A>" description: "<plain-English consequence>"
- label: "<option B> (Recommended)" description: "<why best long-term>"
- label: "<option C>" description: "<consequence>"
One decision per payload. The (Recommended) flag goes on the best long-term-maintenance option. After the orchestrator returns answers, flip each answered row to RESOLVED, set rationale to "user chose : ".
If a fork is blocking but you have a genuinely dominant recommendation with no real tradeoff (escape hatch), resolve it yourself, mark RESOLVED, state the rationale, and do not surface. Reserve user questions for forks with real consequence.
Phase 4: Alternatives panel (enterprise extra only)
Run only when Include extras: contains alternatives_panel. Pick the SINGLE biggest design fork (widest blast radius across data + arch). Lay out 2-3 whole approaches end to end. Score each:
| Approach | Effort (1-5) | Risk (1-5) | Maintainability (1-5) | Net | Notes |
|---|
- Effort/Risk: lower is better. Maintainability: higher is better.
Net= maintainability - risk - effort (rank only, not gospel). - Recommend one approach. Then graft the best ideas from the runners-up into it explicitly ("take X's retry model, keep Y's schema").
- The recommended approach becomes (or updates) the corresponding
D-nrow in the register.
Phase 5: Decision-log write (closes the loop)
Two sinks, both written. They serve different readers and neither is optional.
Sink 1, repo artifact (planning/<slug>/DECISIONS_<slug>.md): produced in Phase 2, updated by Phase 4 when the alternatives panel runs. Versioned with the code, survives forever, human-readable. This is the canonical register.
Sink 2 — structured decision store (decision-log SQLite): for EVERY RESOLVED decision, call the structured write API so future plans query settled forks across sessions, separate from conversation noise.
DL="${HOME}/.claude/skills/decision-log"
# one call per resolved decision; --blocking when the fork blocked the build
"$DL/record.sh" --slug "<slug>" --id "D-1" \
--title "<decision, one line>" \
--chosen "<chosen option>" \
--rationale "<why, plain English>" \
--alternatives "<rejected options + one-line why-not each>" \
[--blocking] [--supersede D-0]
# project + timestamp auto-derive (git toplevel basename, UTC now).
Rules:
- Write AFTER user answers return, so user-chosen options are captured, not recommendations.
--blockingfor forks that were pushed to the user (the OPEN rows). Omit for self-resolved.- Re-running is safe:
record.shupserts on(project, slug, decision_id), no duplicates. - If a later plan reverses a decision, the new record uses
--supersede <old-id>so the old one is markedsuperseded, not deleted (audit trail preserved).
Readers: cfn-megaplan Step 0 and cfn-plan-review Phase 1 query both — the FTS message index (conversation context) AND decisions.sh search "<entities>" / decisions.sh list --slug <slug> (the structured register). Settled forks surface as RESOLVED prior-resolution rows so they are not re-asked.
Concrete example register
planning/realtime_notifications/DECISIONS_realtime_notifications.md (beta tier, full):
| id | decision | options | tradeoff | recommendation | status | rationale |
|---|---|---|---|---|---|---|
| D-1 | How are notifications delivered to the browser | WebSocket; Server-Sent Events; short polling | WebSocket is two-way but needs sticky sessions and more infra. SSE is one-way, simpler, reconnects natively. Polling is trivial but laggy and chatty. | SSE (Recommended) | OPEN | needs user: infra appetite vs latency |
| D-2 | Where unread-state is stored | Postgres column; Redis; in-memory | Postgres survives restarts and is queryable. Redis is fast but another dependency. In-memory loses state on deploy. | Postgres | RESOLVED | non-blocking, spec already requires a notifications table; reuse it |
| D-3 | Markdown sanitizer for notification body | hand-roll regex; dompurify |
hand-roll risks XSS holes | dompurify |
RESOLVED | security floor, never hand-roll sanitization |
| D-4 | Delivery timing | send inline on event; queue + worker | inline is simple but blocks the request and drops on crash | queue + worker | OPEN | needs user: added worker infra vs reliability |
Footer: Log: planning/<slug>/DECISIONS_<slug>.md (repo) + decision-log SQLite via record.sh (RESOLVED rows D-2, D-3). Query: decisions.sh list --slug <slug>.
AskUserQuestion payloads produced: D-1 and D-4 only (the two OPEN rows), one question each, SSE and queue flagged (Recommended).
Return to orchestrator
artifact: planning/<slug>/DECISIONS_<slug>.md
summary (3 lines):
- <N> forks extracted (<b> blocking, <n> non-blocking); <r> resolved, <o> open
- alternatives panel: <ran on D-x | skipped (tier)>
- decision log: <k> RESOLVED records written via record.sh (SQLite) + register in planning/<slug>/DECISIONS_<slug>.md
needs user: [D-1, D-4] # the OPEN blocking decisions, with their AskUserQuestion payloads attached
If needs user is non-empty, the orchestrator must call AskUserQuestion and feed answers back before advancing past L3.
Anti-Patterns
- Bundling decisions — two forks in one question. One decision per question, always.
- Asking trivial decisions — "should I commit", "tabs vs spaces", order of implementation. Only meaningful forks reach the user. You decide implementation order.
- Deciding silently on a meaningful fork — picking WebSocket vs SSE yourself when it materially changes infra. Blocking forks go to the user.
- Never writing the log — leaving the loop open. Every RESOLVED decision is appended. This is the whole point of the phase.
- RESOLVED with no rationale — a resolution must say why. Empty rationale is rejected.
- Hand-rolling security to avoid a dependency — crypto/auth/JWT/sanitization forks are not forks. The audited dep wins, RESOLVED.
- Running the alternatives panel outside enterprise — it is an enterprise extra. Do not add it to mvp/beta.
- Flagging the quick-fix option as Recommended — the flag is for long-term maintenance, not speed.
Related
- Upstream:
cfn-spec(produces the SPEC input) - Parallel:
cfn-pseudo(same DAG level) - Downstream:
cfn-data,cfn-arch(consume resolved decisions),/write-plan - Build ladder reference:
cfn-arch(build-vs-buy fork classification) - Log consumers:
cfn-plan-reviewPhase 1,cfn-megaplanStep 0 (query prior resolutions) - Orchestrator:
cfn-megaplan(spawns this at L3) - Backlog:
docs/PLANNING_PIPELINE_GAPS.md(G04, G24, decision-log write loop)