docs(workflows): clarify on_reject:skip semantics — engine returns COMPLETED, not auto-skip

Copilot finding on b8982a7:

The README example's gate message said "reject to skip the rest of this
branch", and the explanatory paragraph claimed [approve, reject] map
to "continue" vs "skip the rest of this branch". The engine does not
implement automatic branch-skipping. `on_reject: skip` returns
`StepStatus.COMPLETED` (gate/__init__.py:65-66); the next sibling step
runs unconditionally unless the author wires a downstream `if` reading
`{{ steps.<gate-id>.output.choice }}`.

Two fixes:

1. Restructured the YAML example so it actually demonstrates the
   manual-branching pattern: added a `recover` if-step after the gate
   that conditions on `steps.review.output.choice == 'approve'`. Now
   the example shows the real workflow author's responsibility instead
   of implying the engine does it.

2. Replaced the trailing paragraph with three precise notes:
   - both gate options return COMPLETED; `on_reject: skip` controls
     abort behaviour only, not sibling-skipping
   - all three `on_reject` values enumerated with their actual engine
     semantics (FAILED+aborted / COMPLETED / PAUSED)
   - the original retry-loop guidance retained as the third bullet

Updated the gate message in the example to match — "reject to leave the
failure recorded and move on" instead of "reject to skip the rest of
this branch".

Audited the whole PR diff for the same overclaim: no other instance.
Engine semantics, validation, and test bodies are unchanged. Docs-only.

161/161 tests/test_workflows.py pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
doquanghuy
2026-05-29 21:47:00 +07:00
parent b8982a748a
commit 393ac6b710

View File

@@ -242,18 +242,36 @@ remains available on `steps.<id>.output.exit_code` so downstream
then: then:
- id: review - id: review
type: gate type: gate
message: "Step failed (exit {{ steps.heavy-thing.output.exit_code }}). Approve to continue, or reject to skip the rest of this branch." message: "Step failed (exit {{ steps.heavy-thing.output.exit_code }}). Approve to run the recovery path, or reject to leave the failure recorded and move on."
on_reject: skip on_reject: skip
- id: recover
type: if
condition: "{{ steps.review.output.choice == 'approve' }}"
then:
- id: rerun
command: speckit.recovery
else: else:
- id: next-thing - id: next-thing
command: speckit.next-thing command: speckit.next-thing
``` ```
The gate's default `options: [approve, reject]` map directly to "continue A few things worth knowing about that example:
the run" vs. "skip the rest of this branch" — gates do not automatically
re-run the failed step. To express a retry path, either define custom - Both gate options (`approve`, `reject`) return `StepStatus.COMPLETED`;
gate options and branch on the choice downstream, or wrap the failing `on_reject: skip` controls only whether the engine aborts on reject
step in your own loop. (it doesn't, with `skip`) — it does **not** auto-skip subsequent
sibling steps in the `then:` list. Downstream branching is the
workflow author's responsibility: read
`{{ steps.<gate-id>.output.choice }}` in a follow-up `if`, `switch`,
or expression, as the `recover` step above does.
- `on_reject` has three values: `abort` (default — reject → `FAILED`
with `output.aborted = True`, halts the run), `skip` (reject →
`COMPLETED`, author handles branching as shown), and `retry`
(reject → `PAUSED` so the next `specify workflow resume` re-runs
the gate).
- Gates do not automatically re-run the failed step. To express a
retry path, either define custom gate options and branch on the
choice downstream, or wrap the failing step in your own loop.
**Notes:** **Notes:**