Repository navigation
fix(workflows): stop dispatching "None" as command step arguments - #5016
Merged
Merged
Conversation
A command step's `args` that is a single `{{ }}` block keeps the
expression's typed value, so a reference with nothing behind it resolves
to None: an optional input that was not supplied, or an output key the
earlier step does not produce. The documented example
`args: "{{ steps.specify.output.file }}"` is one, since command steps
have no `file` output. `execute()` passed `str(None)` to the
integration, so the agent received `/speckit-plan None`. The same value
inside a longer template renders as empty text, and a missing `args` key
already means no arguments.
Treat a null `args` as no arguments. Values that are only falsy (0,
false, the empty string) are dispatched as before.
Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The review records no remaining active issues for the focused null-argument dispatch fix.
No open issues 🎉
What changed in this PR
This pull request fixes command-step dispatch when a standalone argument expression resolves to None, preventing the literal string None from being passed to agent CLIs while preserving existing behavior for other values.
Changes:
- Treat null command arguments as empty arguments.
- Add regression coverage for missing inputs and outputs.
- Preserve handling of
0,False, empty strings, and mixed templates.
| File | Description |
|---|---|
tests/test_workflows.py |
Adds regression and control-case coverage for command argument resolution. |
src/specify_cli/workflows/step/command/__init__.py |
Prevents null command arguments from being dispatched as the literal None. |
Review effort: Lite (auto)
KSchlobohm
approved these changes
Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
When a command step's
argsis a single{{ }}expression that resolves to null, the agent is invoked with the literal textNoneas its arguments:/speckit-plan None. Two common ways to get there:The documented example
args: "{{ steps.specify.output.file }}"(workflows/README.md) is one, since command steps have nofileoutput.Reproduction with a stand-in
claudeonPATHthat logs its argv:Root cause:
CommandStep.execute()doesargs_str = str(resolved_input.get("args", "")). A lone{{ }}block keeps the expression's typed value (expressions.py), so a reference with nothing behind it isNone, andstr(None)is"None". The same value inside a longer template is rendered as empty text, and a missingargskey already means no arguments.The fix treats a null
argsas no arguments (6 lines). Values that are only falsy (0,false,"") and mixed templates are dispatched as before.Prompt and shell steps have the same
str(None)pattern, but they are deliberately left out. Forshell, an empty command would turn a visible failure into a silent success. Forprompt, sending nothing versus failing is a separate design question.Testing
Tested locally with
uv run specify --helpRan existing tests with
uv sync && uv run pytestTested with a sample project (if applicable): the stand-in
claudereproduction above, onmainand on this branchNew parametrized test
TestCommandStep.test_dispatch_args_resolving_to_null_send_no_argumentsintests/test_workflows.py. It covers 2 null cases ({{ inputs.feature }}unset,{{ steps.specify.output.file }}) and 4 controls (0,False,"", a mixed template).The null cases fail on
mainwithAssertionError: assert '/speckit-plan None' == '/speckit-plan'and the controls pass. With the change all 35TestCommandSteptests pass.Full suite (macOS, Python 3.13): 12336 passed, 376 skipped. 11 failures in the
tests/test_*_python_parity.pyfiles occur identically on unmodifiedmainon this machine under full-suite load; those files pass when run on their own (257 passed, 112 skipped).uvx ruff@0.15.0 check src tests: cleanAI Disclosure
AI disclosure: Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode) opened this PR on behalf of @yxshee. A Claude Code subagent (same model, autonomous) found the bug and wrote the fix and tests. The lead agent independently re-ran the new tests against
mainand the branch, reproduced the argv above with the real CLI, reviewed the diff, and wrote this description. No human reviewed the change line by line before it was opened.🤖 Generated with Claude Code