steerdev 1.1.29__tar.gz → 1.1.43__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.29 → steerdev-1.1.43}/PKG-INFO +1 -1
- {steerdev-1.1.29 → steerdev-1.1.43}/pyproject.toml +1 -1
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/cli.py +14 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/prompt/builder.py +4 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/runner.py +18 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +38 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workspace/preparation.py +218 -30
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/worktree.py +11 -1
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_preparation.py +553 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_runner_merge_modes.py +25 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_runner_worktrees.py +25 -1
- {steerdev-1.1.29 → steerdev-1.1.43}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/.gitignore +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/AGENTS.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/CLAUDE.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/README.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/git/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/git/commit.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_api_client.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_config.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_context_search.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_executor.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_git_commit.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_prompt.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_retry.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_runner_boundary_commit.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_tasks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_version.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_workspace.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.43}/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.43
|
|
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
|
|
@@ -1806,6 +1806,16 @@ def run(
|
|
|
1806
1806
|
),
|
|
1807
1807
|
),
|
|
1808
1808
|
] = None,
|
|
1809
|
+
base_branch: Annotated[
|
|
1810
|
+
str | None,
|
|
1811
|
+
typer.Option(
|
|
1812
|
+
"--base-branch",
|
|
1813
|
+
help=(
|
|
1814
|
+
"Branch to cut a new wave/task branch from (overrides the "
|
|
1815
|
+
"auto-chosen predecessor/default). Falls back to STEERDEV_BASE_BRANCH."
|
|
1816
|
+
),
|
|
1817
|
+
),
|
|
1818
|
+
] = None,
|
|
1809
1819
|
) -> None:
|
|
1810
1820
|
"""Run the agent for a project!
|
|
1811
1821
|
|
|
@@ -1841,6 +1851,9 @@ def run(
|
|
|
1841
1851
|
console.print("[red]Error: --project-id required (or set via env/config)[/red]")
|
|
1842
1852
|
raise typer.Exit(1)
|
|
1843
1853
|
|
|
1854
|
+
# Resolve base branch override: CLI > env > None
|
|
1855
|
+
resolved_base_branch = base_branch or os.environ.get("STEERDEV_BASE_BRANCH") or None
|
|
1856
|
+
|
|
1844
1857
|
# Resolve other options: CLI > config > hardcoded default
|
|
1845
1858
|
resolved_model = model if model is not None else config.agent.model
|
|
1846
1859
|
resolved_max_turns = max_turns if max_turns is not None else config.agent.max_turns
|
|
@@ -1937,6 +1950,7 @@ def run(
|
|
|
1937
1950
|
task_id=task_id,
|
|
1938
1951
|
agent_id=agent_id,
|
|
1939
1952
|
wave_id=wave_id,
|
|
1953
|
+
base_branch_override=resolved_base_branch,
|
|
1940
1954
|
)
|
|
1941
1955
|
)
|
|
1942
1956
|
|
|
@@ -28,6 +28,10 @@ class WaveContext(BaseModel):
|
|
|
28
28
|
wave_description: str = Field(default="", description="Description of this wave")
|
|
29
29
|
wave_tasks_summary: str = Field(default="", description="Summary of tasks in this wave")
|
|
30
30
|
completed_waves_summary: str = Field(default="", description="Summary of completed waves")
|
|
31
|
+
predecessor_branch: str | None = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description="Branch of the previous completed wave to stack this wave on",
|
|
34
|
+
)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class TaskContext(BaseModel):
|
|
@@ -110,6 +110,15 @@ def build_wave_context(wave_response: dict[str, Any]) -> WaveContext | None:
|
|
|
110
110
|
]
|
|
111
111
|
completed_summary = "\n".join(completed_lines) if completed_lines else ""
|
|
112
112
|
|
|
113
|
+
# Stacked-wave base: the server sends the previous completed wave's branch
|
|
114
|
+
# NAME (never a number — the wave name IS the branch). A blank/absent name
|
|
115
|
+
# means no valid predecessor, so we leave the base unset and cut from default.
|
|
116
|
+
predecessor_branch: str | None = None
|
|
117
|
+
predecessor = wave_info.get("predecessor_wave")
|
|
118
|
+
if predecessor:
|
|
119
|
+
name = (predecessor.get("name") or "").strip()
|
|
120
|
+
predecessor_branch = name or None
|
|
121
|
+
|
|
113
122
|
return WaveContext(
|
|
114
123
|
wave_number=wave_info.get("wave_number", 1),
|
|
115
124
|
total_waves=wave_info.get("total_waves", 1),
|
|
@@ -117,6 +126,7 @@ def build_wave_context(wave_response: dict[str, Any]) -> WaveContext | None:
|
|
|
117
126
|
wave_description=wave_info.get("description", ""),
|
|
118
127
|
wave_tasks_summary=tasks_summary,
|
|
119
128
|
completed_waves_summary=completed_summary,
|
|
129
|
+
predecessor_branch=predecessor_branch,
|
|
120
130
|
)
|
|
121
131
|
|
|
122
132
|
|
|
@@ -328,6 +338,7 @@ class Runner:
|
|
|
328
338
|
agent_id: str | None = None,
|
|
329
339
|
task_id: str | None = None,
|
|
330
340
|
wave_id: str | None = None,
|
|
341
|
+
base_branch_override: str | None = None,
|
|
331
342
|
) -> None:
|
|
332
343
|
"""Initialize the runner.
|
|
333
344
|
|
|
@@ -353,6 +364,8 @@ class Runner:
|
|
|
353
364
|
task_id: Run a single specific task (bypasses queue/wave scheduling).
|
|
354
365
|
wave_id: Scope scheduling to a single wave; the loop processes every
|
|
355
366
|
task of that wave and stops when it is exhausted.
|
|
367
|
+
base_branch_override: Override the base branch used when preparing
|
|
368
|
+
each task's workspace.
|
|
356
369
|
"""
|
|
357
370
|
self.project_id = project_id
|
|
358
371
|
self.working_directory = Path(working_directory or Path.cwd())
|
|
@@ -380,6 +393,7 @@ class Runner:
|
|
|
380
393
|
self._agent_id = agent_id
|
|
381
394
|
self._target_task_id = task_id
|
|
382
395
|
self._target_wave_id = wave_id
|
|
396
|
+
self._base_branch_override = base_branch_override
|
|
383
397
|
|
|
384
398
|
# State
|
|
385
399
|
self._state = RunState.STOPPED
|
|
@@ -826,6 +840,7 @@ class Runner:
|
|
|
826
840
|
working_directory=self.working_directory,
|
|
827
841
|
worktree_config=self._worktree_config,
|
|
828
842
|
branch_config=self._branch_config,
|
|
843
|
+
base_branch_override=self._base_branch_override,
|
|
829
844
|
)
|
|
830
845
|
prep_result = await preparation.prepare(task, wave_context)
|
|
831
846
|
if not prep_result.success:
|
|
@@ -1043,6 +1058,7 @@ class Runner:
|
|
|
1043
1058
|
working_directory=self.working_directory,
|
|
1044
1059
|
worktree_config=self._worktree_config,
|
|
1045
1060
|
branch_config=self._branch_config,
|
|
1061
|
+
base_branch_override=self._base_branch_override,
|
|
1046
1062
|
)
|
|
1047
1063
|
prep_result = await preparation.prepare(task, wave_context)
|
|
1048
1064
|
if not prep_result.success:
|
|
@@ -1500,6 +1516,7 @@ async def run_agent(
|
|
|
1500
1516
|
task_id: str | None = None,
|
|
1501
1517
|
agent_id: str | None = None,
|
|
1502
1518
|
wave_id: str | None = None,
|
|
1519
|
+
base_branch_override: str | None = None,
|
|
1503
1520
|
) -> dict[str, Any]:
|
|
1504
1521
|
"""Run the steerdev agent.
|
|
1505
1522
|
|
|
@@ -1550,6 +1567,7 @@ async def run_agent(
|
|
|
1550
1567
|
task_id=task_id,
|
|
1551
1568
|
agent_id=agent_id,
|
|
1552
1569
|
wave_id=wave_id,
|
|
1570
|
+
base_branch_override=base_branch_override,
|
|
1553
1571
|
)
|
|
1554
1572
|
|
|
1555
1573
|
# Install signal handler that kills the subprocess immediately on Ctrl+C
|
|
@@ -63,12 +63,34 @@ git merge-tree --write-tree origin/<default-branch> HEAD
|
|
|
63
63
|
|
|
64
64
|
**Fallback** (if `git merge-tree` is unavailable): Skip the dry-run and rebase directly. If rebase fails, abort and re-queue as above.
|
|
65
65
|
|
|
66
|
+
### Before pushing: reconcile concurrent work on the wave branch
|
|
67
|
+
|
|
68
|
+
Another task of this wave may have pushed to the wave branch while you worked.
|
|
69
|
+
Before any push to the wave branch:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git fetch origin "$(git branch --show-current)"
|
|
73
|
+
git rebase "origin/$(git branch --show-current)"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- If the rebase is clean, continue.
|
|
77
|
+
- If it conflicts, resolve the conflicts in the files in your working tree
|
|
78
|
+
(you have full context for the change you just made), then
|
|
79
|
+
`git rebase --continue`.
|
|
80
|
+
- If you genuinely cannot resolve them, abort (`git rebase --abort`) and report
|
|
81
|
+
a blocker with `steerdev activity report`.
|
|
82
|
+
|
|
66
83
|
4. Create one pull request for the wave:
|
|
67
84
|
|
|
68
85
|
```bash
|
|
69
86
|
steerdev git pr --title "Wave <N>: <description>" --body "..."
|
|
70
87
|
```
|
|
71
88
|
|
|
89
|
+
**Do not pass `--base`.** `steerdev git pr` sets the PR base automatically from
|
|
90
|
+
the task's recorded base branch. For a stacked wave (a wave that builds on a
|
|
91
|
+
previous wave of the same PRD) the base is the predecessor wave's branch, so the
|
|
92
|
+
PR shows only this wave's diff. Overriding `--base` would break the stack.
|
|
93
|
+
|
|
72
94
|
5. Update all related tasks with the shared PR URL
|
|
73
95
|
6. Log progress and completion in the root `docs/` progress files
|
|
74
96
|
|
|
@@ -86,11 +108,27 @@ message so GitHub integrations can link commits to issues:
|
|
|
86
108
|
It also pushes the wave branch after each commit. Your job is to implement and,
|
|
87
109
|
once the wave is done, rebase and open the PR (steps 3–5).
|
|
88
110
|
|
|
111
|
+
## Stacked waves (same PRD)
|
|
112
|
+
|
|
113
|
+
Waves of one PRD are **stacked**: wave-2 is branched off wave-1, wave-3 off
|
|
114
|
+
wave-2, and so on. The CLI handles this for you — it cuts the wave branch off the
|
|
115
|
+
previous wave's branch and records that branch as the PR base. As a result:
|
|
116
|
+
|
|
117
|
+
- Each wave still gets its own PR, but its diff shows only that wave's changes.
|
|
118
|
+
- The PRs form a chain: `wave-1 → default`, `wave-2 → wave-1`, `wave-3 → wave-2`.
|
|
119
|
+
- **Do not delete a wave's branch until the next wave's PR has merged.** When
|
|
120
|
+
wave-1's PR merges into the default branch, GitHub automatically retargets
|
|
121
|
+
wave-2's PR — but only if wave-1's branch still exists.
|
|
122
|
+
- The first wave of a PRD (and any wave with no completed same-PRD predecessor)
|
|
123
|
+
is cut from the default branch as usual.
|
|
124
|
+
|
|
89
125
|
## Conventions
|
|
90
126
|
|
|
91
127
|
- Use one branch per wave: `wave/<wave-number>`
|
|
92
128
|
- Do not commit task work — the runtime commits and pushes each task automatically
|
|
93
129
|
- Do not create per-task PRs inside the wave
|
|
130
|
+
- Do not pass `--base` to `steerdev git pr` — the base is set automatically
|
|
131
|
+
- Do not delete wave branches until the dependent wave's PR has merged
|
|
94
132
|
- Include the shared PR URL in every related task result
|
|
95
133
|
- Replace `<default-branch>` in commands above with the actual branch name from the Git Branch Configuration section
|
|
96
134
|
|
|
@@ -114,10 +114,12 @@ class WorkspacePreparation:
|
|
|
114
114
|
working_directory: Path,
|
|
115
115
|
worktree_config: WorktreeConfig,
|
|
116
116
|
branch_config: BranchConfig,
|
|
117
|
+
base_branch_override: str | None = None,
|
|
117
118
|
) -> None:
|
|
118
119
|
self.working_directory = working_directory
|
|
119
120
|
self._worktree_config = worktree_config
|
|
120
121
|
self._branch_config = branch_config
|
|
122
|
+
self._base_branch_override = base_branch_override
|
|
121
123
|
|
|
122
124
|
async def prepare(
|
|
123
125
|
self,
|
|
@@ -294,6 +296,48 @@ class WorkspacePreparation:
|
|
|
294
296
|
repo_directories=prepared_repos,
|
|
295
297
|
)
|
|
296
298
|
|
|
299
|
+
async def _claim_or_adopt(self, cwd: Path, target_branch: str) -> tuple[bool, bool]:
|
|
300
|
+
"""Push the freshly-cut branch to claim it (plain push, no force).
|
|
301
|
+
|
|
302
|
+
Returns (success, adopted). A push rejected because the branch already
|
|
303
|
+
exists on origin (another VM won the race) is adopted: we fetch and
|
|
304
|
+
reset to the remote branch and continue its commits. Any OTHER push
|
|
305
|
+
failure (network, auth, ...) is a hard error, not a lost race — we do
|
|
306
|
+
not adopt, so the caller surfaces a real failure rather than silently
|
|
307
|
+
building on an unexpected branch. adopted=True means continuation
|
|
308
|
+
(don't persist base).
|
|
309
|
+
"""
|
|
310
|
+
rc, out, err = await _run_git("push", "origin", target_branch, cwd=cwd, check=False)
|
|
311
|
+
if rc == 0:
|
|
312
|
+
logger.info(f"Claimed branch {target_branch} on origin")
|
|
313
|
+
return True, False
|
|
314
|
+
|
|
315
|
+
# Only a ref-already-exists / non-fast-forward rejection means we lost
|
|
316
|
+
# the race. Match git's rejection wording; treat anything else as fatal.
|
|
317
|
+
message = f"{out}\n{err}".lower()
|
|
318
|
+
is_lost_race = (
|
|
319
|
+
"[rejected]" in message
|
|
320
|
+
or "fetch first" in message
|
|
321
|
+
or "already exists" in message
|
|
322
|
+
or "non-fast-forward" in message
|
|
323
|
+
)
|
|
324
|
+
if not is_lost_race:
|
|
325
|
+
logger.warning(f"Claim push for {target_branch} failed (not a rejection): {err or out}")
|
|
326
|
+
return False, False
|
|
327
|
+
|
|
328
|
+
logger.info(
|
|
329
|
+
f"Claim of {target_branch} rejected ({err or out}); adopting existing "
|
|
330
|
+
f"origin/{target_branch}"
|
|
331
|
+
)
|
|
332
|
+
rc_fetch, _, _ = await _run_git("fetch", "origin", target_branch, cwd=cwd, check=False)
|
|
333
|
+
if rc_fetch != 0:
|
|
334
|
+
logger.warning(f"Failed to fetch origin/{target_branch} for adoption")
|
|
335
|
+
return False, True
|
|
336
|
+
rc, _, _ = await _run_git(
|
|
337
|
+
"checkout", "-B", target_branch, f"origin/{target_branch}", cwd=cwd, check=False
|
|
338
|
+
)
|
|
339
|
+
return rc == 0, True
|
|
340
|
+
|
|
297
341
|
async def _prepare_single_repo(
|
|
298
342
|
self,
|
|
299
343
|
cwd: Path,
|
|
@@ -347,40 +391,180 @@ class WorkspacePreparation:
|
|
|
347
391
|
# 4. Fetch from origin
|
|
348
392
|
await _run_git("fetch", "origin", cwd=cwd, check=False)
|
|
349
393
|
|
|
350
|
-
# 5.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
394
|
+
# 5. Create or checkout the target (wave) branch.
|
|
395
|
+
#
|
|
396
|
+
# Branch resolution priority — a wave has exactly ONE branch, so an
|
|
397
|
+
# already-existing wave branch must always be CONTINUED, never re-cut:
|
|
398
|
+
# a) local branch exists → checkout (same VM, mid-run)
|
|
399
|
+
# b) branch exists on origin → checkout tracking it (fresh clone
|
|
400
|
+
# / resume — continue prior commits)
|
|
401
|
+
# c) new wave with predecessor → stacked cut off origin/<predecessor>
|
|
402
|
+
# d) otherwise → default cut
|
|
403
|
+
# Cases (a)/(b) are continuations: they do NOT re-persist base_branch
|
|
404
|
+
# (task 1 already recorded the wave's base — don't clobber it). The base
|
|
405
|
+
# is resolved ONLY on the cut paths (c)/(d), so a continuation never
|
|
406
|
+
# surfaces a spurious "predecessor missing / chain broken" warning.
|
|
407
|
+
|
|
408
|
+
# Does the wave branch already exist? (origin is authoritative for a
|
|
409
|
+
# fresh clone). If so we CONTINUE it — no base resolution / no cut.
|
|
410
|
+
rc, _, _ = await _run_git("rev-parse", "--verify", target_branch, cwd=cwd, check=False)
|
|
411
|
+
branch_exists_local = rc == 0
|
|
361
412
|
|
|
362
|
-
# Try fast-forward merge
|
|
363
413
|
rc, _, _ = await _run_git(
|
|
364
|
-
"
|
|
414
|
+
"rev-parse", "--verify", f"origin/{target_branch}", cwd=cwd, check=False
|
|
365
415
|
)
|
|
366
|
-
|
|
367
|
-
# Fallback: hard reset to origin
|
|
368
|
-
logger.warning(f"Fast-forward merge failed for {default_branch}, resetting to origin")
|
|
369
|
-
await _run_git("reset", "--hard", f"origin/{default_branch}", cwd=cwd, check=False)
|
|
370
|
-
warnings.append(
|
|
371
|
-
f"Fast-forward failed, reset {default_branch} to origin/{default_branch}"
|
|
372
|
-
)
|
|
416
|
+
branch_exists_origin = rc == 0
|
|
373
417
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
branch_exists = rc == 0
|
|
418
|
+
is_continuation = False
|
|
419
|
+
effective_base = default_branch
|
|
377
420
|
|
|
378
|
-
if
|
|
379
|
-
|
|
421
|
+
if branch_exists_local:
|
|
422
|
+
# Branch already present locally (same VM, continuing the wave).
|
|
423
|
+
is_continuation = True
|
|
424
|
+
rc, _, _ = await _run_git("checkout", target_branch, cwd=cwd, check=False)
|
|
425
|
+
if rc != 0:
|
|
426
|
+
return PreparationResult(
|
|
427
|
+
success=False,
|
|
428
|
+
branch_name=target_branch,
|
|
429
|
+
working_directory=cwd,
|
|
430
|
+
default_branch=default_branch,
|
|
431
|
+
is_worktree=False,
|
|
432
|
+
error=f"Failed to checkout existing branch {target_branch}",
|
|
433
|
+
)
|
|
380
434
|
logger.info(f"Switched to existing branch: {target_branch}")
|
|
435
|
+
elif branch_exists_origin:
|
|
436
|
+
# Wave branch exists on origin but not locally — a fresh clone / resume
|
|
437
|
+
# continuing a wave another VM started. Track it and continue its
|
|
438
|
+
# commits; NEVER re-cut (that would drop the earlier tasks' work).
|
|
439
|
+
is_continuation = True
|
|
440
|
+
rc, _, _ = await _run_git(
|
|
441
|
+
"checkout", "-b", target_branch, f"origin/{target_branch}", cwd=cwd, check=False
|
|
442
|
+
)
|
|
443
|
+
if rc != 0:
|
|
444
|
+
return PreparationResult(
|
|
445
|
+
success=False,
|
|
446
|
+
branch_name=target_branch,
|
|
447
|
+
working_directory=cwd,
|
|
448
|
+
default_branch=default_branch,
|
|
449
|
+
is_worktree=False,
|
|
450
|
+
error=f"Failed to track existing branch origin/{target_branch}",
|
|
451
|
+
)
|
|
452
|
+
logger.info(f"Continuing wave branch from origin/{target_branch}")
|
|
381
453
|
else:
|
|
382
|
-
|
|
383
|
-
|
|
454
|
+
# New branch — NOW resolve the base a NEW wave branch is cut from.
|
|
455
|
+
# Existence is judged on ORIGIN (a sandbox is a fresh clone).
|
|
456
|
+
# Priority: explicit user override > predecessor wave > default
|
|
457
|
+
# branch. A chosen base that is missing from origin falls back to
|
|
458
|
+
# default with a loud warning. Warnings are emitted ONLY here, on
|
|
459
|
+
# the cut path — never on a continuation.
|
|
460
|
+
async def _origin_has(branch: str) -> bool:
|
|
461
|
+
rc, _, _ = await _run_git(
|
|
462
|
+
"rev-parse", "--verify", f"origin/{branch}", cwd=cwd, check=False
|
|
463
|
+
)
|
|
464
|
+
return rc == 0
|
|
465
|
+
|
|
466
|
+
override = self._base_branch_override
|
|
467
|
+
predecessor_branch = wave_context.predecessor_branch if wave_context else None
|
|
468
|
+
|
|
469
|
+
if override:
|
|
470
|
+
if await _origin_has(override):
|
|
471
|
+
effective_base = override
|
|
472
|
+
else:
|
|
473
|
+
logger.warning(
|
|
474
|
+
f"Chosen base branch origin/{override} missing — "
|
|
475
|
+
f"cutting {target_branch} from {default_branch} instead"
|
|
476
|
+
)
|
|
477
|
+
warnings.append(
|
|
478
|
+
f"Chosen base branch '{override}' not found on origin; "
|
|
479
|
+
f"cut from '{default_branch}' instead."
|
|
480
|
+
)
|
|
481
|
+
elif predecessor_branch:
|
|
482
|
+
if await _origin_has(predecessor_branch):
|
|
483
|
+
effective_base = predecessor_branch
|
|
484
|
+
else:
|
|
485
|
+
logger.warning(
|
|
486
|
+
f"Predecessor branch origin/{predecessor_branch} missing — "
|
|
487
|
+
f"stacked-wave chain broken; cutting {target_branch} from "
|
|
488
|
+
f"{default_branch} instead"
|
|
489
|
+
)
|
|
490
|
+
warnings.append(
|
|
491
|
+
f"Predecessor branch '{predecessor_branch}' not found on origin; "
|
|
492
|
+
f"stacked-wave chain broken — cut from '{default_branch}'. This wave "
|
|
493
|
+
f"may not contain the previous wave's work."
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if effective_base != default_branch:
|
|
497
|
+
# Stacked cut: branch straight off the predecessor's remote branch.
|
|
498
|
+
# No default-branch sync — the predecessor already carries the work.
|
|
499
|
+
rc, _, _ = await _run_git(
|
|
500
|
+
"checkout",
|
|
501
|
+
"-b",
|
|
502
|
+
target_branch,
|
|
503
|
+
f"origin/{effective_base}",
|
|
504
|
+
cwd=cwd,
|
|
505
|
+
check=False,
|
|
506
|
+
)
|
|
507
|
+
if rc != 0:
|
|
508
|
+
return PreparationResult(
|
|
509
|
+
success=False,
|
|
510
|
+
branch_name=target_branch,
|
|
511
|
+
working_directory=cwd,
|
|
512
|
+
default_branch=default_branch,
|
|
513
|
+
is_worktree=False,
|
|
514
|
+
error=f"Failed to cut {target_branch} from origin/{effective_base}",
|
|
515
|
+
)
|
|
516
|
+
logger.info(f"Created {target_branch} stacked on origin/{effective_base}")
|
|
517
|
+
ok, adopted = await self._claim_or_adopt(cwd, target_branch)
|
|
518
|
+
if not ok:
|
|
519
|
+
return PreparationResult(
|
|
520
|
+
success=False,
|
|
521
|
+
branch_name=target_branch,
|
|
522
|
+
working_directory=cwd,
|
|
523
|
+
default_branch=default_branch,
|
|
524
|
+
is_worktree=False,
|
|
525
|
+
error=f"Failed to claim/adopt {target_branch}",
|
|
526
|
+
)
|
|
527
|
+
is_continuation = adopted
|
|
528
|
+
else:
|
|
529
|
+
# Default cut: sync to the default branch, then branch off it.
|
|
530
|
+
rc, _, _ = await _run_git("checkout", default_branch, cwd=cwd, check=False)
|
|
531
|
+
if rc != 0:
|
|
532
|
+
return PreparationResult(
|
|
533
|
+
success=False,
|
|
534
|
+
branch_name=target_branch,
|
|
535
|
+
working_directory=cwd,
|
|
536
|
+
default_branch=default_branch,
|
|
537
|
+
is_worktree=False,
|
|
538
|
+
error=f"Failed to checkout {default_branch}. Does the branch exist?",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
rc, _, _ = await _run_git(
|
|
542
|
+
"merge", "--ff-only", f"origin/{default_branch}", cwd=cwd, check=False
|
|
543
|
+
)
|
|
544
|
+
if rc != 0:
|
|
545
|
+
logger.warning(
|
|
546
|
+
f"Fast-forward merge failed for {default_branch}, resetting to origin"
|
|
547
|
+
)
|
|
548
|
+
await _run_git(
|
|
549
|
+
"reset", "--hard", f"origin/{default_branch}", cwd=cwd, check=False
|
|
550
|
+
)
|
|
551
|
+
warnings.append(
|
|
552
|
+
f"Fast-forward failed, reset {default_branch} to origin/{default_branch}"
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
await _run_git("checkout", "-b", target_branch, cwd=cwd)
|
|
556
|
+
logger.info(f"Created new branch: {target_branch}")
|
|
557
|
+
ok, adopted = await self._claim_or_adopt(cwd, target_branch)
|
|
558
|
+
if not ok:
|
|
559
|
+
return PreparationResult(
|
|
560
|
+
success=False,
|
|
561
|
+
branch_name=target_branch,
|
|
562
|
+
working_directory=cwd,
|
|
563
|
+
default_branch=default_branch,
|
|
564
|
+
is_worktree=False,
|
|
565
|
+
error=f"Failed to claim/adopt {target_branch}",
|
|
566
|
+
)
|
|
567
|
+
is_continuation = adopted
|
|
384
568
|
|
|
385
569
|
# 7. Validate current branch
|
|
386
570
|
_, actual_branch, _ = await _run_git("branch", "--show-current", cwd=cwd, check=False)
|
|
@@ -394,9 +578,13 @@ class WorkspacePreparation:
|
|
|
394
578
|
error=f"Branch mismatch: expected {target_branch}, got {actual_branch}",
|
|
395
579
|
)
|
|
396
580
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
581
|
+
# Record provenance only when this task actually CUT the branch. A
|
|
582
|
+
# continuation (the branch already existed locally or on origin) must not
|
|
583
|
+
# overwrite the base the wave's first task recorded.
|
|
584
|
+
if not is_continuation:
|
|
585
|
+
await persist_branch_provenance(
|
|
586
|
+
task_id=task_id, branch=target_branch, base_branch=effective_base
|
|
587
|
+
)
|
|
400
588
|
|
|
401
589
|
return PreparationResult(
|
|
402
590
|
success=True,
|
|
@@ -206,6 +206,16 @@ class WorktrunkClient:
|
|
|
206
206
|
return None
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
def wave_branch_name(name: str | None, wave_number: int) -> str:
|
|
210
|
+
"""The single source of truth for a wave's git branch name.
|
|
211
|
+
|
|
212
|
+
A wave branch is its `name` (e.g. "wave-a1b2c3"), falling back to
|
|
213
|
+
"wave-{wave_number}". Used by `compute_branch_name` for the current wave and
|
|
214
|
+
by the runner to derive a predecessor wave's branch for stacked-wave cuts.
|
|
215
|
+
"""
|
|
216
|
+
return name or f"wave-{wave_number}"
|
|
217
|
+
|
|
218
|
+
|
|
209
219
|
def compute_branch_name(
|
|
210
220
|
task: dict[str, Any],
|
|
211
221
|
wave_context: WaveContext | None = None,
|
|
@@ -225,7 +235,7 @@ def compute_branch_name(
|
|
|
225
235
|
Branch name string.
|
|
226
236
|
"""
|
|
227
237
|
if wave_context:
|
|
228
|
-
return wave_context.wave_name
|
|
238
|
+
return wave_branch_name(wave_context.wave_name, wave_context.wave_number)
|
|
229
239
|
|
|
230
240
|
# Try external ticket ID first (e.g., Linear "PROJ-123")
|
|
231
241
|
external_id = task.get("linear_identifier") or task.get("external_id")
|