steerdev 1.1.29__tar.gz → 1.1.31__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.31}/PKG-INFO +1 -1
- {steerdev-1.1.29 → steerdev-1.1.31}/pyproject.toml +1 -1
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/prompt/builder.py +4 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/runner.py +10 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +21 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workspace/preparation.py +78 -25
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/worktree.py +11 -1
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_preparation.py +135 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_runner_merge_modes.py +25 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_runner_worktrees.py +17 -1
- {steerdev-1.1.29 → steerdev-1.1.31}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/.gitignore +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/AGENTS.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/CLAUDE.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/README.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/cli.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/git/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/git/commit.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/__init__.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_api_client.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_config.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_context_search.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_executor.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_git_commit.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_prompt.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_retry.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_runner_boundary_commit.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_tasks.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_version.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_workspace.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.29 → steerdev-1.1.31}/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.31
|
|
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
|
|
@@ -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
|
|
|
@@ -69,6 +69,11 @@ git merge-tree --write-tree origin/<default-branch> HEAD
|
|
|
69
69
|
steerdev git pr --title "Wave <N>: <description>" --body "..."
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
**Do not pass `--base`.** `steerdev git pr` sets the PR base automatically from
|
|
73
|
+
the task's recorded base branch. For a stacked wave (a wave that builds on a
|
|
74
|
+
previous wave of the same PRD) the base is the predecessor wave's branch, so the
|
|
75
|
+
PR shows only this wave's diff. Overriding `--base` would break the stack.
|
|
76
|
+
|
|
72
77
|
5. Update all related tasks with the shared PR URL
|
|
73
78
|
6. Log progress and completion in the root `docs/` progress files
|
|
74
79
|
|
|
@@ -86,11 +91,27 @@ message so GitHub integrations can link commits to issues:
|
|
|
86
91
|
It also pushes the wave branch after each commit. Your job is to implement and,
|
|
87
92
|
once the wave is done, rebase and open the PR (steps 3–5).
|
|
88
93
|
|
|
94
|
+
## Stacked waves (same PRD)
|
|
95
|
+
|
|
96
|
+
Waves of one PRD are **stacked**: wave-2 is branched off wave-1, wave-3 off
|
|
97
|
+
wave-2, and so on. The CLI handles this for you — it cuts the wave branch off the
|
|
98
|
+
previous wave's branch and records that branch as the PR base. As a result:
|
|
99
|
+
|
|
100
|
+
- Each wave still gets its own PR, but its diff shows only that wave's changes.
|
|
101
|
+
- The PRs form a chain: `wave-1 → default`, `wave-2 → wave-1`, `wave-3 → wave-2`.
|
|
102
|
+
- **Do not delete a wave's branch until the next wave's PR has merged.** When
|
|
103
|
+
wave-1's PR merges into the default branch, GitHub automatically retargets
|
|
104
|
+
wave-2's PR — but only if wave-1's branch still exists.
|
|
105
|
+
- The first wave of a PRD (and any wave with no completed same-PRD predecessor)
|
|
106
|
+
is cut from the default branch as usual.
|
|
107
|
+
|
|
89
108
|
## Conventions
|
|
90
109
|
|
|
91
110
|
- Use one branch per wave: `wave/<wave-number>`
|
|
92
111
|
- Do not commit task work — the runtime commits and pushes each task automatically
|
|
93
112
|
- Do not create per-task PRs inside the wave
|
|
113
|
+
- Do not pass `--base` to `steerdev git pr` — the base is set automatically
|
|
114
|
+
- Do not delete wave branches until the dependent wave's PR has merged
|
|
94
115
|
- Include the shared PR URL in every related task result
|
|
95
116
|
- Replace `<default-branch>` in commands above with the actual branch name from the Git Branch Configuration section
|
|
96
117
|
|
|
@@ -347,38 +347,91 @@ class WorkspacePreparation:
|
|
|
347
347
|
# 4. Fetch from origin
|
|
348
348
|
await _run_git("fetch", "origin", cwd=cwd, check=False)
|
|
349
349
|
|
|
350
|
-
# 5.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
# Try fast-forward merge
|
|
363
|
-
rc, _, _ = await _run_git(
|
|
364
|
-
"merge", "--ff-only", f"origin/{default_branch}", cwd=cwd, check=False
|
|
365
|
-
)
|
|
366
|
-
if rc != 0:
|
|
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}"
|
|
350
|
+
# 5. Resolve the base this wave stacks on. For a new wave with a
|
|
351
|
+
# predecessor (stacked-wave mode), branch directly off the predecessor's
|
|
352
|
+
# remote branch so the wave physically contains the previous wave's work.
|
|
353
|
+
# If that branch is missing from origin (never pushed / deleted), the
|
|
354
|
+
# chain is broken — fall back to the default branch and surface a loud
|
|
355
|
+
# warning rather than silently building on the wrong base.
|
|
356
|
+
predecessor_branch = wave_context.predecessor_branch if wave_context else None
|
|
357
|
+
effective_base = default_branch
|
|
358
|
+
if predecessor_branch:
|
|
359
|
+
rc, _, _ = await _run_git(
|
|
360
|
+
"rev-parse", "--verify", f"origin/{predecessor_branch}", cwd=cwd, check=False
|
|
372
361
|
)
|
|
362
|
+
if rc == 0:
|
|
363
|
+
effective_base = predecessor_branch
|
|
364
|
+
else:
|
|
365
|
+
logger.warning(
|
|
366
|
+
f"Predecessor branch origin/{predecessor_branch} missing — "
|
|
367
|
+
f"stacked-wave chain broken; cutting {target_branch} from "
|
|
368
|
+
f"{default_branch} instead"
|
|
369
|
+
)
|
|
370
|
+
warnings.append(
|
|
371
|
+
f"Predecessor branch '{predecessor_branch}' not found on origin; "
|
|
372
|
+
f"stacked-wave chain broken — cut from '{default_branch}'. This wave "
|
|
373
|
+
f"may not contain the previous wave's work."
|
|
374
|
+
)
|
|
373
375
|
|
|
374
|
-
# 6. Create or checkout target branch
|
|
376
|
+
# 6. Create or checkout the target (wave) branch.
|
|
375
377
|
rc, _, _ = await _run_git("rev-parse", "--verify", target_branch, cwd=cwd, check=False)
|
|
376
378
|
branch_exists = rc == 0
|
|
377
379
|
|
|
378
380
|
if branch_exists:
|
|
379
|
-
|
|
381
|
+
# Branch already present locally (e.g. a continuing wave on a fresh
|
|
382
|
+
# clone): just switch to it.
|
|
383
|
+
rc, _, _ = await _run_git("checkout", target_branch, cwd=cwd, check=False)
|
|
384
|
+
if rc != 0:
|
|
385
|
+
return PreparationResult(
|
|
386
|
+
success=False,
|
|
387
|
+
branch_name=target_branch,
|
|
388
|
+
working_directory=cwd,
|
|
389
|
+
default_branch=default_branch,
|
|
390
|
+
is_worktree=False,
|
|
391
|
+
error=f"Failed to checkout existing branch {target_branch}",
|
|
392
|
+
)
|
|
380
393
|
logger.info(f"Switched to existing branch: {target_branch}")
|
|
394
|
+
elif effective_base != default_branch:
|
|
395
|
+
# Stacked cut: branch straight off the predecessor's remote branch.
|
|
396
|
+
# No default-branch sync — the predecessor already carries the work.
|
|
397
|
+
rc, _, _ = await _run_git(
|
|
398
|
+
"checkout", "-b", target_branch, f"origin/{effective_base}", cwd=cwd, check=False
|
|
399
|
+
)
|
|
400
|
+
if rc != 0:
|
|
401
|
+
return PreparationResult(
|
|
402
|
+
success=False,
|
|
403
|
+
branch_name=target_branch,
|
|
404
|
+
working_directory=cwd,
|
|
405
|
+
default_branch=default_branch,
|
|
406
|
+
is_worktree=False,
|
|
407
|
+
error=f"Failed to cut {target_branch} from origin/{effective_base}",
|
|
408
|
+
)
|
|
409
|
+
logger.info(f"Created {target_branch} stacked on origin/{effective_base}")
|
|
381
410
|
else:
|
|
411
|
+
# Default cut: sync to the default branch, then branch off it.
|
|
412
|
+
rc, _, _ = await _run_git("checkout", default_branch, cwd=cwd, check=False)
|
|
413
|
+
if rc != 0:
|
|
414
|
+
return PreparationResult(
|
|
415
|
+
success=False,
|
|
416
|
+
branch_name=target_branch,
|
|
417
|
+
working_directory=cwd,
|
|
418
|
+
default_branch=default_branch,
|
|
419
|
+
is_worktree=False,
|
|
420
|
+
error=f"Failed to checkout {default_branch}. Does the branch exist?",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
rc, _, _ = await _run_git(
|
|
424
|
+
"merge", "--ff-only", f"origin/{default_branch}", cwd=cwd, check=False
|
|
425
|
+
)
|
|
426
|
+
if rc != 0:
|
|
427
|
+
logger.warning(
|
|
428
|
+
f"Fast-forward merge failed for {default_branch}, resetting to origin"
|
|
429
|
+
)
|
|
430
|
+
await _run_git("reset", "--hard", f"origin/{default_branch}", cwd=cwd, check=False)
|
|
431
|
+
warnings.append(
|
|
432
|
+
f"Fast-forward failed, reset {default_branch} to origin/{default_branch}"
|
|
433
|
+
)
|
|
434
|
+
|
|
382
435
|
await _run_git("checkout", "-b", target_branch, cwd=cwd)
|
|
383
436
|
logger.info(f"Created new branch: {target_branch}")
|
|
384
437
|
|
|
@@ -395,7 +448,7 @@ class WorkspacePreparation:
|
|
|
395
448
|
)
|
|
396
449
|
|
|
397
450
|
await persist_branch_provenance(
|
|
398
|
-
task_id=task_id, branch=target_branch, base_branch=
|
|
451
|
+
task_id=task_id, branch=target_branch, base_branch=effective_base
|
|
399
452
|
)
|
|
400
453
|
|
|
401
454
|
return PreparationResult(
|
|
@@ -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")
|
|
@@ -368,6 +368,141 @@ class TestWorkspacePreparationInPlace:
|
|
|
368
368
|
assert any("fast-forward" in w.lower() or "reset" in w.lower() for w in result.warnings)
|
|
369
369
|
|
|
370
370
|
|
|
371
|
+
class TestWorkspacePreparationStackedWave:
|
|
372
|
+
"""Tests for stacked-wave cuts (wave-N branches off the predecessor wave)."""
|
|
373
|
+
|
|
374
|
+
@pytest.mark.asyncio
|
|
375
|
+
async def test_stacked_cut_branches_off_predecessor(
|
|
376
|
+
self, worktree_disabled, default_config, sample_task
|
|
377
|
+
):
|
|
378
|
+
"""A new wave with a predecessor branches off origin/<predecessor>."""
|
|
379
|
+
from steerdev_agent.prompt.builder import WaveContext
|
|
380
|
+
|
|
381
|
+
wave_ctx = WaveContext(
|
|
382
|
+
wave_number=2,
|
|
383
|
+
total_waves=3,
|
|
384
|
+
wave_name="wave-bbb222",
|
|
385
|
+
predecessor_branch="wave-aaa111",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
prep = WorkspacePreparation(
|
|
389
|
+
working_directory=Path("/tmp/repo"),
|
|
390
|
+
worktree_config=worktree_disabled,
|
|
391
|
+
branch_config=default_config,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
calls = []
|
|
395
|
+
|
|
396
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
397
|
+
calls.append(args)
|
|
398
|
+
cmd = " ".join(args)
|
|
399
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
400
|
+
return (0, "true", "")
|
|
401
|
+
if "branch --show-current" in cmd:
|
|
402
|
+
# Start on main; after checkout -b we're on the wave branch.
|
|
403
|
+
if any("checkout -b" in " ".join(c) for c in calls):
|
|
404
|
+
return (0, "wave-bbb222", "")
|
|
405
|
+
return (0, "main", "")
|
|
406
|
+
if "status --porcelain" in cmd:
|
|
407
|
+
return (0, "", "")
|
|
408
|
+
if "fetch origin" in cmd:
|
|
409
|
+
return (0, "", "")
|
|
410
|
+
if "rev-parse --verify origin/wave-aaa111" in cmd:
|
|
411
|
+
return (0, "deadbeef", "") # predecessor exists on origin
|
|
412
|
+
if "rev-parse --verify" in cmd:
|
|
413
|
+
return (1, "", "") # local wave branch doesn't exist yet
|
|
414
|
+
if "checkout -b" in cmd:
|
|
415
|
+
return (0, "", "")
|
|
416
|
+
if "checkout" in cmd:
|
|
417
|
+
return (0, "", "")
|
|
418
|
+
return (0, "", "")
|
|
419
|
+
|
|
420
|
+
with (
|
|
421
|
+
patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git),
|
|
422
|
+
patch("steerdev_agent.workspace.preparation.persist_branch_provenance") as mock_persist,
|
|
423
|
+
):
|
|
424
|
+
result = await prep.prepare(sample_task, wave_ctx)
|
|
425
|
+
|
|
426
|
+
assert result.success is True
|
|
427
|
+
assert result.branch_name == "wave-bbb222"
|
|
428
|
+
# The new branch is cut directly off the predecessor's remote branch.
|
|
429
|
+
checkout_b = [c for c in calls if "checkout -b" in " ".join(c)]
|
|
430
|
+
assert len(checkout_b) == 1
|
|
431
|
+
assert "origin/wave-aaa111" in " ".join(checkout_b[0])
|
|
432
|
+
# We must NOT sync to the default branch on the stacked path.
|
|
433
|
+
assert not any("checkout main" in " ".join(c) for c in calls)
|
|
434
|
+
# Provenance records the predecessor as the base so `steerdev git pr`
|
|
435
|
+
# targets it (stacked PR base).
|
|
436
|
+
mock_persist.assert_awaited_once_with(
|
|
437
|
+
task_id=sample_task["id"], branch="wave-bbb222", base_branch="wave-aaa111"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
@pytest.mark.asyncio
|
|
441
|
+
async def test_missing_predecessor_falls_back_to_default(
|
|
442
|
+
self, worktree_disabled, default_config, sample_task
|
|
443
|
+
):
|
|
444
|
+
"""If origin/<predecessor> is missing, cut from default + warn (degraded)."""
|
|
445
|
+
from steerdev_agent.prompt.builder import WaveContext
|
|
446
|
+
|
|
447
|
+
wave_ctx = WaveContext(
|
|
448
|
+
wave_number=2,
|
|
449
|
+
total_waves=3,
|
|
450
|
+
wave_name="wave-bbb222",
|
|
451
|
+
predecessor_branch="wave-gone",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
prep = WorkspacePreparation(
|
|
455
|
+
working_directory=Path("/tmp/repo"),
|
|
456
|
+
worktree_config=worktree_disabled,
|
|
457
|
+
branch_config=default_config,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
calls = []
|
|
461
|
+
|
|
462
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
463
|
+
calls.append(args)
|
|
464
|
+
cmd = " ".join(args)
|
|
465
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
466
|
+
return (0, "true", "")
|
|
467
|
+
if "branch --show-current" in cmd:
|
|
468
|
+
if any("checkout -b" in " ".join(c) for c in calls):
|
|
469
|
+
return (0, "wave-bbb222", "")
|
|
470
|
+
return (0, "main", "")
|
|
471
|
+
if "status --porcelain" in cmd:
|
|
472
|
+
return (0, "", "")
|
|
473
|
+
if "fetch origin" in cmd:
|
|
474
|
+
return (0, "", "")
|
|
475
|
+
if "rev-parse --verify origin/wave-gone" in cmd:
|
|
476
|
+
return (1, "", "") # predecessor NOT on origin
|
|
477
|
+
if "rev-parse --verify" in cmd:
|
|
478
|
+
return (1, "", "")
|
|
479
|
+
if "checkout main" in cmd:
|
|
480
|
+
return (0, "", "")
|
|
481
|
+
if "merge --ff-only" in cmd:
|
|
482
|
+
return (0, "", "")
|
|
483
|
+
if "checkout -b" in cmd:
|
|
484
|
+
return (0, "", "")
|
|
485
|
+
if "checkout" in cmd:
|
|
486
|
+
return (0, "", "")
|
|
487
|
+
return (0, "", "")
|
|
488
|
+
|
|
489
|
+
with (
|
|
490
|
+
patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git),
|
|
491
|
+
patch("steerdev_agent.workspace.preparation.persist_branch_provenance"),
|
|
492
|
+
):
|
|
493
|
+
result = await prep.prepare(sample_task, wave_ctx)
|
|
494
|
+
|
|
495
|
+
assert result.success is True
|
|
496
|
+
assert result.branch_name == "wave-bbb222"
|
|
497
|
+
# Fell back to the default-branch sync path.
|
|
498
|
+
assert any("checkout main" in " ".join(c) for c in calls)
|
|
499
|
+
# And surfaced a warning that the chain was broken.
|
|
500
|
+
assert any(
|
|
501
|
+
"wave-gone" in w or "predecessor" in w.lower() or "chain" in w.lower()
|
|
502
|
+
for w in result.warnings
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
371
506
|
class TestWorkspacePreparationMultiRepo:
|
|
372
507
|
"""Tests for multi-repo workspace preparation."""
|
|
373
508
|
|
|
@@ -2,12 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
from steerdev_agent.prompt.builder import WaveContext
|
|
4
4
|
from steerdev_agent.runner import (
|
|
5
|
+
build_wave_context,
|
|
5
6
|
build_workflow_task_context,
|
|
6
7
|
extract_task_and_wave_context,
|
|
7
8
|
resolve_merge_flow,
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
class TestPredecessorBranch:
|
|
13
|
+
"""build_wave_context derives the stacked-wave predecessor branch."""
|
|
14
|
+
|
|
15
|
+
def test_no_predecessor_when_field_absent(self):
|
|
16
|
+
wave = build_wave_context(_make_wave_response())
|
|
17
|
+
assert wave is not None
|
|
18
|
+
assert wave.predecessor_branch is None
|
|
19
|
+
|
|
20
|
+
def test_uses_predecessor_wave_name_as_branch(self):
|
|
21
|
+
response = _make_wave_response()
|
|
22
|
+
response["wave"]["predecessor_wave"] = {"name": "wave-aaa111"}
|
|
23
|
+
wave = build_wave_context(response)
|
|
24
|
+
assert wave is not None
|
|
25
|
+
assert wave.predecessor_branch == "wave-aaa111"
|
|
26
|
+
|
|
27
|
+
def test_blank_predecessor_name_yields_no_base(self):
|
|
28
|
+
# A blank name is not a valid branch — never guess "wave-N".
|
|
29
|
+
response = _make_wave_response()
|
|
30
|
+
response["wave"]["predecessor_wave"] = {"name": ""}
|
|
31
|
+
wave = build_wave_context(response)
|
|
32
|
+
assert wave is not None
|
|
33
|
+
assert wave.predecessor_branch is None
|
|
34
|
+
|
|
35
|
+
|
|
11
36
|
def _make_wave_context() -> WaveContext:
|
|
12
37
|
return WaveContext(
|
|
13
38
|
wave_number=3,
|
|
@@ -6,7 +6,23 @@ from unittest.mock import patch
|
|
|
6
6
|
from steerdev_agent.config.models import EvidenceConfig, WorktreeConfig
|
|
7
7
|
from steerdev_agent.prompt.builder import WaveContext
|
|
8
8
|
from steerdev_agent.runner import Runner
|
|
9
|
-
from steerdev_agent.worktree import compute_branch_name
|
|
9
|
+
from steerdev_agent.worktree import compute_branch_name, wave_branch_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestWaveBranchName:
|
|
13
|
+
"""The single source of truth for a wave's branch name."""
|
|
14
|
+
|
|
15
|
+
def test_uses_name_when_present(self):
|
|
16
|
+
assert wave_branch_name("wave-a1b2c3", 3) == "wave-a1b2c3"
|
|
17
|
+
|
|
18
|
+
def test_falls_back_to_number(self):
|
|
19
|
+
assert wave_branch_name("", 3) == "wave-3"
|
|
20
|
+
assert wave_branch_name(None, 4) == "wave-4"
|
|
21
|
+
|
|
22
|
+
def test_matches_compute_branch_name_wave_output(self):
|
|
23
|
+
wave = WaveContext(wave_number=3, total_waves=5, wave_name="wave-a1b2c3")
|
|
24
|
+
task = {"id": "abc123", "title": "Something"}
|
|
25
|
+
assert wave_branch_name(wave.wave_name, wave.wave_number) == compute_branch_name(task, wave)
|
|
10
26
|
|
|
11
27
|
|
|
12
28
|
class TestComputeBranchName:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.1.29 → steerdev-1.1.31}/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
|
{steerdev-1.1.29 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/worktrunk.config.toml
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
|