feat(workflows): move --dry-run to specify workflow run; remove from specify spec/plan#2704
feat(workflows): move --dry-run to specify workflow run; remove from specify spec/plan#2704fuleinist wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
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_runonWorkflowEngine.execute()and propagates it throughStepContext. - Implements dry-run behavior for
CommandStep(skip CLI dispatch) andGateStep(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.
|
Please address Copilot feedback |
…ut AI invocation Implements GitHub issue github#2661. - Add dry_run field to StepContext (workflows/base.py) - Add dry_run parameter to WorkflowEngine.execute() (workflows/engine.py) - Add --dry-run to 'specify workflow run' CLI command - Add 'specify specify' and 'specify plan' CLI commands with --dry-run support - CommandStep: in dry-run mode, renders the command/integration/model and returns COMPLETED without spawning the integration CLI subprocess - GateStep: in dry-run mode, skips interactive prompt and returns COMPLETED - Add tests for dry-run in TestCommandStep, TestGateStep, and TestWorkflowEngine Usage: specify specify --spec 'Build a kanban board' --dry-run specify plan --spec 'Build a kanban board' --dry-run specify workflow run speckit --input spec='Build kanban' --dry-run
- Set exit_code=0 in dry-run mode (CommandStep) instead of None, matching the COMPLETED status and not breaking expression evaluation - Add dry_run parameter documentation to WorkflowEngine.execute() docstring - Fix contradictory 'Run with --dry-run' hint messages in specify specify/plan commands (the message appeared inside the dry-run block itself)
7a3db5a to
d271c5c
Compare
|
All four review items addressed in the latest commits:
Branch rebased onto latest main and force-pushed to |
Avoids 'specify specify specify' CLI path by using 'specify spec' instead. Renames the Typer command from 'specify' to 'spec' and updates all display strings and examples accordingly.
There was a problem hiding this comment.
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!
…c/plan DRY RUN only meaningful for step-based workflow execution. CLI spec/plan only does scaffolding — no AI invocation there. BREAKING CHANGE: --dry-run removed from specify spec and specify plan. ADDED: specify workflow run --dry-run surfaces command/gate step outputs.
|
Review 4382194003 addressed. Summary:
Follow-up items for next PR:
Commit: 6a074ba on feat/2661-dry-run |
workflow commands already registered inline at line ~4160 via app.add_typer(workflow_app). The commands.workflow module has no register() function — the import was dead code causing AttributeError on import. Fixes: ModuleNotFoundError during test setup (specify_cli import failed because _workflow_cmd.register(app) threw AttributeError)
- 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>
|
Fixed in latest commit (8fa7bbc): Item #10 (step isolation): Added Item #11 (dry-run output): After execution, Commit: 8fa7bbc on |
…unused import - specify spec/plan: actually print the computed status_color after execution (was assigned but never used — fixes Copilot comment #9995/#9997) - commands/workflow.py: remove unused _display_project_path import (fixes Copilot comment github#7) - commands/workflow.py: add f-string prefix to resume message so {run_id} is interpolated, not printed literally (fixes Copilot comment github#20)
…d_invocation in dry-run - commands/workflow.py: add register(app) and wire it from __init__.py (fixes dead-code Copilot comment github#16) - commands/workflow.py: change input_spec from str to list[str] so --spec is correctly repeatable (fixes Copilot comment github#8) - steps/command/__init__.py: dry-run now calls impl.build_command_invocation() instead of manual string concat so preview matches real dispatch (fixes Copilot comment github#3)
- workflow run --dry-run now prints state.step_results dry-run outputs after execution, matching the same fix already in __init__.py (fixes Copilot comment #0000)
| # ===== workflow commands ===== | ||
| from .commands import workflow as _workflow_cmd # noqa: E402 | ||
| _workflow_cmd.register(app) |
| specify_app = typer.Typer( | ||
| name="specify", | ||
| help="Create a feature specification (direct CLI alternative to /speckit.specify in coding agents)", | ||
| add_completion=False, | ||
| ) | ||
| app.add_typer(specify_app, name="specify") | ||
|
|
| app.add_typer(_self_app, name="self") | ||
|
|
||
|
|
||
| # ===== Spec / Plan Commands (direct CLI access with dry-run) ===== |
| If ``True``, each step is executed normally but without | ||
| invoking the underlying AI integration (e.g. no CLI subprocess | ||
| is spawned for ``command`` steps, interactive gates return | ||
| ``COMPLETED`` immediately, etc.). The workflow state is | ||
| still persisted to disk so ``specify workflow resume`` works. | ||
| Use this to preview the resolved inputs and prompts for a | ||
| workflow without making any AI API calls. |
| # Filter steps by start_at / stop_after | ||
| steps_to_run = definition.steps | ||
| if start_at is not None or stop_after is not None: | ||
| filtered = [] | ||
| started = start_at is None | ||
| for step_cfg in steps_to_run: | ||
| step_id = step_cfg.get("id", "") | ||
| if not started and step_id == start_at: | ||
| started = True | ||
| if started: | ||
| filtered.append(step_cfg) | ||
| if stop_after is not None and step_id == stop_after: | ||
| break | ||
| steps_to_run = filtered | ||
|
|
| If ``True``, each step is executed normally but without | ||
| invoking the underlying AI integration (e.g. no CLI subprocess | ||
| is spawned for ``command`` steps, interactive gates return | ||
| ``COMPLETED`` immediately, etc.). The workflow state is | ||
| still persisted to disk so ``specify workflow resume`` works. | ||
| Use this to preview the resolved inputs and prompts for a | ||
| workflow without making any AI API calls. |
| # Filter steps by start_at / stop_after | ||
| steps_to_run = definition.steps | ||
| if start_at is not None or stop_after is not None: | ||
| filtered = [] | ||
| started = start_at is None | ||
| for step_cfg in steps_to_run: | ||
| step_id = step_cfg.get("id", "") | ||
| if not started and step_id == start_at: | ||
| started = True | ||
| if started: | ||
| filtered.append(step_cfg) | ||
| if stop_after is not None and step_id == stop_after: | ||
| break | ||
| steps_to_run = filtered | ||
|
|
| # Dry-run: skip interactive gates | ||
| if context.dry_run: | ||
| return StepResult( | ||
| status=StepStatus.COMPLETED, | ||
| output=output, | ||
| ) |
| if state.status.value == "paused": | ||
| console.print(f"[dim]Run ID: {state.run_id}[/dim]") | ||
| console.print(f"[dim]Resume with: specify workflow resume {run_id}[/dim]") |
| console.print(f"[red]Error:[/red] Workflow not found: {workflow_id!r}") | ||
| console.print("Run 'specify workflow list' to see installed workflows") | ||
| raise typer.Exit(1) |
|
Re-requesting review on @mnriem's CHANGES_REQUESTED — the design concerns have been reworked in subsequent commits. Design reworked to match your guidance:
Latest commit: b9b78e7 on Files changed: src/specify_cli/init.py, src/specify_cli/commands/workflow.py (new), src/specify_cli/workflows/{base,engine}.py, tests/test_workflows.py If |
mnriem
left a comment
There was a problem hiding this comment.
Did you address the Copilot feedback prior to your comment? Note I did trigger another Copilot cycle after your comment prematurely
| # ===== workflow commands ===== | ||
| from .commands import workflow as _workflow_cmd # noqa: E402 | ||
| _workflow_cmd.register(app) |
| specify_app = typer.Typer( | ||
| name="specify", | ||
| help="Create a feature specification (direct CLI alternative to /speckit.specify in coding agents)", | ||
| add_completion=False, | ||
| ) | ||
| app.add_typer(specify_app, name="specify") | ||
|
|
| app.add_typer(_self_app, name="self") | ||
|
|
||
|
|
||
| # ===== Spec / Plan Commands (direct CLI access with dry-run) ===== |
| """Create a feature specification from a description. | ||
|
|
||
| This is a direct CLI alternative to the /speckit.specify agent command. | ||
| Runs the spec workflow and generates spec.md in the feature directory. | ||
|
|
| ``COMPLETED`` immediately, etc.). The workflow state is | ||
| still persisted to disk so ``specify workflow resume`` works. | ||
| Use this to preview the resolved inputs and prompts for a | ||
| workflow without making any AI API calls. |
| # Filter steps by start_at / stop_after | ||
| steps_to_run = definition.steps | ||
| if start_at is not None or stop_after is not None: | ||
| filtered = [] | ||
| started = start_at is None | ||
| for step_cfg in steps_to_run: | ||
| step_id = step_cfg.get("id", "") | ||
| if not started and step_id == start_at: | ||
| started = True | ||
| if started: | ||
| filtered.append(step_cfg) | ||
| if stop_after is not None and step_id == stop_after: | ||
| break | ||
| steps_to_run = filtered |
| if state.status.value == "paused": | ||
| console.print(f"[dim]Run ID: {state.run_id}[/dim]") | ||
| console.print(f"[dim]Resume with: specify workflow resume {run_id}[/dim]") |
| if input_spec: | ||
| for item in input_spec: | ||
| if "=" in item: | ||
| key, value = item.split("=", 1) | ||
| inputs[key.strip()] = value.strip() |
| @workflow_app.command("run") | ||
| def workflow_run( | ||
| workflow_id: str = typer.Argument(..., help="Workflow ID or path to workflow YAML"), | ||
| input_spec: list[str] = typer.Option( | ||
| None, "--spec", "-s", help="Workflow input as key=value pairs (repeatable)" | ||
| ), | ||
| dry_run: bool = typer.Option( | ||
| False, "--dry-run", help="Preview step execution without invoking AI (no commands spawned, gates return COMPLETED)" | ||
| ), | ||
| run_id: str = typer.Option(None, "--run-id", help="Custom run ID (auto-generated if not provided)"), |
|
Please address Copilot feedback |
Summary
Implements GitHub issue #2661 — add a
--dry-runflag tospecify workflow runfor previewing step execution without AI invocation. Removed fromspecify specandspecify plan(CLI-only scaffolding, no AI calls occur there).Changes
Core engine
src/specify_cli/workflows/base.py:StepContexthasdry_run: bool = Falsesrc/specify_cli/workflows/engine.py:execute(dry_run=False)propagates to steps; documents semantics in docstringCLI commands
src/specify_cli/__init__.py:specify spec/specify plan— CLI scaffolding only; no AI invocation, no--dry-runflagspecify workflow run --dry-run— step-based execution with dry-run previewStep behavior
CommandStep(workflows/steps/command/):dry_run=True→ renders invoke_command/integration/model, setsexit_code=0, returnsCOMPLETEDwithout spawning CLIGateStep(workflows/steps/gate/):dry_run=True→ returnsCOMPLETEDimmediately without interactive promptBug fixes (review-driven)
exit_codeset to0in dry-run (notNone) — matchesCOMPLETED, avoids downstream expression errorsexecute()docstring now documentsdry_runsemantics fullyspecify spec/specify plan(not triple-nested)Tests
tests/test_workflows.py: 3 dry-run tests (CommandStep, GateStep, WorkflowEngine) — all passingUsage
Follow-up items (not in this PR)
GateStepdeterministic choice in dry-run (first option)start_at/stop_afterstep ID filtering for engine-level spec/plan/implement isolationdry_runinRunStatefor safe resume of interrupted dry-runsCloses #2661