What happened
Archiving an openspec-lane epic with no Gate 2 verdict printed a clear refusal — and then archived it anyway.
$ node conductor.mjs update-epic taxonomy-model-v2 --status archived
conductor: cannot archive openspec-lane epic 'taxonomy-model-v2' — missing a passing Gate 2
(implementation review) verdict. Run 'record-gate-review taxonomy-model-v2 --gate 2 --verdict pass'
after a real fresh-context implementation review before archiving.
State after that command:
$ jq -r '.epics[] | select(.id=="taxonomy-model-v2") | {status, gateReview}' .conductor/state.json
{ "status": "archived", "gateReview": null }
The diff confirms the write landed on the same invocation:
$ git diff .conductor/state.json | rg '^[+-].*"status"'
- "status": "paused",
+ "status": "archived",
So the guard detects the violation, explains it correctly, tells you the exact remediation command — and does not enforce it. gateReview is still null, so nothing downstream can tell this epic was archived without a review.
Why it matters
This is worse than having no guard. A refusal message that isn't a refusal trains you to trust the output rather than verify the state. I only caught it because I re-read state.json afterward; the message reads exactly like a hard stop, so the natural reading is that nothing changed.
The specific damage: an openspec epic can reach archived with gateReview: null, which is indistinguishable in PROJECT.md from one that passed a real implementation review. The two-gate discipline the guard exists to enforce becomes advisory without any signal that it was skipped.
Repro
# any openspec-lane epic with no recorded Gate 2
node conductor.mjs update-epic <openspec-epic-id> --status archived
# observe: refusal printed, exit 0, status changed to archived, gateReview still null
jq -r '.epics[] | select(.id=="<id>") | {status, gateReview}' .conductor/state.json
Note gate-guard was never explicitly configured in this repo (jq '.gateGuard' → absent), so this appears to be default-on behavior.
Suggested fix
- Make it enforce: on refusal, do not write, and exit non-zero so scripted callers fail loudly.
- Or make it honest: if the guard is advisory by design, word it as a warning ("archiving without a Gate 2 verdict") and record the skip durably — e.g.
gateReview: {gate2: {verdict: "skipped", at: ...}} — so a future reader can tell. Silent null is the one outcome that should not be possible.
- Either way, an
--allow-missing-gate style opt-in would make the exception explicit and auditable instead of accidental.
Engine 0.25.0. Related: #79 (add-epic --notes accepted and discarded) — same shape of defect, a flag/guard that reports success while not doing the thing.
What happened
Archiving an openspec-lane epic with no Gate 2 verdict printed a clear refusal — and then archived it anyway.
State after that command:
The diff confirms the write landed on the same invocation:
So the guard detects the violation, explains it correctly, tells you the exact remediation command — and does not enforce it.
gateReviewis stillnull, so nothing downstream can tell this epic was archived without a review.Why it matters
This is worse than having no guard. A refusal message that isn't a refusal trains you to trust the output rather than verify the state. I only caught it because I re-read
state.jsonafterward; the message reads exactly like a hard stop, so the natural reading is that nothing changed.The specific damage: an openspec epic can reach
archivedwithgateReview: null, which is indistinguishable inPROJECT.mdfrom one that passed a real implementation review. The two-gate discipline the guard exists to enforce becomes advisory without any signal that it was skipped.Repro
Note
gate-guardwas never explicitly configured in this repo (jq '.gateGuard'→ absent), so this appears to be default-on behavior.Suggested fix
gateReview: {gate2: {verdict: "skipped", at: ...}}— so a future reader can tell. Silentnullis the one outcome that should not be possible.--allow-missing-gatestyle opt-in would make the exception explicit and auditable instead of accidental.Engine 0.25.0. Related: #79 (
add-epic --notesaccepted and discarded) — same shape of defect, a flag/guard that reports success while not doing the thing.