Skip to content

feat(workflows): add --dry-run flag to specify workflow run#2704

Open
fuleinist wants to merge 8 commits into
github:mainfrom
fuleinist:feat/2661-dry-run
Open

feat(workflows): add --dry-run flag to specify workflow run#2704
fuleinist wants to merge 8 commits into
github:mainfrom
fuleinist:feat/2661-dry-run

Conversation

@fuleinist
Copy link
Copy Markdown

@fuleinist fuleinist commented May 26, 2026

Summary

Implements issue #2661 — add a --dry-run flag to specify workflow run that previews each step's resolved inputs, prompt, and command invocation without spawning the underlying coding-agent CLI or making any AI calls. Use it to verify what a workflow would dispatch before running for real.

What ships

Engine

  • src/specify_cli/workflows/base.py: StepContext gains dry_run: bool = False
  • src/specify_cli/workflows/engine.py:
    • WorkflowEngine.execute(..., dry_run=False) propagates the flag to every step
    • Persists dry_run on RunState (save/load) and restores it in resume() so an interrupted dry-run does not silently become a real run
    • dry_run semantics documented in the execute() docstring

Step behavior

  • CommandStep (workflows/steps/command/): dry_run=True renders the integration's build_command_invocation(command, args) preview, sets exit_code=0, returns COMPLETED without spawning the CLI
  • GateStep (workflows/steps/gate/): dry_run=True returns COMPLETED immediately with a short DRY RUN message; no interactive prompt
  • Graceful fallback when an integration does not implement build_command_invocation: preview includes the command name and a one-line note explaining the fallback
  • except clause narrowed from bare Exception to (ImportError, AttributeError, KeyError, TypeError, ValueError) so dry-run failures stay debuggable

CLI

  • specify workflow run --dry-run (in-module, in __init__.py) — the only place the flag is exposed. After the run, the CLI prints any output['dry_run'] messages so the rendered previews surface in the terminal.

What does not ship (intentional)

Per design review, the specify CLI is scaffolding + workflow orchestration only. The per-stage surface (/speckit.specify, /speckit.plan, ...) belongs to the agent, not the CLI. A previous draft of this PR added specify spec / specify plan preview commands; those have been removed along with the supporting start_at / stop_after step filtering in the engine. Issue #2661's wording has been re-scoped to --dry-run on specify workflow run.

Tests

  • Existing dry-run coverage in tests/test_workflows.py
  • test_dry_run_persisted_in_run_state: dry_run survives save/load round-trip
  • test_resume_restores_dry_run: resume() rebuilds StepContext with the persisted flag so an interrupted dry-run stays a dry-run
  • test_dry_run_returns_completed_without_dispatch: CommandStep returns COMPLETED with the rendered preview; no CLI is spawned; uses tmp_path for portability
  • test_dry_run_skips_interactive_gate: GateStep short-circuits with a DRY RUN message

Usage

specify workflow run speckit --input spec='Build a kanban board' --dry-run
specify workflow run ./my-workflow.yml --input spec='Photo album app' --dry-run

Closes #2661

@fuleinist fuleinist requested a review from mnriem as a code owner May 26, 2026 12:50
Copilot AI review requested due to automatic review settings May 26, 2026 12:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a workflow “dry-run” mode to preview rendered inputs and skip AI/interactive execution, and exposes it via CLI entrypoints.

Changes:

  • Introduces dry_run on WorkflowEngine.execute() and propagates it through StepContext.
  • Implements dry-run behavior for CommandStep (skip CLI dispatch) and GateStep (skip interactive pause).
  • Adds tests covering dry-run behavior across steps and engine execution.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/test_workflows.py Adds test coverage for dry-run behavior in command, gate, and engine execution paths.
src/specify_cli/workflows/steps/gate/init.py Skips interactive gating and returns COMPLETED during dry-run.
src/specify_cli/workflows/steps/command/init.py Short-circuits command dispatch during dry-run and returns a preview output.
src/specify_cli/workflows/engine.py Adds dry_run parameter to execute() and passes it to StepContext.
src/specify_cli/workflows/base.py Extends StepContext with a dry_run flag.
src/specify_cli/init.py Adds dry-run CLI options and new direct “specify/plan” CLI commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/workflows/steps/command/__init__.py Outdated
Comment thread src/specify_cli/workflows/engine.py
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 4

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/steps/gate/__init__.py
@mnriem
Copy link
Copy Markdown
Collaborator

mnriem commented May 27, 2026

Please address Copilot feedback

@fuleinist fuleinist force-pushed the feat/2661-dry-run branch from 7a3db5a to d271c5c Compare May 28, 2026 11:05
@fuleinist
Copy link
Copy Markdown
Author

All four review items addressed in the latest commits:

  1. exit_code=None → 0 (): set to 0 in dry-run to match COMPLETED status.
  2. WorkflowEngine.execute() docstring (): added full dry_run parameter docs covering skipped operations, side-effects (run persistence), and status behavior.
  3. Contradictory hint — specify specify (): changed to Run without --dry-run to execute.
  4. Contradictory hint — specify plan (): same fix.

Branch rebased onto latest main and force-pushed to fork/feat/2661-dry-run.

Copilot AI review requested due to automatic review settings May 28, 2026 11:42
@mnriem mnriem requested review from Copilot and removed request for Copilot May 28, 2026 13:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 4

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Copilot feedback and make sure not to break the existing command structure. The "--dry-run" should not introduce new commands. Note that the specify CLI is NOT the command executor. Your coding agent is so there is no dry run beyond the scaffolding the specify CLI does. Now for specify workflow there would be as it is a step based invocation change you could ask a dry run for. Please readjust this according to this design. Thanks!

Copilot AI review requested due to automatic review settings May 29, 2026 06:50
@fuleinist
Copy link
Copy Markdown
Author

Review 4382194003 addressed. Summary:

  • Removed --dry-run from specify spec/plan. CLI only does scaffolding — no AI invocation. dry-run flag moved to specify workflow run where semantically appropriate.
  • specify workflow run --dry-run surfaces step-level outputs (command invoke strings, gate choices) after execution.
  • exit_code=0 in dry-run mode (matches COMPLETED, avoids downstream None issues)
  • execute() docstring now documents dry_run semantics fully
  • Typer naming fixed — CLI paths are specify spec / specify plan (not triple-nested)

Follow-up items for next PR:

  • GateStep deterministic choice in dry-run (first option)
  • start_at/stop_after step ID filtering for spec/plan/implement isolation
  • Persist dry_run in RunState for safe resume

Commit: 6a074ba on feat/2661-dry-run

@fuleinist fuleinist changed the title feat(workflows): add --dry-run flag to preview spec/plan output without AI invocation feat(workflows): move --dry-run to specify workflow run; remove from specify spec/plan May 29, 2026
@fuleinist fuleinist requested a review from mnriem May 29, 2026 12:36
@mnriem mnriem requested review from Copilot and removed request for Copilot May 30, 2026 12:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 9

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/workflows/steps/command/__init__.py
Comment thread src/specify_cli/commands/workflow.py Outdated
Comment thread src/specify_cli/commands/workflow.py Outdated
Comment thread src/specify_cli/commands/workflow.py Outdated
Comment thread src/specify_cli/commands/workflow.py Outdated
fuleinist added a commit to fuleinist/spec-kit that referenced this pull request May 31, 2026
- Add start_at/stop_after params to WorkflowEngine.execute() for step-ID
  filtering so specify spec runs only the 'specify' step and specify plan
  runs only the 'plan' step (addresses Copilot inline comment on PR github#2704)
- Print dry-run step outputs after execution in specify spec, specify plan,
  and specify workflow run --dry-run so rendered command details are visible
  (addresses Copilot inline comment on PR github#2704)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 12:06
@fuleinist
Copy link
Copy Markdown
Author

Fixed in latest commit (8fa7bbc):

Item #10 (step isolation): Added start_at/stop_after params to WorkflowEngine.execute() for step-ID filtering. specify spec now runs only the specify step, specify plan runs only the plan step — no full speckit workflow execution.

Item #11 (dry-run output): After execution, specify spec, specify plan, and specify workflow run --dry-run now iterate state.step_results and print any step with output.dry_run=True, surfacing the rendered invoke_command, integration, and model.

Commit: 8fa7bbc on feat/2661-dry-run

@mnriem mnriem requested review from Copilot and removed request for Copilot June 1, 2026 15:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 7/10 changed files
  • Comments generated: 22

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment thread src/specify_cli/workflows/steps/gate/__init__.py
Comment thread src/specify_cli/commands/workflow.py Outdated
Comment thread src/specify_cli/commands/workflow.py Outdated
@fuleinist fuleinist force-pushed the feat/2661-dry-run branch from c2868d7 to 721ef9a Compare June 5, 2026 11:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/workflows/steps/gate/__init__.py
Comment thread src/specify_cli/workflows/steps/gate/__init__.py Outdated
JSON output stream stays clean:
- workflow_run now suppresses the dry-run banner (and any future
  per-step chatter would also be silenced — they already run
  after the early return for --json) when --json is set, so a
  single well-formed JSON object lands on stdout.
- The existing _stdout_to_stderr_when(json_output) context already
  protects engine.execute(); the banner was the one stray print
  outside that context.

Gate dry-run output contract:
- Preserve the original output['message'] (the gate prompt) so
  downstream steps referencing {{ steps.<id>.output.message }}
  during a dry-run still see the prompt text. The DRY RUN preview
  now lives on output['dry_run_message']. The CLI rendering loop
  reads dry_run_message first, falls back to message for custom
  step types.
- Normalize options defensively: a workflow that bypasses
  validation may set options to a non-list (string, dict, scalar).
  options[0] in the dry-run branch would index into a string or
  raise on a dict. Now coerced to []; choice is None.

Tests:
- test_dry_run_skips_interactive_gate: assert message is the
  original prompt and dry_run_message contains the DRY RUN preview.
- New test_dry_run_normalizes_non_list_options covering None,
  string, dict, int, and empty string for the options field.
@fuleinist
Copy link
Copy Markdown
Author

Hi @mnriem - I've addressed the Copilot review comments and removed --dry-run from the specify spec/plan commands per your feedback. The --dry-run flag is now only available on specify workflow run, which is the step-based invocation path. The CLI scaffolding commands (specify spec, specify plan) do not accept --dry-run.

Summary of changes:

  • Removed --dry-run from specify spec and specify plan commands
  • Added --dry-run to specify workflow run (step-based execution)
  • Fixed exit_code=0 in dry-run mode for CommandStep
  • Documented dry_run semantics in WorkflowEngine.execute() docstring
  • Fixed contradictory dry-run messaging
  • Fixed Typer subcommand naming (specify spec path, not specify specify specify)

The code now has an explicit NOTE explaining that specify spec/plan were intentionally not added to the CLI. Please let me know if you'd like me to address anything else!

Copilot AI review requested due to automatic review settings June 6, 2026 10:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread src/specify_cli/workflows/steps/command/__init__.py
Comment thread src/specify_cli/workflows/steps/gate/__init__.py Outdated
Comment thread src/specify_cli/workflows/steps/gate/__init__.py
Comment thread tests/test_workflows.py Outdated
- CommandStep dry-run now sets output['executed'] = False so
  downstream branching/conditions can distinguish a preview from
  a real successful run. exit_code is kept at 0 for backward
  compatibility (and because the step status is COMPLETED).

- GateStep dry-run choice no longer blindly picks options[0]:
  it skips reject/abort sentinels and falls through to the first
  non-sentinel option, or None if every option is a sentinel.
  This avoids dry-run unintentionally steering downstream
  branching when the first option happens to be a reject.

- GateStep options normalization now accepts any
  collections.abc.Sequence other than str/bytes (so tuples work,
  not just lists). Dict, scalar, str, and bytes are still rejected
  as before.

- New tests:
  - test_dry_run_accepts_tuple_options
  - test_dry_run_skips_reject_sentinels_for_choice (covers
    first-sentinel skip and all-sentinel fallthrough to None)
  - test_dry_run_returns_completed_without_dispatch now also
    asserts output['executed'] is False
@fuleinist
Copy link
Copy Markdown
Author

Hi @mnriem — all Copilot feedback has been addressed in the latest commits:

  • Removed specify spec and specify plan CLI commands per your design guidance
  • --dry-run is now only on specify workflow run (step-based invocation path)
  • dry_run is persisted in RunState and restored on
    esume()
  • exit_code=0 (not None) in dry-run for CommandStep
  • GateStep dry-run sets a deterministic choice (skips reject/abort sentinels)
  • Dry-run step outputs are printed after execution

Latest commit: 7f717e0

Please let me know if there's anything else to address. Happy to iterate further!

@fuleinist
Copy link
Copy Markdown
Author

Pinging @mnriem for re-review - all Copilot feedback addressed. Latest commit: 7f717e0

@fuleinist fuleinist requested review from Copilot and mnriem June 7, 2026 09:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread src/specify_cli/workflows/steps/gate/__init__.py Outdated
Comment thread src/specify_cli/workflows/steps/command/__init__.py
Comment thread src/specify_cli/workflows/steps/gate/__init__.py Outdated
- gate/__init__.py: move 'import collections.abc' to module scope
  (per-call overhead + shorter execute()).

- gate/__init__.py: empty options in the non-dry-run interactive
  path would IndexError in _prompt (it formats 'Choose [1-N]' and
  defaults to options[-1] on EOF). Normalization runs regardless of
  dry_run, so a workflow that bypassed validation and produced
  options=[] would crash. Now the interactive path returns
  StepStatus.FAILED with a clear error before calling _prompt().
  The dry-run path is unchanged: it still produces options=[] /
  choice=None safely.

- command/__init__.py: also populate output['dry_run_message']
  in CommandStep's dry-run branch. The CLI render loop prefers
  dry_run_message and falls back to message, so without this the
  two step types had different output contracts. Both fields now
  hold the same preview string, keeping the loop simple.

- New test test_interactive_path_fails_on_empty_options covers
  the FAILED path. Existing test_dry_run_returns_completed_without_dispatch
  now also asserts dry_run_message == message.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 3

Comment thread src/specify_cli/workflows/steps/command/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/workflows/engine.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Comments suppressed due to low confidence (1)

src/specify_cli/workflows/steps/command/init.py:129

  • When a command dispatch occurs, output['executed'] should be set to True so downstream expressions can distinguish a real invocation from a dry-run preview (where executed is forced to False).
        if dispatch_result is not None:
            output["exit_code"] = dispatch_result["exit_code"]
            output["stdout"] = dispatch_result["stdout"]
            output["stderr"] = dispatch_result["stderr"]
            output["dispatched"] = True
  • Files reviewed: 6/6 changed files
  • Comments generated: 3

Comment thread src/specify_cli/workflows/engine.py
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/workflows/steps/command/__init__.py
- PromptStep now honors context.dry_run: renders a preview with
  executed=False, dispatched=False, exit_code=0, dry_run=True,
  and a DRY RUN message. Without this, a workflow with
  type: prompt would still spawn the integration CLI even in
  dry-run mode, contradicting the docstring claim that dry_run
  skips AI invocation across the board.

- workflow_run's dry-run preview loop is no longer gated on
  state.status == 'completed'. Dry-run previews print regardless
  of the run's final status (completed / failed / paused), so a
  dry-run that fails mid-run still surfaces the prompts / command
  invocations that would have been resolved up to the point of
  failure. The --json branch is still suppressed (the early
  return for json_output returns before the loop).

- CommandStep real-run path now sets output['executed'] = True,
  and the no-dispatch (CLI-not-found) branch sets it False. The
  dry-run branch already sets it False. Downstream
  {{ steps.<id>.output.executed }} expressions can now reliably
  key on the field regardless of which branch executed.

- New test test_dry_run_prompt_short_circuits covers PromptStep
  dry-run. Existing test_dispatch_with_mock_cli now also asserts
  executed is True on the real-run success path.
Copilot AI review requested due to automatic review settings June 8, 2026 13:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/workflows/steps/command/__init__.py
Copy link
Copy Markdown
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Copilot feedback

@fuleinist
Copy link
Copy Markdown
Author

Hi @mnriem — all Copilot feedback from the 2026-06-01 cycle has been addressed in commit 608d414 (pushed 2026-06-08):

  • PromptStep now honors context.dry_run — same short-circuit contract as CommandStep/GateStep. Docstring in engine.py:execute() is no longer inaccurate.
  • Dry-run preview loop in workflow_run no longer gated on state.status == 'completed' — previews print even when the run fails mid-way.
  • output['executed'] set reliably on every path: True on real-run success, False on dry-run and no-dispatch branches.
  • Integration fallback now emits a preview_note when get_integration() returns None, so dry-run output is explicit about the fallback.
  • Unused step_id variable in GateStep dry-run branch removed.
  • Hard-coded project_root='/tmp' replaced with pytest's tmp_path fixture.

Would you mind taking another look? #2704

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add dry-run flag to preview spec output without AI invocation

3 participants