Bug: porch consultation.models: "parent" and "none" dead-end on per-plan-phase protocols
Summary
For SPIR-style per-plan-phase protocols, setting .codev/config.json porch.consultation.models to either "parent" or "none" causes the porch state machine to never advance the plan phase. The phase-review-<phase_id> gate is emitted as instruction text in porch next's output but is never registered in state.gates, so porch approve rejects it as Unknown gate. The porch done + porch next loop emits the same "wait" task indefinitely.
Reproduction
- Set up a SPIR project with multiple
plan_phases.
- Set
.codev/config.json:
{
"porch": {
"consultation": {
"models": "parent"
}
}
}
- Reach the implement phase; complete the first plan phase's work.
- Run
porch done <id> — succeeds, marks build_complete: true.
- Run
porch next <id> — returns the parent-mode wait task:
"description": "Consultation is set to \"parent\" mode. The architect must review this phase directly.\n\nGate phase-review-<phase_id> is pending. STOP and wait for architect approval."
- Try
porch approve <id> phase-review-<phase_id> --a-human-explicitly-approved-this:
Error: Unknown gate: phase-review-<phase_id>
Known gates: spec-approval, plan-approval, pr, verify-approval
- Plan phase never advances.
The same outcome holds for models: "none".
Root cause
In dist/commands/porch/next.js around lines 408-417, the parent-mode branch early-returns a task with a synthetic gate name:
if (consultMode === 'parent') {
const gateName = `phase-review-${state.current_plan_phase || state.phase}`;
const tasks = [{
subject: `Request architect review: ${gateName}`,
// ...
description: `Consultation is set to "parent" mode. The architect must review this phase directly.\n\nGate ${gateName} is pending. STOP and wait for architect approval.`,
// ...
}];
return { status: 'tasks', ...baseResponse, tasks };
}
The gateName is constructed for the user-facing message but never written into state.gates. The none-mode branch has the same structural issue (returns a "Run: porch done" task that doesn't advance state).
The only code path that advances current_plan_phase is handleVerifyApproved, which fires only when findReviewFiles returns a non-empty reviews array with allApprove(reviews) === true. Both parent and none modes early-return before reaching findReviewFiles, so neither path can advance.
Workaround
Use porch.consultation.models: "claude" (single-model normal mode) and drop architect-authored <id>-<phase>-iter<N>-<model>.txt review files at the porch-expected path (<projectDir>/<id>-<phase>-iter<N>-<model>.txt) ending in VERDICT: APPROVE. porch's normal-mode findReviewFiles picks them up and handleVerifyApproved advances.
Recommended fix
Two options:
-
Make parent and none modes auto-advance by falling through to handleVerifyApproved with an empty reviews array. allApprove([]) === true per current verdict.js logic, so this would work without other changes — except that handleVerifyApproved expects reviews for its history-recording step (cheap fix).
-
Register the synthetic gate in state.gates so porch approve <id> phase-review-<phase_id> becomes a real operation. Requires extending the gate machinery to support per-plan-phase gates.
Option 1 is simpler and matches the apparent intent of none mode ("skip consultation, auto-advance"). For parent mode, the gate-registration option preserves the "architect must explicitly approve" semantics.
Impact
Without a workaround, any project that sets parent or none mode on a per-plan-phase protocol becomes unable to advance. We hit this in a security-research project (private repo) running SPIR with claude-only consultation; the workaround unblocked us, but the dead-end loop cost several review cycles before we traced it through the source.
Discovered
2026-05-23/24 during a SPIR-protocol security-research project. Workaround documented in our project's CLAUDE.md for the benefit of future sessions.
Bug: porch
consultation.models: "parent"and"none"dead-end on per-plan-phase protocolsSummary
For SPIR-style per-plan-phase protocols, setting
.codev/config.jsonporch.consultation.modelsto either"parent"or"none"causes the porch state machine to never advance the plan phase. Thephase-review-<phase_id>gate is emitted as instruction text inporch next's output but is never registered instate.gates, soporch approverejects it asUnknown gate. Theporch done+porch nextloop emits the same "wait" task indefinitely.Reproduction
plan_phases..codev/config.json:{ "porch": { "consultation": { "models": "parent" } } }porch done <id>— succeeds, marksbuild_complete: true.porch next <id>— returns the parent-mode wait task:porch approve <id> phase-review-<phase_id> --a-human-explicitly-approved-this:The same outcome holds for
models: "none".Root cause
In
dist/commands/porch/next.jsaround lines 408-417, the parent-mode branch early-returns a task with a synthetic gate name:The
gateNameis constructed for the user-facing message but never written intostate.gates. Thenone-mode branch has the same structural issue (returns a "Run: porch done" task that doesn't advance state).The only code path that advances
current_plan_phaseishandleVerifyApproved, which fires only whenfindReviewFilesreturns a non-emptyreviewsarray withallApprove(reviews) === true. Bothparentandnonemodes early-return before reachingfindReviewFiles, so neither path can advance.Workaround
Use
porch.consultation.models: "claude"(single-model normal mode) and drop architect-authored<id>-<phase>-iter<N>-<model>.txtreview files at the porch-expected path (<projectDir>/<id>-<phase>-iter<N>-<model>.txt) ending inVERDICT: APPROVE. porch's normal-modefindReviewFilespicks them up andhandleVerifyApprovedadvances.Recommended fix
Two options:
Make
parentandnonemodes auto-advance by falling through tohandleVerifyApprovedwith an emptyreviewsarray.allApprove([]) === trueper current verdict.js logic, so this would work without other changes — except thathandleVerifyApprovedexpects reviews for its history-recording step (cheap fix).Register the synthetic gate in
state.gatessoporch approve <id> phase-review-<phase_id>becomes a real operation. Requires extending the gate machinery to support per-plan-phase gates.Option 1 is simpler and matches the apparent intent of
nonemode ("skip consultation, auto-advance"). Forparentmode, the gate-registration option preserves the "architect must explicitly approve" semantics.Impact
Without a workaround, any project that sets
parentornonemode on a per-plan-phase protocol becomes unable to advance. We hit this in a security-research project (private repo) running SPIR with claude-only consultation; the workaround unblocked us, but the dead-end loop cost several review cycles before we traced it through the source.Discovered
2026-05-23/24 during a SPIR-protocol security-research project. Workaround documented in our project's CLAUDE.md for the benefit of future sessions.