Fiery crucible smelter with question-mark glyphs crystallizing into a blueprint shape, the idea-smelting station
Chapter 5

Crucible — The Idea Smelter

The Crucible is the intake interview for Plan Forge. You bring a rough idea ("add user profile editing") and the smelter walks you through 4–12 questions, then writes out a complete Phase plan that's ready for the Forge to execute.

Why an interview? Most plans fail because the spec is too vague. Crucible refuses to write a plan until you've answered a fixed set of CRITICAL_FIELDS, build command, test command, scope, validation gates, forbidden actions, and rollback steps. No TBDs allowed. The Forge can only execute plans that pass this gate.

How It Works

The Crucible has three sizes (called lanes) that scale the interview to the size of the change:

  • tweak, 4 questions. For version bumps, config tweaks, doc edits, small fixes.
  • feature, 7 questions. The default. For new endpoints, new tools, new services with a handful of slices.
  • full, about 12 questions. For architectural changes that touch three or more top-level modules.

You can pick a lane explicitly, or let Crucible infer one from your raw idea (it looks for keywords like "bump" or "refactor subsystem"). When the interview ends, Crucible writes docs/plans/Phase-NN.md and hands it off to the Plan Hardener (Step 2 of the pipeline).

Why Smelt Before Hardening?

The Plan Hardener (Step 2) assumes you already know what you want to build. Crucible exists because most of the time, you don't, not precisely enough for a hardened plan. The smelter enforces three things:

  • A stable phase name, an atomic claim prevents two parallel agents from both creating "Phase 17".
  • A minimum set of answered questions, lane-scaled (4 / 7 / ~12) so trivial tweaks don't get full-feature ceremony.
  • A provenance stamp, every plan carries crucibleId, lane, and source in its frontmatter so downstream gates can audit how it got here.
One enforcement detail to know up front: every plan under docs/plans/Phase-*.md must carry a crucibleId. Plans get one of three ways: by finishing a smelt, by using --manual-import for hand-authored or Spec Kit imports, or by the grandfather migration that runs once when you first upgrade. Plans without a crucibleId are rejected at run time.

The Three Lanes

Crucible lane decision tree: raw idea enters forge_crucible_submit. If lane is explicitly provided, use it. Otherwise, keyword inference matches: 'bump'/'fix typo'/'update dep' -> tweak (4 questions, scope+build+test+forbidden), 'new endpoint'/'new service' -> feature (7 questions, all 6 CRITICAL_FIELDS), 'refactor subsystem'/'architectural shift' -> full (~12 questions, feature plus architecture/integration/risk/dep matrix). Default fallback if no keyword match: feature lane. Lane drives interview depth and which CRITICAL_FIELDS are enforced at finalize.

Crucible scales its interview to the size of the change. Pick (or let the server infer) one of:

tweak
4 questions

Version bumps, config flag flips, doc edits, small bug fixes. Inferred when the raw idea mentions "bump", "fix typo", "update dep". Includes a forbidden-actions question so even tiny changes declare what they won't touch.

feature
7 questions

Default lane. New endpoint, new tool, new UI section, new service with a handful of slices.

full
~12 questions

Architectural shifts, subsystem introductions, anything that touches three or more top-level modules.

The Interview Loop

Crucible streams one question at a time. You answer, it writes the answer to the smelt's JSONL record, then it computes the next question. Six MCP tools drive the loop:

forge_crucible_submit  { rawIdea, lane? source? }      → { id, firstQuestion }
forge_crucible_ask     { id, answer, questionId? }      → { nextQuestion | done: true }
forge_crucible_preview { id }                            → { draft, criticalGaps[] }
forge_crucible_finalize{ id, overwrite? }                → { phaseName, planPath, hardenerHandoff }
forge_crucible_list    { status? }                       → [ smelts … ]
forge_crucible_abandon { id, reason? }                   → { ok }

Optional questionId on ask: pass the question id you're answering. If it doesn't match the server's pending question id, the call returns 409 with ASK_QUESTION_MISMATCH and an { expected, got } payload. Multi-turn LLM clients that fall out of sync get a loud failure instead of silent answer corruption.

Build/test command inference: when the build-command or test-command questions come up, the interview pre-fills suggestions via inferRepoCommands, it inspects package.json scripts, *.csproj, pyproject.toml, Cargo.toml, go.mod, etc. You usually just confirm.

Finalize writes docs/plans/<phaseName>.md with the answer-derived draft and emits crucible-handoff-to-hardener on the hub so the dashboard (and downstream agents) can pick up the plan for Step 2.

CRITICAL_FIELDS Gate v2.82.1+

Crucible refuses to finalize a smelt with placeholder TBDs. The gate checks six fields; any unresolved field is a hard block:

CRITICAL_FIELDS gate flow: forge_crucible_finalize call enters the gate which checks six required fields (build-command, test-command, scope, validation-gates, forbidden-actions, rollback). If any field is missing, returns 409 with CRITICAL_FIELDS_MISSING and criticalGaps[]. If plan file already exists, returns 409 with PLAN_ALREADY_EXISTS. Otherwise writes Phase-NN.md and emits crucible-handoff event.
Figure 5-1. CRITICAL_FIELDS gate flow
FieldLane(s)What it locks down
build-commandallExact build command the orchestrator runs as a per-slice gate. Inferred from repo if possible.
test-commandallExact test command. Inferred from repo if possible.
scopeallPlan-level + per-slice Files in scope
validation-gatesfeature, fullAt least one executable gate per slice
forbidden-actionstweak (4), feature (7)Concrete file patterns or named actions that are out-of-bounds
rollbackfeature, fullHow to undo the change cleanly

Finalize behavior with gaps

If any field is missing, forge_crucible_finalize returns:

  • MCP tool: { ok: false, code: "CRITICAL_FIELDS_MISSING", criticalGaps: [{ field, reason, hint }, …] }
  • REST: 409 Conflict with { criticalGaps[], unresolvedFields[], hint: "call /api/crucible/preview" }

The preview tool returns the same criticalGaps[] structure without trying to write a plan, so LLM agents can self-correct.

Plan-already-exists protection

If docs/plans/<phaseName>.md already exists and is non-empty, finalize refuses to overwrite a hand-authored plan:

  • A side-by-side <phaseName>.crucible-draft.md is written so the smelt's draft is preserved
  • MCP tool: { ok: false, code: "PLAN_ALREADY_EXISTS", phaseName, planPath, draftPath }
  • REST: 409 Conflict with the same payload
  • Pass overwrite: true on either surface to bypass, the previous plan moves to <phaseName>.replaced-<timestamp>.md
Don't overwrite without reading the existing plan. The gate exists because hand-authored plans frequently encode constraints the smelter's questions can't elicit. The draft file lets you cherry-pick what's new from the smelt before destroying the original.

Recursion Guardrails

A smelt can spawn a child smelt, useful when answering a question reveals a sub-feature that itself needs its own phase. The server enforces a maximum recursion depth (default 1, configurable up to 3) so a runaway agent cannot chain smelts indefinitely.

Child smelts inherit parentSmeltId and appear linked in the dashboard. The parent can reference the child's crucibleId in its frontmatter so the audit chain stays intact.

Enforcement Gate

The crucible-enforce gate refuses to accept any plan under docs/plans/Phase-*.md without a crucibleId. There are exactly three legitimate ways to satisfy it:

  1. Finalize a smelt, the normal path. Frontmatter is written automatically.
  2. Grandfather migration, on first run after upgrade, existing phase files get a synthetic crucibleId: grandfathered-<uuid> and a row in .forge/crucible/manual-imports.jsonl.
  3. Manual import, pforge run-plan --manual-import path/to/plan.md stamps a synthetic imported-<source>-<uuid> id and logs the bypass. Reserved for Spec Kit imports, offline drafts, and genuine emergencies.
Every manual import is audited. The Governance tab surfaces every row in the audit log. If a reviewer sees an unexplained import, the gate did its job, investigate.

Spec Kit Coexistence

Spec Kit users import external specs regularly. Crucible treats those imports as a first-class path:

pforge run-plan --manual-import docs/plans/imported/Phase-from-speckit.md \
                 --source speckit \
                 --reason "Imported from Spec Kit session 2026-04-15"

The gate writes frontmatter with source: speckit and appends an audit row. The Spec Kit importer does not require a full interview, it trusts that the external spec already carried equivalent structure.

Dashboard Integration

Two tabs expose Crucible's state:

  • 🔥 Crucible tab, live list of in-progress smelts, the active interview prompt, and a draft preview that updates as answers are recorded. A "Hardener ready" notice appears after finalize.
  • 🛡 Governance tab, read-only view of docs/plans/PROJECT-PRINCIPLES.md, .github/instructions/project-profile.instructions.md, and .github/instructions/project-principles.instructions.md, plus the full manual-import audit log. Edits happen in your editor; this tab is intentionally non-editable.

Config Tab Fields

The Config tab's Crucible section persists to .forge/crucible/config.json. All writes go through a sanitizer that drops unknown fields and snaps numbers to safe bounds, so no UI bug can corrupt the file.

FieldRangeDefaultWhat it does
defaultLanetweak / feature / fullfeatureLane used when forge_crucible_submit is called without one.
recursionDepth0–31Max child-smelt depth before the server refuses to spawn another.
autoApproveAgentbooleanfalseWhen true, smelts with source: agent auto-finalize after the interview completes. Use with care.
sourceWeightssum 10034/33/33Weighting for how Memory / Principles / Plans contribute to default answers in the interview. Server normalizes any sum to 100.
staleDefaultsHours1–16824If your Principles or profile file is newer than the smelt by this many hours, the interview flags a STALE_PRINCIPLES / STALE_PROFILE warning so you re-read before finalizing.

Troubleshooting

"Plan rejected: missing crucibleId"
Expected. Either finalize a smelt, re-run setup to trigger grandfather migration, or use --manual-import with a --reason.
CRITICAL_FIELDS_MISSING on finalize v2.82.1
Call forge_crucible_preview to see criticalGaps[] with { field, reason, hint } for each missing answer. The interview will queue a question for each gap when you call ask next.
PLAN_ALREADY_EXISTS on finalize v2.82.1
Read the existing plan at planPath and the smelt's draft at draftPath before deciding. If you genuinely want to replace the existing plan, call finalize again with overwrite: true; the original moves to <phaseName>.replaced-<timestamp>.md.
ASK_QUESTION_MISMATCH v2.82.1
Your client passed a questionId that doesn't match the server's pending question. Re-fetch state via forge_crucible_preview (returns the active question) and retry. Common when two LLM clients drive the same smelt out of order.
"STALE_PRINCIPLES" warning on every smelt
Your Principles file changed after the smelt started. Read it, then resume or abandon. If you consistently hit this, raise staleDefaultsHours in Config.
Recursion blocked at depth 1
By design. If you genuinely need deeper chains, bump recursionDepth in Config. Three is the hard ceiling, beyond that, extract a separate Phase.
Governance tab shows empty file list
You haven't created docs/plans/PROJECT-PRINCIPLES.md yet. Run /project-principles in Copilot chat, or create the file manually.

Downstream extensions

Crucible's downstream surfaces have grown beyond the original chapter. None of these change the core interview → plan → hardener flow above; they extend it with feedback loops the rest of the system uses:

  • Classifier feedback loop. A finalized smelt that later produces a tempering finding can be routed via forge_classifier_issue when the finding turns out to be classifier noise rather than a product bug, closing the audit loop.
  • Drain round-loop. The forge_tempering_drain round-loop (Ch 21 Audit Loop) re-probes a plan's gates until convergence, surfacing residual Crucible STALE_PRINCIPLES warnings as actionable findings rather than transient noise.
  • Knowledge graph enrollment. Every Crucible smelt lands as a Phase / Slice node in the knowledge graph, so forge_patterns_list's detectors can spot Crucible-stage anti-patterns alongside execution-stage ones.

Further Reading