Skip to content

[Feature]: Make the shell step's execution timeout configurable (currently hardcoded to 300s) #3327

Description

@mdnyeemakhtar

Problem Statement

The workflow shell step hardcodes a 300-second execution timeout with no way to override it. In init.py, ShellStep.execute runs:

proc = subprocess.run(
    run_cmd, shell=True, capture_output=True, text=True, cwd=cwd, timeout=300,
)

and the step config is only ever read for run and output_format (in both execute and validate). Any shell command that legitimately takes longer than 5 minutes — a full build, a linter aggregator, an integration-test target — is killed with TimeoutExpired, which the step maps to StepResult(status=FAILED, output={"exit_code": -1, "stderr": "timeout"}), failing the entire run.

This makes shell steps unusable as a probe/gate over real project QA commands. Concretely: a step running a repo's make code-qa (a MegaLinter/golangci-lint aggregator + build + tests, ~10+ min) fails at exactly 300s regardless of whether the command itself would pass, and there is no YAML knob to raise the limit.

Proposed Solution

Add an optional timeout field (seconds) to the shell step, defaulting to the current 300 for backward compatibility:

- id: qa
  type: shell
  timeout: 1800        # seconds; optional, default 300
  run: make code-qa

The change is minimal and additive:

timeout = config.get("timeout", 300)
proc = subprocess.run(
    run_cmd, shell=True, capture_output=True, text=True, cwd=cwd, timeout=timeout,
)

plus a validate check that timeout, when present, is a positive number. (Optionally timeout: 0/null could mean "no timeout" for intentionally long-running steps, though requiring an explicit positive value may be preferable to avoid runaway steps.)

Alternatives Considered

Global env var (e.g. SPECKIT_SHELL_TIMEOUT): coarser than per-step and easy to forget; a per-step field is more precise. Could be layered in as a lower-precedence default.

Component

Specify CLI (initialization, commands)

AI Agent (if applicable)

None

Use Cases

No response

Acceptance Criteria

  • A timeout: value on a shell step overrides the 300s default and is honored by subprocess.run.
  • Omitting timeout: preserves the current behavior (300s).
  • validate rejects a non-positive or non-numeric timeout.
  • A test asserts the configured value is threaded through (and/or that a step which would exceed the default succeeds when the timeout is raised).

Additional Context

  • Per-step timeouts are standard in comparable systems (e.g. GitHub Actions timeout-minutes), so this is an idiomatic, non-breaking addition.
  • shell is the only step type with a hardcoded wall-clock limit; command steps (agent subprocess) have no equivalent cap — so long-running work is blocked only when it's a plain shell command.
  • Source: init.py (as of 0.12.5.dev0).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    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