Bug
Running porch done <id> when status.yaml is already in the verified terminal state performs a redundant write to status.yaml and creates a redundant git commit (chore(porch): <id> protocol complete).
porch done should be idempotent on terminal states: re-running it on an already-verified project should be a silent no-op, not a fresh state mutation.
Root cause
packages/codev/src/commands/porch/index.ts:334-517 (done() function) has no early-exit when state.phase === 'verified'. The function falls through every check (no phase config, no gate, not phased, not build_verify) and reaches advanceProtocolPhase() at line 519.
In advanceProtocolPhase() (lines 519-528), getNextPhase(protocol, 'verified') returns null because verified is a terminal pseudo-phase not present in protocol.phases. The if (!nextPhase) branch then unconditionally re-assigns state.phase = 'verified' (it already was) and calls writeStateAndCommit(...) with message ```chore(porch): ${state.id} protocol complete``` — producing the redundant write and commit.
For comparison, the rollback handler at index.ts:813 already treats 'verified' and 'complete' as special terminal states. The same recognition is missing from done().
Reproduction
- Run any protocol to completion so
status.yaml reaches phase: verified
- Run `porch done ` again
- Observe a new commit: `chore(porch): protocol complete` and a touched `status.yaml` (mtime updated, content unchanged)
Expected behavior
- Bare `porch done ` on a project whose `state.phase === 'verified'` should print a brief message (e.g. "Project already verified — nothing to do."), exit 0, and perform no state write and no git commit.
- Record-only modes (`--pr` and `--merged`) must continue to work on verified projects — recording PR metadata after a project completes is a legitimate use case.
Fix scope
- Add an early-exit at the top of `done()` in `packages/codev/src/commands/porch/index.ts` (after the record-only handlers at lines 344-366, before phase-check loading at line 367) that detects `state.phase === 'verified'`, prints a friendly message, and returns without writing state.
- Add a regression test to `packages/codev/src/commands/porch/tests/done-verification.test.ts` that constructs a `verified` state, calls `done()`, and asserts no `writeStateAndCommit` invocation occurred.
Out of scope
- Auditing `porch next` for similar idempotency gaps (separate concern).
- Changing behavior of `'complete'` state (the user-confirmed scope is `'verified'` only).
Bug
Running
porch done <id>whenstatus.yamlis already in theverifiedterminal state performs a redundant write tostatus.yamland creates a redundant git commit (chore(porch): <id> protocol complete).porch doneshould be idempotent on terminal states: re-running it on an already-verified project should be a silent no-op, not a fresh state mutation.Root cause
packages/codev/src/commands/porch/index.ts:334-517(done()function) has no early-exit whenstate.phase === 'verified'. The function falls through every check (no phase config, no gate, not phased, not build_verify) and reachesadvanceProtocolPhase()at line 519.In
advanceProtocolPhase()(lines 519-528),getNextPhase(protocol, 'verified')returnsnullbecauseverifiedis a terminal pseudo-phase not present inprotocol.phases. Theif (!nextPhase)branch then unconditionally re-assignsstate.phase = 'verified'(it already was) and callswriteStateAndCommit(...)with message ```chore(porch): ${state.id} protocol complete``` — producing the redundant write and commit.For comparison, the rollback handler at
index.ts:813already treats'verified'and'complete'as special terminal states. The same recognition is missing fromdone().Reproduction
status.yamlreachesphase: verifiedExpected behavior
Fix scope
Out of scope