Skip to content

porch: parent/none-mode consultation dead-ends per-plan-phase protocols #836

@swiftraccoon

Description

@swiftraccoon

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

  1. Set up a SPIR project with multiple plan_phases.
  2. Set .codev/config.json:
    {
      "porch": {
        "consultation": {
          "models": "parent"
        }
      }
    }
  3. Reach the implement phase; complete the first plan phase's work.
  4. Run porch done <id> — succeeds, marks build_complete: true.
  5. 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."
    
  6. 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
    
  7. 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:

  1. 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).

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions