Skip to content

[Bug]: while/do-while loop condition reads stale iteration-0 step output #2592

@doquanghuy

Description

@doquanghuy

Bug Description

workflows/engine.py namespaces nested step IDs per iteration in
while / do-while loops:

# engine.py around line 678
ns_copy["id"] = f"{step_id}:{ns_copy['id']}:{_loop_iter + 1}"

But the loop's condition (resolved via
expressions.evaluate_condition
_resolve_dot_path_build_namespace) reads step output by the
unprefixed step ID. So a condition like
{{ steps.my-gate.output.choice }} != 'approve' reads the
iteration-0 value on every subsequent iteration, because
steps.my-gate in the namespace points at the original
un-namespaced step output and is never updated to track
steps.my-gate:my-loop:1, :2, etc.

Net: gate-driven loop termination based on a gate's verdict
never fires. The loop runs to max_iterations.

Steps to Reproduce

schema_version: "1.0"
workflow:
  id: while-loop-bug-repro
  name: "While-loop namespacing bug reproducer"
  version: "0.0.1"
  workflow_status: test
requires:
  speckit_version: ">=0.8.7"
  integrations:
    any: ["claude"]
inputs: {}
steps:
  - id: my-loop
    type: while
    condition: "{{ steps.my-gate.output.choice }} != 'approve'"
    max_iterations: 5
    body:
      - id: my-gate
        type: gate
        prompt: "Approve, improve, rebuild, or abort?"
        choices: ["approve", "improve", "rebuild", "abort"]
specify workflow run while-loop-bug-repro
# choose 'approve' on the first prompt

Expected Behavior

Loop terminates after iteration 1; condition evaluates false.

Actual Behavior

Loop continues for all max_iterations (= 5), prompting again on
every iteration. Condition evaluator on iteration 1+ reads the
iteration-0 stale output.choice.

Specify CLI Version

engine.py byte-identical (sha256 543dbb3676…) on every recent
0.8.x release; bug present in current main (0.8.12.dev0).

AI Agent

Claude Code

Operating System

Any (pure-Python engine code; not platform-specific).

Python Version

Python 3.11+

Error Logs

Not an error — silent wrong behaviour. The loop runs to
max_iterations and exits normally, ignoring the gate verdict.

Additional Context

Scope: the bug is in the workflow engine's condition resolver
(expressions.evaluate_condition), not in any integration. The
reproducer happens to use Claude as the integration the gate step
runs through, but the bug manifests identically with any agent —
the dropdown above just reflects the reproducer environment.

while and do-while are documented step kinds (engine.py:97).
If they don't work for the canonical use case (gate-driven
termination), they're effectively dead. Workflow authors today must
manually unroll review loops into nested switch-after-gate
chains, capped at 2 iterations.

Possible fix shapes

  • A. Inside a loop body, expose the most recent iteration's
    nested-step output under an alias like
    {{ steps.my-gate.latest.output.choice }}.
  • B. Auto-resolve unprefixed step refs in loop conditions to
    the latest namespaced version. Backward-compatible — existing
    condition text starts working.
  • C. Expose iteration counter as {{ context.iteration }} and
    let condition authors reference iteration-suffixed IDs manually.

B is least friction. Want shape agreement before opening a PR
(engine internals).

AI disclosure: drafted with Claude Opus including reproducer +
analysis. Human-reviewed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions