steerdev 1.1.7__tar.gz → 1.1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {steerdev-1.1.7 → steerdev-1.1.14}/.github/workflows/publish.yml +4 -3
  2. {steerdev-1.1.7 → steerdev-1.1.14}/PKG-INFO +1 -1
  3. steerdev-1.1.14/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +253 -0
  4. steerdev-1.1.14/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +130 -0
  5. {steerdev-1.1.7 → steerdev-1.1.14}/pyproject.toml +1 -1
  6. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/cli.py +10 -6
  7. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/runner.py +16 -0
  8. steerdev-1.1.14/tests/test_resolve_max_tasks.py +27 -0
  9. {steerdev-1.1.7 → steerdev-1.1.14}/.github/workflows/pre-commit.yml +0 -0
  10. {steerdev-1.1.7 → steerdev-1.1.14}/.gitignore +0 -0
  11. {steerdev-1.1.7 → steerdev-1.1.14}/.pre-commit-config.yaml +0 -0
  12. {steerdev-1.1.7 → steerdev-1.1.14}/AGENTS.md +0 -0
  13. {steerdev-1.1.7 → steerdev-1.1.14}/CLAUDE.md +0 -0
  14. {steerdev-1.1.7 → steerdev-1.1.14}/README.md +0 -0
  15. {steerdev-1.1.7 → steerdev-1.1.14}/scripts/pre-commit-version-bump.sh +0 -0
  16. {steerdev-1.1.7 → steerdev-1.1.14}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
  17. {steerdev-1.1.7 → steerdev-1.1.14}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
  18. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/__init__.py +0 -0
  19. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/agent_loop.py +0 -0
  20. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/__init__.py +0 -0
  21. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/activity.py +0 -0
  22. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/agents.py +0 -0
  23. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/canals.py +0 -0
  24. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/client.py +0 -0
  25. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/commands.py +0 -0
  26. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/configs.py +0 -0
  27. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/context.py +0 -0
  28. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/events.py +0 -0
  29. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/hooks.py +0 -0
  30. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/implementation_plan.py +0 -0
  31. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/merger.py +0 -0
  32. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/messages.py +0 -0
  33. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/prd.py +0 -0
  34. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/reports.py +0 -0
  35. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/runs.py +0 -0
  36. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/sessions.py +0 -0
  37. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/specs.py +0 -0
  38. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/tasks.py +0 -0
  39. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/workflow_runs.py +0 -0
  40. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/api/workflows.py +0 -0
  41. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/config/__init__.py +0 -0
  42. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/config/models.py +0 -0
  43. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/config/platform.py +0 -0
  44. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/config/settings.py +0 -0
  45. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/evidence.py +0 -0
  46. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/evidence_assets.py +0 -0
  47. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/executor/__init__.py +0 -0
  48. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/executor/base.py +0 -0
  49. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/executor/claude.py +0 -0
  50. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/executor/stream.py +0 -0
  51. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/handlers/__init__.py +0 -0
  52. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/handlers/prd.py +0 -0
  53. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/integration.py +0 -0
  54. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/prompt/__init__.py +0 -0
  55. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/prompt/builder.py +0 -0
  56. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/prompt/templates.py +0 -0
  57. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  58. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/py.typed +0 -0
  59. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/retry.py +0 -0
  60. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/__init__.py +0 -0
  61. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/claude_setup.py +0 -0
  62. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/repo_setup.py +0 -0
  63. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  64. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  65. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/settings.json +0 -0
  66. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  67. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  68. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  69. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  70. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
  71. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  72. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
  73. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  74. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  75. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
  76. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
  77. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  78. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/update_check.py +0 -0
  79. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/version.py +0 -0
  80. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workflow/__init__.py +0 -0
  81. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workflow/context.py +0 -0
  82. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workflow/executor.py +0 -0
  83. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workflow/memory.py +0 -0
  84. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workspace/__init__.py +0 -0
  85. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workspace/preparation.py +0 -0
  86. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workspace/project_manager.py +0 -0
  87. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  88. {steerdev-1.1.7 → steerdev-1.1.14}/src/steerdev_agent/worktree.py +0 -0
  89. {steerdev-1.1.7 → steerdev-1.1.14}/tests/__init__.py +0 -0
  90. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_agent_loop.py +0 -0
  91. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_agent_loop_extended.py +0 -0
  92. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_agents_api.py +0 -0
  93. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_api_client.py +0 -0
  94. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_claude_executor.py +0 -0
  95. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_claude_setup.py +0 -0
  96. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_client_methods.py +0 -0
  97. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_commands_api.py +0 -0
  98. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_config.py +0 -0
  99. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_config_extended.py +0 -0
  100. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_conflict_mitigation.py +0 -0
  101. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_context_search.py +0 -0
  102. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_executor.py +0 -0
  103. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_platform_config.py +0 -0
  104. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_preparation.py +0 -0
  105. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_prompt.py +0 -0
  106. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_reports_client.py +0 -0
  107. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_retry.py +0 -0
  108. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_runner_merge_modes.py +0 -0
  109. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_runner_worktrees.py +0 -0
  110. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_stream_parser.py +0 -0
  111. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_tasks.py +0 -0
  112. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_version.py +0 -0
  113. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_wave_scoped_fetch.py +0 -0
  114. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_workflow_context.py +0 -0
  115. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_workflow_memory.py +0 -0
  116. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_workflow_prompt_template.py +0 -0
  117. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_workflow_runs_api.py +0 -0
  118. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_workspace.py +0 -0
  119. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_workspace_extended.py +0 -0
  120. {steerdev-1.1.7 → steerdev-1.1.14}/tests/test_worktree.py +0 -0
@@ -1,6 +1,9 @@
1
1
  name: Publish to PyPI
2
2
 
3
3
  on:
4
+ push:
5
+ branches:
6
+ - main
4
7
  workflow_dispatch:
5
8
 
6
9
  permissions:
@@ -53,9 +56,7 @@ jobs:
53
56
  run: uv build
54
57
 
55
58
  - name: Publish to PyPI
56
- env:
57
- UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }}
58
- run: uv publish
59
+ run: uv publish --trusted-publishing always
59
60
 
60
61
  - name: Create git tag and GitHub release
61
62
  env:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerdev
3
- Version: 1.1.7
3
+ Version: 1.1.14
4
4
  Summary: Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting
5
5
  Project-URL: Homepage, https://github.com/pentoai/steerdev-agent
6
6
  Project-URL: Repository, https://github.com/pentoai/steerdev-agent
@@ -0,0 +1,253 @@
1
+ # `--wave-id` implies unlimited `--max-tasks` — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make `steerdev run --wave-id X` (with no `--max-tasks`) process the whole wave, while keeping every other `--max-tasks` case unchanged.
6
+
7
+ **Architecture:** Make the `--max-tasks` CLI option nullable (`None` = unspecified), add a pure `resolve_max_tasks(max_tasks, wave_id)` helper in `runner.py` that returns the effective int (unspecified + wave → 0/unlimited; unspecified + no wave → 1; explicit value always honored), and use it in `cli.py:run` for both the `run_agent(...)` call and the "Max Tasks" banner. `AgentRunner`/`run_agent` are unchanged (still take a concrete int, `0` = unlimited).
8
+
9
+ **Tech Stack:** Python 3.12, Typer CLI, pytest, uv. Lint/format: ruff. Type check: basedpyright. (Repo pre-commit runs ruff + basedpyright + an auto version bump.)
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md`
12
+ **Branch:** `feat/wave-id-implies-unlimited` (already created; the spec is committed there).
13
+
14
+ ---
15
+
16
+ ## File Structure
17
+
18
+ - `src/steerdev_agent/runner.py` — add pure `resolve_max_tasks(max_tasks: int | None, wave_id: str | None) -> int` next to the other pure resolvers (`resolve_merge_flow` is at ~line 139). One clear responsibility: resolve the effective max-tasks. No I/O.
19
+ - `src/steerdev_agent/cli.py` — in the `run` command: change the `--max-tasks` option to `int | None = None`, call `resolve_max_tasks` to get `resolved_max_tasks`, and use it for both the `max_tasks_display` banner (~line 1824) and the `max_tasks=` argument to `run_agent` (~line 1875).
20
+ - `tests/test_resolve_max_tasks.py` — new pure unit tests for `resolve_max_tasks`.
21
+
22
+ ---
23
+
24
+ ## Task 1: Pure `resolve_max_tasks` helper (TDD)
25
+
26
+ **Files:**
27
+ - Modify: `src/steerdev_agent/runner.py` (add the function near `resolve_merge_flow`, ~line 139)
28
+ - Test: `tests/test_resolve_max_tasks.py` (new)
29
+
30
+ - [ ] **Step 1: Write the failing test**
31
+
32
+ Create `tests/test_resolve_max_tasks.py` with exactly:
33
+
34
+ ```python
35
+ """Tests for resolve_max_tasks: --wave-id implies unlimited when --max-tasks is unset."""
36
+
37
+ from steerdev_agent.runner import resolve_max_tasks
38
+
39
+
40
+ def test_unspecified_with_wave_is_unlimited():
41
+ # Bare `steerdev run --wave-id X` → pull the whole wave (0 = unlimited).
42
+ assert resolve_max_tasks(None, "wave-1") == 0
43
+
44
+
45
+ def test_unspecified_without_wave_is_one():
46
+ # Historical single-task default is preserved.
47
+ assert resolve_max_tasks(None, None) == 1
48
+
49
+
50
+ def test_explicit_zero_is_honored_with_wave():
51
+ # The sandbox bootstrap passes --max-tasks 0 explicitly; still 0.
52
+ assert resolve_max_tasks(0, "wave-1") == 0
53
+
54
+
55
+ def test_explicit_cap_is_honored_with_wave():
56
+ # An explicit cap wins even when a wave is scoped.
57
+ assert resolve_max_tasks(2, "wave-1") == 2
58
+
59
+
60
+ def test_explicit_is_honored_without_wave():
61
+ assert resolve_max_tasks(5, None) == 5
62
+ ```
63
+
64
+ - [ ] **Step 2: Run test to verify it fails**
65
+
66
+ Run: `uv run pytest tests/test_resolve_max_tasks.py -q`
67
+ Expected: FAIL — `ImportError: cannot import name 'resolve_max_tasks' from 'steerdev_agent.runner'`.
68
+
69
+ - [ ] **Step 3: Implement the helper**
70
+
71
+ In `src/steerdev_agent/runner.py`, add this function immediately ABOVE `def resolve_merge_flow(` (around line 139), as a module-level function:
72
+
73
+ ```python
74
+ def resolve_max_tasks(max_tasks: int | None, wave_id: str | None) -> int:
75
+ """Resolve the effective --max-tasks for a run.
76
+
77
+ - An explicit value (including 0 = unlimited) always wins.
78
+ - Unspecified (None) with a wave scope → 0 (unlimited): a wave run pulls
79
+ every task of the wave, then stops. This makes a bare
80
+ `steerdev run --wave-id X` behave like the sandbox's
81
+ `steerdev run --wave-id X --max-tasks 0`.
82
+ - Unspecified (None) with no wave scope → 1 (the historical single-task
83
+ default).
84
+ """
85
+ if max_tasks is not None:
86
+ return max_tasks
87
+ return 0 if wave_id else 1
88
+ ```
89
+
90
+ - [ ] **Step 4: Run test to verify it passes**
91
+
92
+ Run: `uv run pytest tests/test_resolve_max_tasks.py -q`
93
+ Expected: PASS (5 passed).
94
+
95
+ - [ ] **Step 5: Commit**
96
+
97
+ ```bash
98
+ git add src/steerdev_agent/runner.py tests/test_resolve_max_tasks.py
99
+ git commit -m "feat: add resolve_max_tasks (--wave-id implies unlimited)"
100
+ ```
101
+
102
+ Note: the repo pre-commit runs ruff, basedpyright, and an auto version bump. If ruff reformats, re-add and re-commit. If basedpyright complains about the new function, fix the typing (signature is fully annotated, so it should pass). The version-bump hook modifying a version file is expected — let it include that change.
103
+
104
+ ---
105
+
106
+ ## Task 2: Wire `resolve_max_tasks` into the `run` CLI command
107
+
108
+ **Files:**
109
+ - Modify: `src/steerdev_agent/cli.py` (the `run` command — the `max_tasks` option definition ~line 1646-1652; the `max_tasks_display` line ~1824; the `run_agent(... max_tasks=max_tasks ...)` call ~1875)
110
+
111
+ This task has no new unit test of its own (the logic is the Task 1 helper; the CLI wiring is thin glue). It is verified by a manual `--help`/dry-run check and the full suite.
112
+
113
+ - [ ] **Step 1: Make the `--max-tasks` option nullable**
114
+
115
+ In `src/steerdev_agent/cli.py`, find the `max_tasks` parameter in the `run` command (currently):
116
+
117
+ ```python
118
+ max_tasks: Annotated[
119
+ int,
120
+ typer.Option(
121
+ "--max-tasks",
122
+ help="Maximum number of tasks to process (default: 1, use 0 for unlimited)",
123
+ ),
124
+ ] = 1,
125
+ ```
126
+
127
+ Replace it with:
128
+
129
+ ```python
130
+ max_tasks: Annotated[
131
+ int | None,
132
+ typer.Option(
133
+ "--max-tasks",
134
+ help=(
135
+ "Maximum number of tasks to process (default: 1, or unlimited "
136
+ "when --wave-id is set; use 0 for unlimited)"
137
+ ),
138
+ ),
139
+ ] = None,
140
+ ```
141
+
142
+ - [ ] **Step 2: Import and call `resolve_max_tasks`**
143
+
144
+ In `src/steerdev_agent/cli.py`, the `run` command already imports from the runner at the top of its body:
145
+ `from steerdev_agent.runner import run_agent`.
146
+ Change that import to also bring in the helper:
147
+
148
+ ```python
149
+ from steerdev_agent.runner import resolve_max_tasks, run_agent
150
+ ```
151
+
152
+ Then, BEFORE the `console.print(Panel(...))` banner block (i.e. before the `max_tasks_display` line ~1824), add:
153
+
154
+ ```python
155
+ resolved_max_tasks = resolve_max_tasks(max_tasks, wave_id)
156
+ ```
157
+
158
+ - [ ] **Step 3: Use `resolved_max_tasks` in the banner**
159
+
160
+ Change the display line (~1824) from:
161
+
162
+ ```python
163
+ max_tasks_display = "unlimited" if max_tasks == 0 else str(max_tasks)
164
+ ```
165
+
166
+ to:
167
+
168
+ ```python
169
+ max_tasks_display = "unlimited" if resolved_max_tasks == 0 else str(resolved_max_tasks)
170
+ ```
171
+
172
+ - [ ] **Step 4: Use `resolved_max_tasks` in the run_agent call**
173
+
174
+ In the `run_agent(...)` call (~line 1875), change:
175
+
176
+ ```python
177
+ max_tasks=max_tasks,
178
+ ```
179
+
180
+ to:
181
+
182
+ ```python
183
+ max_tasks=resolved_max_tasks,
184
+ ```
185
+
186
+ - [ ] **Step 5: Verify the CLI resolves correctly (dry run + help)**
187
+
188
+ Run these and read the "Max Tasks" line in the Starting panel (use `--dry` so nothing executes; `--project-id x` satisfies the required arg):
189
+
190
+ ```bash
191
+ uv run steerdev run --project-id x --wave-id w1 --dry 2>&1 | grep -i "max tasks"
192
+ ```
193
+ Expected: `Max Tasks: unlimited`
194
+
195
+ ```bash
196
+ uv run steerdev run --project-id x --dry 2>&1 | grep -i "max tasks"
197
+ ```
198
+ Expected: `Max Tasks: 1`
199
+
200
+ ```bash
201
+ uv run steerdev run --project-id x --wave-id w1 --max-tasks 2 --dry 2>&1 | grep -i "max tasks"
202
+ ```
203
+ Expected: `Max Tasks: 2`
204
+
205
+ If `--dry` exits before printing the panel or requires more args, instead run `uv run steerdev run --help` to confirm the `--max-tasks` help text now mentions "unlimited when --wave-id is set", and rely on Task 1's unit tests + the full suite for the resolution logic. Report which verification path you used.
206
+
207
+ - [ ] **Step 6: Run the full suite + type check**
208
+
209
+ Run: `uv run pytest -q`
210
+ Expected: all pass (the new 5 tests included; no regressions).
211
+
212
+ Run: `uv run basedpyright src/steerdev_agent/cli.py src/steerdev_agent/runner.py`
213
+ Expected: no new errors. (`max_tasks` is now `int | None`; `resolved_max_tasks` is `int`, matching `run_agent`'s `max_tasks: int` param — so no type error at the call site.)
214
+
215
+ - [ ] **Step 7: Commit**
216
+
217
+ ```bash
218
+ git add src/steerdev_agent/cli.py
219
+ git commit -m "feat: steerdev run --wave-id pulls the whole wave by default"
220
+ ```
221
+
222
+ (Pre-commit ruff/basedpyright/version-bump as in Task 1.)
223
+
224
+ ---
225
+
226
+ ## Task 3: Full verification
227
+
228
+ **Files:** none (verification only)
229
+
230
+ - [ ] **Step 1: Full test suite**
231
+
232
+ Run: `uv run pytest -q`
233
+ Expected: all pass.
234
+
235
+ - [ ] **Step 2: Lint + format check**
236
+
237
+ Run: `uv run ruff check . && uv run ruff format --check .`
238
+ Expected: clean. If format check fails, run `uv run ruff format .`, then `git add -A && git commit -m "chore: ruff format"`.
239
+
240
+ - [ ] **Step 3: Type check (whole package)**
241
+
242
+ Run: `uv run basedpyright`
243
+ Expected: no NEW errors introduced by this change. (Pre-existing errors elsewhere, if any, are out of scope — report them but don't fix.)
244
+
245
+ ---
246
+
247
+ ## Notes for the implementer
248
+
249
+ - **Why nullable:** Typer can't distinguish "user typed `--max-tasks 1`" from "default 1" if the default is `1`. Making it `None` lets `resolve_max_tasks` apply the wave-aware default only when the user didn't specify.
250
+ - **`run_agent` is unchanged:** it still takes `max_tasks: int` where `0` means unlimited (`runner.py`: `self.max_tasks = max_tasks if max_tasks != 0 else None`). We only change what value the CLI computes and passes.
251
+ - **Sandbox bootstrap is unaffected:** it passes `--max-tasks 0` explicitly, which `resolve_max_tasks(0, waveId)` returns as `0` — identical to today.
252
+ - **Do NOT** change `runner.py`'s loop, `run_agent`'s signature, or the server. Scope is the helper + the CLI wiring only.
253
+ - **Version-bump hook:** committing in this repo auto-bumps a version file via pre-commit. That extra change landing in your commit is expected; don't fight it.
@@ -0,0 +1,130 @@
1
+ # `steerdev run --wave-id` should pull the whole wave (imply unlimited max-tasks)
2
+
3
+ **Date:** 2026-06-05
4
+ **Status:** Design — awaiting review
5
+ **Scope:** `steerdev-agent` CLI only (`cli.py` option resolution). No server change.
6
+
7
+ ## Problem
8
+
9
+ `steerdev run --wave-id <id>` is meant to "process every task of a specific wave,
10
+ then stop" (its own `--wave-id` help text). But `--max-tasks` defaults to `1`, and
11
+ the run loop is governed solely by `max_tasks` (`runner.py`: `tasks_remaining =
12
+ self.max_tasks`; `0 → None → unlimited`). So a bare `steerdev run --wave-id X`
13
+ runs exactly **one** task and exits — contradicting the help text and surprising
14
+ the user.
15
+
16
+ Today the only way to get whole-wave behavior is `steerdev run --wave-id X
17
+ --max-tasks 0`, which the sandbox bootstrap passes explicitly. A human running it
18
+ locally must remember `--max-tasks 0`.
19
+
20
+ ## Goal
21
+
22
+ `steerdev run --wave-id X` (no `--max-tasks`) pulls **the whole wave** — the run
23
+ process requests the wave's next task from `/api/v1/tasks/next?wave_id=…` after
24
+ each one, until the wave is exhausted (404 → stop). This is the SAME behavior as
25
+ the sandbox; local and sandbox `run --wave-id` are identical. An explicit
26
+ `--max-tasks N` still caps the run if the user wants.
27
+
28
+ ## Key context: one shared behavior, local or sandbox
29
+
30
+ `steerdev run --wave-id` is a single code path. The run process is the puller:
31
+ it loops `get_next_wave(wave_id=…)` and executes each task. The only difference
32
+ between environments is who starts it and whether anything is pre-seeded:
33
+
34
+ - **Sandbox:** the dispatch seeds the first task as a Run row (for tracking), then
35
+ the VM runs `steerdev run --wave-id --max-tasks 0` which pulls the rest.
36
+ - **Local CLI:** nothing is dispatched; the human runs `steerdev run --wave-id X`
37
+ and it starts pulling from the first task immediately.
38
+
39
+ Standalone (no `--agent-id`) works: the server's no-agent path
40
+ (`getNextWaveTask(projectId, waveId)`) returns the wave's next task without
41
+ conflict-claiming, and the runtime marks each task `started` itself, so the
42
+ state-ordered selection advances correctly on each pull.
43
+
44
+ ## Design
45
+
46
+ Distinguish "user gave `--max-tasks`" from "default" by making the option
47
+ nullable, then resolve the effective value with a pure helper.
48
+
49
+ ### 1. Make `--max-tasks` nullable in `cli.py:run`
50
+
51
+ Change:
52
+ ```python
53
+ max_tasks: Annotated[int, typer.Option("--max-tasks", help="...default: 1...")] = 1,
54
+ ```
55
+ to:
56
+ ```python
57
+ max_tasks: Annotated[
58
+ int | None,
59
+ typer.Option(
60
+ "--max-tasks",
61
+ help="Maximum number of tasks to process (default: 1, or unlimited when --wave-id is set; use 0 for unlimited)",
62
+ ),
63
+ ] = None,
64
+ ```
65
+
66
+ ### 2. Pure resolution helper
67
+
68
+ In `runner.py` (next to the other pure resolvers like `resolve_merge_flow`),
69
+ add:
70
+ ```python
71
+ def resolve_max_tasks(max_tasks: int | None, wave_id: str | None) -> int:
72
+ """Resolve the effective --max-tasks.
73
+
74
+ - Explicit value (incl. 0 = unlimited) wins.
75
+ - Unspecified + a wave scope → 0 (unlimited): a wave run pulls the whole wave.
76
+ - Unspecified + no wave scope → 1 (the historical single-task default).
77
+ """
78
+ if max_tasks is not None:
79
+ return max_tasks
80
+ return 0 if wave_id else 1
81
+ ```
82
+
83
+ ### 3. Use it in `cli.py:run`
84
+
85
+ Before the `run_agent(...)` call, resolve:
86
+ ```python
87
+ resolved_max_tasks = resolve_max_tasks(max_tasks, wave_id)
88
+ ```
89
+ Pass `max_tasks=resolved_max_tasks` to `run_agent`, and use `resolved_max_tasks`
90
+ for the `max_tasks_display` panel line (so the "Max Tasks: unlimited" banner is
91
+ correct).
92
+
93
+ `run_agent`/`AgentRunner` are unchanged — they still take a concrete `int`
94
+ (`0 → unlimited` as today).
95
+
96
+ ## Behavior matrix (after change)
97
+
98
+ | Command | Effective max_tasks | Result |
99
+ |---|---|---|
100
+ | `run --wave-id X` | 0 (unlimited) | whole wave |
101
+ | `run --wave-id X --max-tasks 0` | 0 | whole wave (sandbox bootstrap; unchanged) |
102
+ | `run --wave-id X --max-tasks 2` | 2 | first 2 wave tasks then stop |
103
+ | `run` (no wave) | 1 | one task (unchanged) |
104
+ | `run --max-tasks 0` (no wave) | 0 | unlimited single-task queue drain (unchanged) |
105
+ | `run --task-id T` | (loop breaks after 1 by `_target_task_id` guard) | one task (unchanged) |
106
+
107
+ ## Tests
108
+
109
+ Pure unit tests for `resolve_max_tasks` (in `tests/`, e.g. `test_runner_merge_modes.py`
110
+ style or a new `test_resolve_max_tasks.py`):
111
+ - `resolve_max_tasks(None, "wave-1") == 0` (wave + unspecified → unlimited)
112
+ - `resolve_max_tasks(None, None) == 1` (no wave + unspecified → 1)
113
+ - `resolve_max_tasks(0, "wave-1") == 0` (explicit 0 honored)
114
+ - `resolve_max_tasks(2, "wave-1") == 2` (explicit cap honored, even with a wave)
115
+ - `resolve_max_tasks(5, None) == 5` (explicit honored, no wave)
116
+
117
+ (The CLI wiring + display are thin and covered by the helper; no full CliRunner
118
+ test required, matching the repo's pure-resolver test pattern.)
119
+
120
+ ## Out of scope
121
+
122
+ - No server change. No sandbox dispatch change (it already passes `--max-tasks 0`
123
+ explicitly, which `resolve_max_tasks` honors unchanged).
124
+ - The sandbox bootstrap can KEEP passing `--max-tasks 0` (explicit, still correct)
125
+ or later drop it (now redundant) — not required by this change.
126
+
127
+ ## Decisions to confirm in review
128
+
129
+ 1. Unspecified `--max-tasks` + `--wave-id` → unlimited (0). (The core ask.)
130
+ 2. The historical no-wave default stays `1` (unchanged single-task behavior).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerdev"
3
- version = "1.1.7"
3
+ version = "1.1.14"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1644,12 +1644,15 @@ def run(
1644
1644
  ),
1645
1645
  ] = None,
1646
1646
  max_tasks: Annotated[
1647
- int,
1647
+ int | None,
1648
1648
  typer.Option(
1649
1649
  "--max-tasks",
1650
- help="Maximum number of tasks to process (default: 1, use 0 for unlimited)",
1650
+ help=(
1651
+ "Maximum number of tasks to process (default: 1, or unlimited "
1652
+ "when --wave-id is set; use 0 for unlimited)"
1653
+ ),
1651
1654
  ),
1652
- ] = 1,
1655
+ ] = None,
1653
1656
  timeout: Annotated[
1654
1657
  int | None,
1655
1658
  typer.Option(
@@ -1776,7 +1779,7 @@ def run(
1776
1779
  steerdev run --project-id abc123 --workflow-id wf-abc123
1777
1780
  steerdev run --config custom.yaml --project-id abc123
1778
1781
  """
1779
- from steerdev_agent.runner import run_agent
1782
+ from steerdev_agent.runner import resolve_max_tasks, run_agent
1780
1783
 
1781
1784
  # Get config from context (loaded in app callback)
1782
1785
  config: SteerDevConfig = ctx.obj or SteerDevConfig()
@@ -1821,7 +1824,8 @@ def run(
1821
1824
  dry_run_status = "enabled" if dry_run else "disabled"
1822
1825
  waves_status = "enabled" if waves else "disabled"
1823
1826
  canals_status = "enabled" if canals else "disabled"
1824
- max_tasks_display = "unlimited" if max_tasks == 0 else str(max_tasks)
1827
+ resolved_max_tasks = resolve_max_tasks(max_tasks, wave_id)
1828
+ max_tasks_display = "unlimited" if resolved_max_tasks == 0 else str(resolved_max_tasks)
1825
1829
  workflow_status = resolved_workflow_id or "auto (per-task)"
1826
1830
  agent_name_display = agent_name or "(auto from env)"
1827
1831
  wave_scope_display = wave_id or "(all waves)"
@@ -1872,7 +1876,7 @@ def run(
1872
1876
  agent_name=agent_name,
1873
1877
  model=resolved_model,
1874
1878
  max_turns=resolved_max_turns,
1875
- max_tasks=max_tasks,
1879
+ max_tasks=resolved_max_tasks,
1876
1880
  timeout_seconds=resolved_timeout,
1877
1881
  enable_waves=waves,
1878
1882
  enable_canals=canals,
@@ -136,6 +136,22 @@ def extract_task_and_wave_context(
136
136
  return wave_response, None, suggested_workflow_id
137
137
 
138
138
 
139
+ def resolve_max_tasks(max_tasks: int | None, wave_id: str | None) -> int:
140
+ """Resolve the effective --max-tasks for a run.
141
+
142
+ - An explicit value (including 0 = unlimited) always wins.
143
+ - Unspecified (None) with a wave scope → 0 (unlimited): a wave run pulls
144
+ every task of the wave, then stops. This makes a bare
145
+ `steerdev run --wave-id X` behave like the sandbox's
146
+ `steerdev run --wave-id X --max-tasks 0`.
147
+ - Unspecified (None) with no wave scope → 1 (the historical single-task
148
+ default).
149
+ """
150
+ if max_tasks is not None:
151
+ return max_tasks
152
+ return 0 if wave_id else 1
153
+
154
+
139
155
  def resolve_merge_flow(
140
156
  waves_enabled: bool,
141
157
  canals_enabled: bool,
@@ -0,0 +1,27 @@
1
+ """Tests for resolve_max_tasks: --wave-id implies unlimited when --max-tasks is unset."""
2
+
3
+ from steerdev_agent.runner import resolve_max_tasks
4
+
5
+
6
+ def test_unspecified_with_wave_is_unlimited():
7
+ # Bare `steerdev run --wave-id X` → pull the whole wave (0 = unlimited).
8
+ assert resolve_max_tasks(None, "wave-1") == 0
9
+
10
+
11
+ def test_unspecified_without_wave_is_one():
12
+ # Historical single-task default is preserved.
13
+ assert resolve_max_tasks(None, None) == 1
14
+
15
+
16
+ def test_explicit_zero_is_honored_with_wave():
17
+ # The sandbox bootstrap passes --max-tasks 0 explicitly; still 0.
18
+ assert resolve_max_tasks(0, "wave-1") == 0
19
+
20
+
21
+ def test_explicit_cap_is_honored_with_wave():
22
+ # An explicit cap wins even when a wave is scoped.
23
+ assert resolve_max_tasks(2, "wave-1") == 2
24
+
25
+
26
+ def test_explicit_is_honored_without_wave():
27
+ assert resolve_max_tasks(5, None) == 5
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes