steerdev 1.1.6__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.
- {steerdev-1.1.6 → steerdev-1.1.14}/.github/workflows/publish.yml +3 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/PKG-INFO +1 -1
- steerdev-1.1.14/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +253 -0
- steerdev-1.1.14/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +130 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/pyproject.toml +1 -1
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/tasks.py +9 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/cli.py +23 -6
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/runner.py +28 -1
- steerdev-1.1.14/tests/test_resolve_max_tasks.py +27 -0
- steerdev-1.1.14/tests/test_wave_scoped_fetch.py +60 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/.gitignore +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/AGENTS.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/CLAUDE.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/README.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workspace/preparation.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/__init__.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_api_client.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_config.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_context_search.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_executor.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_preparation.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_prompt.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_retry.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_runner_worktrees.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_tasks.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_version.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_workspace.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.6 → steerdev-1.1.14}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerdev
|
|
3
|
-
Version: 1.1.
|
|
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.
|
steerdev-1.1.14/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md
ADDED
|
@@ -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).
|
|
@@ -108,6 +108,7 @@ class TasksClient(SteerDevClient):
|
|
|
108
108
|
self,
|
|
109
109
|
project_id: str | None = None,
|
|
110
110
|
agent_id: str | None = None,
|
|
111
|
+
wave_id: str | None = None,
|
|
111
112
|
) -> dict[str, Any] | None:
|
|
112
113
|
"""Fetch the next task with wave-aware scheduling.
|
|
113
114
|
|
|
@@ -117,6 +118,11 @@ class TasksClient(SteerDevClient):
|
|
|
117
118
|
When agent_id is provided, the server uses conflict-aware scheduling
|
|
118
119
|
to avoid assigning tasks that overlap files with in-flight tasks.
|
|
119
120
|
|
|
121
|
+
When wave_id is provided, scheduling is scoped to that wave: the server
|
|
122
|
+
returns tasks only from that wave and a 404 (→ None) once it is
|
|
123
|
+
exhausted, so the caller's loop stops instead of bleeding into another
|
|
124
|
+
wave or an unrelated task.
|
|
125
|
+
|
|
120
126
|
Returns either:
|
|
121
127
|
- A wave response dict (has "wave" and "context" keys) with full wave context
|
|
122
128
|
- A plain task dict (no wave keys) when no waves exist for the project
|
|
@@ -125,6 +131,7 @@ class TasksClient(SteerDevClient):
|
|
|
125
131
|
Args:
|
|
126
132
|
project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
|
|
127
133
|
agent_id: Agent ID for conflict-aware scheduling.
|
|
134
|
+
wave_id: Scope scheduling to a single wave (sandbox wave dispatch).
|
|
128
135
|
|
|
129
136
|
Returns:
|
|
130
137
|
Wave response, plain task, or None.
|
|
@@ -136,6 +143,8 @@ class TasksClient(SteerDevClient):
|
|
|
136
143
|
params["project_id"] = effective_project_id
|
|
137
144
|
if agent_id:
|
|
138
145
|
params["agent_id"] = agent_id
|
|
146
|
+
if wave_id:
|
|
147
|
+
params["wave_id"] = wave_id
|
|
139
148
|
|
|
140
149
|
console.print(f"Fetching next task from {self.api_base}/tasks/next")
|
|
141
150
|
response = self.get("/tasks/next", params=params)
|
|
@@ -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=
|
|
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
|
-
] =
|
|
1655
|
+
] = None,
|
|
1653
1656
|
timeout: Annotated[
|
|
1654
1657
|
int | None,
|
|
1655
1658
|
typer.Option(
|
|
@@ -1745,6 +1748,16 @@ def run(
|
|
|
1745
1748
|
help="Run a specific task by ID (bypasses queue/wave scheduling)",
|
|
1746
1749
|
),
|
|
1747
1750
|
] = None,
|
|
1751
|
+
wave_id: Annotated[
|
|
1752
|
+
str | None,
|
|
1753
|
+
typer.Option(
|
|
1754
|
+
"--wave-id",
|
|
1755
|
+
help=(
|
|
1756
|
+
"Process every task of a specific wave, then stop. Scopes wave "
|
|
1757
|
+
"scheduling to this wave (used by sandbox wave dispatch)."
|
|
1758
|
+
),
|
|
1759
|
+
),
|
|
1760
|
+
] = None,
|
|
1748
1761
|
) -> None:
|
|
1749
1762
|
"""Run the agent for a project!
|
|
1750
1763
|
|
|
@@ -1766,7 +1779,7 @@ def run(
|
|
|
1766
1779
|
steerdev run --project-id abc123 --workflow-id wf-abc123
|
|
1767
1780
|
steerdev run --config custom.yaml --project-id abc123
|
|
1768
1781
|
"""
|
|
1769
|
-
from steerdev_agent.runner import run_agent
|
|
1782
|
+
from steerdev_agent.runner import resolve_max_tasks, run_agent
|
|
1770
1783
|
|
|
1771
1784
|
# Get config from context (loaded in app callback)
|
|
1772
1785
|
config: SteerDevConfig = ctx.obj or SteerDevConfig()
|
|
@@ -1811,9 +1824,11 @@ def run(
|
|
|
1811
1824
|
dry_run_status = "enabled" if dry_run else "disabled"
|
|
1812
1825
|
waves_status = "enabled" if waves else "disabled"
|
|
1813
1826
|
canals_status = "enabled" if canals else "disabled"
|
|
1814
|
-
|
|
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)
|
|
1815
1829
|
workflow_status = resolved_workflow_id or "auto (per-task)"
|
|
1816
1830
|
agent_name_display = agent_name or "(auto from env)"
|
|
1831
|
+
wave_scope_display = wave_id or "(all waves)"
|
|
1817
1832
|
|
|
1818
1833
|
console.print(
|
|
1819
1834
|
Panel(
|
|
@@ -1826,6 +1841,7 @@ def run(
|
|
|
1826
1841
|
f"Timeout: {resolved_timeout}s\n"
|
|
1827
1842
|
f"Workflow: {workflow_status}\n"
|
|
1828
1843
|
f"Waves: {waves_status}\n"
|
|
1844
|
+
f"Wave Scope: {wave_scope_display}\n"
|
|
1829
1845
|
f"Canals: {canals_status}\n"
|
|
1830
1846
|
f"Worktrees: {worktree_status}\n"
|
|
1831
1847
|
f"Evidence: {evidence_status}\n"
|
|
@@ -1860,7 +1876,7 @@ def run(
|
|
|
1860
1876
|
agent_name=agent_name,
|
|
1861
1877
|
model=resolved_model,
|
|
1862
1878
|
max_turns=resolved_max_turns,
|
|
1863
|
-
max_tasks=
|
|
1879
|
+
max_tasks=resolved_max_tasks,
|
|
1864
1880
|
timeout_seconds=resolved_timeout,
|
|
1865
1881
|
enable_waves=waves,
|
|
1866
1882
|
enable_canals=canals,
|
|
@@ -1872,6 +1888,7 @@ def run(
|
|
|
1872
1888
|
branch_config=resolved_branch_config,
|
|
1873
1889
|
task_id=task_id,
|
|
1874
1890
|
agent_id=agent_id,
|
|
1891
|
+
wave_id=wave_id,
|
|
1875
1892
|
)
|
|
1876
1893
|
)
|
|
1877
1894
|
|
|
@@ -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,
|
|
@@ -288,6 +304,7 @@ class Runner:
|
|
|
288
304
|
shutdown_event: asyncio.Event | None = None,
|
|
289
305
|
agent_id: str | None = None,
|
|
290
306
|
task_id: str | None = None,
|
|
307
|
+
wave_id: str | None = None,
|
|
291
308
|
) -> None:
|
|
292
309
|
"""Initialize the runner.
|
|
293
310
|
|
|
@@ -310,6 +327,9 @@ class Runner:
|
|
|
310
327
|
dry_run: If True, print the command without executing it.
|
|
311
328
|
retry_config: Retry configuration for failed tasks.
|
|
312
329
|
agent_id: Database agent ID for conflict-aware scheduling.
|
|
330
|
+
task_id: Run a single specific task (bypasses queue/wave scheduling).
|
|
331
|
+
wave_id: Scope scheduling to a single wave; the loop processes every
|
|
332
|
+
task of that wave and stops when it is exhausted.
|
|
313
333
|
"""
|
|
314
334
|
self.project_id = project_id
|
|
315
335
|
self.working_directory = Path(working_directory or Path.cwd())
|
|
@@ -336,6 +356,7 @@ class Runner:
|
|
|
336
356
|
self._enable_canals = enable_canals
|
|
337
357
|
self._agent_id = agent_id
|
|
338
358
|
self._target_task_id = task_id
|
|
359
|
+
self._target_wave_id = wave_id
|
|
339
360
|
|
|
340
361
|
# State
|
|
341
362
|
self._state = RunState.STOPPED
|
|
@@ -1144,6 +1165,7 @@ class Runner:
|
|
|
1144
1165
|
client.get_next_wave(
|
|
1145
1166
|
project_id=self.project_id,
|
|
1146
1167
|
agent_id=self._agent_id,
|
|
1168
|
+
wave_id=self._target_wave_id,
|
|
1147
1169
|
)
|
|
1148
1170
|
)
|
|
1149
1171
|
)
|
|
@@ -1153,7 +1175,10 @@ class Runner:
|
|
|
1153
1175
|
exc_info=True,
|
|
1154
1176
|
)
|
|
1155
1177
|
|
|
1156
|
-
|
|
1178
|
+
# When scoped to a wave, never fall back to single-task
|
|
1179
|
+
# mode — an empty wave response means the wave is done, so
|
|
1180
|
+
# the loop should stop rather than pull an unrelated task.
|
|
1181
|
+
if not task and not self._target_wave_id:
|
|
1157
1182
|
task = client.get_next_task(
|
|
1158
1183
|
project_id=self.project_id,
|
|
1159
1184
|
agent_id=self._agent_id,
|
|
@@ -1412,6 +1437,7 @@ async def run_agent(
|
|
|
1412
1437
|
branch_config: BranchConfig | None = None,
|
|
1413
1438
|
task_id: str | None = None,
|
|
1414
1439
|
agent_id: str | None = None,
|
|
1440
|
+
wave_id: str | None = None,
|
|
1415
1441
|
) -> dict[str, Any]:
|
|
1416
1442
|
"""Run the steerdev agent.
|
|
1417
1443
|
|
|
@@ -1461,6 +1487,7 @@ async def run_agent(
|
|
|
1461
1487
|
branch_config=branch_config,
|
|
1462
1488
|
task_id=task_id,
|
|
1463
1489
|
agent_id=agent_id,
|
|
1490
|
+
wave_id=wave_id,
|
|
1464
1491
|
)
|
|
1465
1492
|
|
|
1466
1493
|
# Install signal handler that kills the subprocess immediately on Ctrl+C
|
|
@@ -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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Tests for wave-scoped task fetching (sandbox wave dispatch).
|
|
2
|
+
|
|
3
|
+
`steerdev run --wave-id <id>` scopes scheduling to a single wave: the client
|
|
4
|
+
passes `wave_id` to GET /tasks/next, and the server returns tasks only from that
|
|
5
|
+
wave (404 → None once exhausted) so the run loop stops instead of bleeding into
|
|
6
|
+
another wave or an unrelated task.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from steerdev_agent.api.tasks import TasksClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_response(
|
|
18
|
+
status_code: int,
|
|
19
|
+
json_body: dict[str, Any] | None = None,
|
|
20
|
+
text: str = "",
|
|
21
|
+
) -> MagicMock:
|
|
22
|
+
resp = MagicMock(spec=httpx.Response)
|
|
23
|
+
resp.status_code = status_code
|
|
24
|
+
resp.text = text or (str(json_body) if json_body else "")
|
|
25
|
+
if json_body is not None:
|
|
26
|
+
resp.json.return_value = json_body
|
|
27
|
+
return resp
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestGetNextWaveScoping:
|
|
31
|
+
def test_wave_id_is_forwarded_as_param(self):
|
|
32
|
+
with TasksClient(api_key="test-key") as client:
|
|
33
|
+
mock_resp = _make_response(200, {"wave": {"id": "wave-1"}, "context": {}})
|
|
34
|
+
with patch.object(client, "get", return_value=mock_resp) as mock_get:
|
|
35
|
+
client.get_next_wave(
|
|
36
|
+
project_id="proj-1",
|
|
37
|
+
agent_id="agent-1",
|
|
38
|
+
wave_id="wave-1",
|
|
39
|
+
)
|
|
40
|
+
params = mock_get.call_args.kwargs["params"]
|
|
41
|
+
assert params["wave_id"] == "wave-1"
|
|
42
|
+
assert params["waves"] == "true"
|
|
43
|
+
assert params["project_id"] == "proj-1"
|
|
44
|
+
assert params["agent_id"] == "agent-1"
|
|
45
|
+
|
|
46
|
+
def test_wave_id_omitted_when_not_scoped(self):
|
|
47
|
+
with TasksClient(api_key="test-key") as client:
|
|
48
|
+
mock_resp = _make_response(200, {"id": "task-1"})
|
|
49
|
+
with patch.object(client, "get", return_value=mock_resp) as mock_get:
|
|
50
|
+
client.get_next_wave(project_id="proj-1")
|
|
51
|
+
params = mock_get.call_args.kwargs["params"]
|
|
52
|
+
assert "wave_id" not in params
|
|
53
|
+
|
|
54
|
+
def test_exhausted_wave_returns_none_on_404(self):
|
|
55
|
+
# A wave-scoped request 404s once the wave is done — the loop stops.
|
|
56
|
+
with TasksClient(api_key="test-key") as client:
|
|
57
|
+
mock_resp = _make_response(404, text="No tasks available")
|
|
58
|
+
with patch.object(client, "get", return_value=mock_resp):
|
|
59
|
+
result = client.get_next_wave(project_id="proj-1", wave_id="wave-1")
|
|
60
|
+
assert result is None
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.1.6 → steerdev-1.1.14}/src/steerdev_agent/setup/templates/ci/canal-integration.yml
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|