Skip to content

fix(workflows): stop dispatching "None" as command step arguments - #5016

Open
yxshee wants to merge 1 commit into
github:mainfrom
yxshee:fix/command-step-null-args
Open

yxshee wants to merge 1 commit into
github:mainfrom
yxshee:fix/command-step-null-args

Conversation

@yxshee

@yxshee yxshee commented Oct 17, 2026

Copy link
Copy Markdown

Description

When a command step's args is a single {{ }} expression that resolves to null, the agent is invoked with the literal text None as its arguments: /speckit-plan None. Two common ways to get there:

  • an optional input that wasn't supplied
  • an output key the earlier step doesn't produce

The documented example args: "{{ steps.specify.output.file }}" (workflows/README.md) is one, since command steps have no file output.

Reproduction with a stand-in claude on PATH that logs its argv:

# args.yml
schema_version: "1.0"
workflow: {id: "args-repro", name: "Args repro", version: "1.0.0"}
inputs:
  feature: {type: string, required: false}
steps:
  - id: plan
    command: speckit.plan
    integration: claude
    input:
      args: "{{ inputs.feature }}"
$ specify workflow run args.yml          # feature not supplied
# main:        claude argv: [-p] [/speckit-plan None]
# this branch: claude argv: [-p] [/speckit-plan]

Root cause: CommandStep.execute() does args_str = str(resolved_input.get("args", "")). A lone {{ }} block keeps the expression's typed value (expressions.py), so a reference with nothing behind it is None, and str(None) is "None". The same value inside a longer template is rendered as empty text, and a missing args key already means no arguments.

The fix treats a null args as 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. For shell, an empty command would turn a visible failure into a silent success. For prompt, sending nothing versus failing is a separate design question.

Testing

  • Tested locally with uv run specify --help

  • Ran existing tests with uv sync && uv run pytest

  • Tested with a sample project (if applicable): the stand-in claude reproduction above, on main and on this branch

  • New parametrized test TestCommandStep.test_dispatch_args_resolving_to_null_send_no_arguments in tests/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 main with AssertionError: assert '/speckit-plan None' == '/speckit-plan' and the controls pass. With the change all 35 TestCommandStep tests pass.

  • Full suite (macOS, Python 3.13): 12336 passed, 376 skipped. 11 failures in the tests/test_*_python_parity.py files occur identically on unmodified main on 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: clean

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

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 main and 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

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>
@yxshee
yxshee requested a review from mnriem as a code owner October 17, 2026 19:21
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Oct 18, 2026

This branch has not been deployed

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

Labels

triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants