Repository navigation
fix(workflows): an interrupted gate prompt must not approve the gate - #4529
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Ctrl+C is still converted into a verdict instead of propagating to the engine’s pause handler.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes gate prompts selecting an approving option when interrupted.
Changes:
- Selects a declared reject/abort option on prompt interruption.
- Adds regression tests for option ordering and fallback behavior.
File summaries
| File | Description |
|---|---|
src/specify_cli/workflows/steps/gate/__init__.py |
Changes interrupted-prompt fallback selection. |
tests/test_workflows.py |
Adds prompt and step-level regression coverage. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback and rebase on upstream/main
|
Please resolve conflicts |
3bc7329 to
5492c9e
Compare
|
@mnriem Conflicts resolved (5492c9e). The fix now lives at Verified against current Rebased as a single commit on current |
|
Please address Copilot feedback |
`GateStep._prompt` treated Ctrl+C / Ctrl+D as a choice:
except (EOFError, KeyboardInterrupt):
print()
return options[-1] # default to last (usually reject)
"usually reject" is an assumption `validate` never enforces. It requires only
that *some* option is 'reject'/'abort':
reject_choices = {"reject", "abort"}
if not any(o.lower() in reject_choices for o in options):
so a hand-written `options` list whose reject choice is not last validates
clean, and `options[-1]` is then an approving option. `execute` classifies the
result with `if choice.lower() in ("reject", "abort")`, so the gate reported
COMPLETED and the run walked straight past the human review:
options=['approve', 'reject'] -> failed choice='reject'
options=['reject', 'approve'] -> completed choice='approve'
options=['approve', 'reject', 'request-changes'] -> completed choice='request-changes'
All three validate with zero errors.
This also contradicted the engine's own interrupt contract: `WorkflowEngine`
catches KeyboardInterrupt and sets `RunStatus.PAUSED` with a
`workflow_interrupted` log event, so Ctrl+C anywhere else pauses the run for
`specify workflow resume`. Only at a gate did it silently make an approval
decision.
Now prefers the declared reject/abort option, falling back to the last option
when none is declared. For the documented default `[approve, reject]` this is
byte-for-byte the previous behaviour.
Rebased onto current main (files moved in the workflow/bundler restructure).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…line The gate's Ctrl+C comment pointed at `engine.py:1059-1063` and `:1144-1148`; after the restructure those lines are workflow-copy persistence and resume bounds validation, while the handlers now sit in `WorkflowEngine.execute` and `WorkflowEngine.resume`. Name the methods so the reference survives the next move. Comment-only change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5492c9e to
4dd4a28
Compare
|
@mnriem Copilot feedback addressed in 4dd4a28, and the thread is resolved. The gate's Ctrl+C comment now cites |
|
Thank you! |


Problem
GateStep._prompttreats Ctrl+C / Ctrl+D as a choice:"usually reject" is an assumption
validatenever enforces. It requires only that some option isreject/abort— never that it is last:So a hand-written
optionslist whose reject choice is not last validates clean, andoptions[-1]is then an approving option.executeclassifies the result withif choice.lower() in ("reject", "abort"), so the gate reports COMPLETED and the run continues past the human review the gate exists to enforce.Reproduction on current
main(c173bf1)Interrupting the prompt, with
validate()run on each config first:Identical for both
KeyboardInterruptandEOFError. All three validate with zero errors —specify workflow validategives the author no warning.The middle and last rows are the bug: the operator pressed Ctrl+C at an approval gate and the workflow recorded an approval.
It also contradicts the engine's own interrupt contract
WorkflowEnginecatchesKeyboardInterruptand setsRunStatus.PAUSEDwith aworkflow_interruptedlog event (inWorkflowEngine.executeandWorkflowEngine.resume), so Ctrl+C anywhere else in a run pauses it forspecify workflow resume. Only at a gate did it silently make a decision instead.Fix
Prefer the declared reject/abort option; fall back to the last option when none is declared.
After the fix, every config carrying a reject option rejects on interrupt:
Behaviour change — disclosed
The returned choice changes only when the prompt is interrupted and a reject/abort option is not last. The documented default
options: [approve, reject]is byte-for-byte unchanged, which thereject_lastparametrization pins by passing both before and after. A workflow that deliberately relied on Ctrl+C selecting a trailing non-reject option (saydefer) would now reject instead — that reliance is precisely the defect being fixed, but it is a real change and worth stating.Verification
upstream/main→ 55 passed with the fix.tests/test_workflows.py: 21 failed / 964 passed against a clean-mainbaseline of 21 failed / 956 passed — no new failures (the 21 are the known Windowsos.replaceflakiness in that file).uvx ruff@0.15.0 check src tests→ cleanWritten with assistance from Claude Code. Bug found, reproduced, and verified by me on current
main.🤖 Generated with Claude Code