feat: surface gate detail in the workflow run/resume --json payload#2965
feat: surface gate detail in the workflow run/resume --json payload#2965doquanghuy wants to merge 3 commits into
Conversation
A paused run was indistinguishable from any other pause in the machine-readable outcome, and the gate's prompt/options/choice never left the human-facing stream. Record each step's type in the run state's step results (one engine line) and, when the run sits at a gate, add a gate block (step_id/message/options/choice) to the payload so orchestrators can drive review gates without parsing stdout. Reference implementation for the proposal in github#2964. Addresses github#2964
|
@mnriem when you have a moment, would appreciate your thoughts on the direction here — the issue lists the alternatives considered, and I'm happy to rework toward whichever shape fits Spec Kit best. |
There was a problem hiding this comment.
Pull request overview
This PR extends the workflow CLI’s --json run/resume outcome payload to include structured details when the run is paused at a gate step, enabling external orchestrators to detect “human review needed” without parsing stdout.
Changes:
- Record each executed step’s
typeinto persistedstep_resultsso step types are recoverable from run state. - Add an optional
gateblock to theworkflow run --json/workflow resume --jsonpayload when the current step is a gate. - Add CLI-level tests covering a non-interactive gate pause (includes
gateblock) and a non-gate completed run (nogatekey).
Show a summary per file
| File | Description |
|---|---|
tests/test_workflows.py |
Adds CLI-level tests asserting --json includes a structured gate block on gate pauses and omits it for a normal completed run. |
src/specify_cli/workflows/engine.py |
Persists type in each step’s recorded step_results entry so step-type introspection is possible from run state. |
src/specify_cli/__init__.py |
Builds the --json outcome payload and conditionally injects gate details via a helper when the current step is a gate. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 2
| step = (getattr(state, "step_results", None) or {}).get(state.current_step_id) | ||
| if not isinstance(step, dict) or step.get("type") != "gate": | ||
| return None |
| runner = CliRunner() | ||
| result = runner.invoke(app, ["workflow", "run", str(path), "--json"]) | ||
| return _json.loads(result.stdout) |
|
Please address Copilot feedback |
Address review (github#2965): _gate_outcome() emitted a gate block whenever current_step_id pointed at a gate step. Since RunState.current_step_id is never cleared on completion, a completed/failed run whose last step was a gate leaked stale gate detail in run/resume/status --json. Guard on status == paused. Also assert CLI success in the _run_json test helper before JSON-parsing, and add direct coverage for the suppression guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@mnriem Thanks for the review — addressed the Copilot feedback:
Full suite green; |
Address Copilot review: - `_gate_outcome` now also surfaces the gate block when a run is `aborted` by a gate rejection (`on_reject: abort`), not only when `paused`. Abort is the only path that sets ABORTED and it leaves current_step_id on the gate, so an orchestrator can read the recorded `choice` for the stop. - Coerce `message` to a string (it may be a non-string YAML literal that GateStep only coerces for interpolation) so the JSON schema stays stable. - Tests: add a CLI-level aborted-path test, a message-coercion test, and extend the suppression test to allow `aborted`; share the run helper via `_invoke_json` to avoid duplicating the invoke boilerplate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@mnriem Pushed
|
| result = self._invoke_json(tmp_path, monkeypatch, self._WF_GATE) | ||
| payload = _json.loads(result.stdout) | ||
| assert payload["status"] == "aborted" |
Description
Reference implementation for #2964 — for discussion, direction welcome.
When a run pauses at a gate, the
--jsonoutcome now carries agateblock (step_id/message/options/choice) so orchestrators can detect "human review needed" and present the options without parsing the human-facing stream. Two small pieces:typein the run state's step results (one added line instep_data— previously the type was not recoverable from state)._workflow_run_payloadadds thegateblock via a_gate_outcomehelper when the run's current step is a gate.choicepopulates when the outcome ends at the gate with a decision recorded (e.g. an interactive rejection withon_reject: abort→ afailedpayload carrying"choice": "reject"; anon_reject: retrypause likewise). A mid-flow approval proceeds past the gate, so the block clears — by design. Non-gate runs and runs that end elsewhere are unchanged — nogatekey, payload byte-identical to today.The issue lists alternatives (a generic
paused_stepblock; a dedicated status value) — happy to rework toward either.Testing
uv sync && uv run pytest— full suite 3727 passedTestWorkflowRunGateOutcomeJson): a gate pause carries the exact block (CliRunner stdin is non-TTY, so the gate pauses); a completed run has nogatekey — the gate-pause test is red against currentmain, green with the change (verified both directions)uvx ruff check src/— cleanuv run specify --helpworkflow run --json)AI Disclosure
Code, tests, and this description were authored with AI assistance (Claude); verified by running the repo's test suite and ruff locally in both red and green directions.