steerdev 1.0.60__tar.gz → 1.1.3__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.0.60 → steerdev-1.1.3}/PKG-INFO +1 -1
- {steerdev-1.0.60 → steerdev-1.1.3}/pyproject.toml +1 -1
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/prompt/builder.py +1 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/runner.py +4 -3
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workspace/preparation.py +133 -11
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/worktree.py +2 -2
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_preparation.py +312 -6
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_runner_merge_modes.py +2 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_runner_worktrees.py +4 -4
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_worktree.py +2 -2
- {steerdev-1.0.60 → steerdev-1.1.3}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/.github/workflows/publish.yml +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/.gitignore +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/.pre-commit-config.yaml +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/AGENTS.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/CLAUDE.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/README.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/cli.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/__init__.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_agent_loop.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_agents_api.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_api_client.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_claude_executor.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_claude_setup.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_client_methods.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_commands_api.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_config.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_config_extended.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_context_search.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_executor.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_platform_config.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_prompt.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_reports_client.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_retry.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_stream_parser.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_tasks.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_version.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_workflow_context.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_workspace.py +0 -0
- {steerdev-1.0.60 → steerdev-1.1.3}/tests/test_workspace_extended.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerdev
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.3
|
|
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
|
|
@@ -24,6 +24,7 @@ class WaveContext(BaseModel):
|
|
|
24
24
|
|
|
25
25
|
wave_number: int = Field(description="Current wave number")
|
|
26
26
|
total_waves: int = Field(description="Total number of waves")
|
|
27
|
+
wave_name: str = Field(default="", description="Unique wave name (e.g., wave-532d5c)")
|
|
27
28
|
wave_description: str = Field(default="", description="Description of this wave")
|
|
28
29
|
wave_tasks_summary: str = Field(default="", description="Summary of tasks in this wave")
|
|
29
30
|
completed_waves_summary: str = Field(default="", description="Summary of completed waves")
|
|
@@ -111,6 +111,7 @@ def build_wave_context(wave_response: dict[str, Any]) -> WaveContext | None:
|
|
|
111
111
|
return WaveContext(
|
|
112
112
|
wave_number=wave_info.get("wave_number", 1),
|
|
113
113
|
total_waves=wave_info.get("total_waves", 1),
|
|
114
|
+
wave_name=wave_info.get("name", ""),
|
|
114
115
|
wave_description=wave_info.get("description", ""),
|
|
115
116
|
wave_tasks_summary=tasks_summary,
|
|
116
117
|
completed_waves_summary=completed_summary,
|
|
@@ -187,13 +188,13 @@ def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None)
|
|
|
187
188
|
"""Compute the git worktree name for a task execution.
|
|
188
189
|
|
|
189
190
|
Naming conventions:
|
|
190
|
-
- Wave-based: "
|
|
191
|
+
- Wave-based: "{wave_name}" (e.g., "wave-532d5c")
|
|
191
192
|
- Task-based: "task-{first_8_chars}" (e.g., "task-abc12345")
|
|
192
193
|
|
|
193
194
|
The worktree lifecycle is managed by Claude CLI via the --worktree flag.
|
|
194
195
|
"""
|
|
195
196
|
if wave_context:
|
|
196
|
-
return f"wave-{wave_context.wave_number}"
|
|
197
|
+
return wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
197
198
|
task_short = task_id[:8] if len(task_id) > 8 else task_id
|
|
198
199
|
return f"task-{task_short}"
|
|
199
200
|
|
|
@@ -643,7 +644,7 @@ class Runner:
|
|
|
643
644
|
|
|
644
645
|
wave_id: str | None = None
|
|
645
646
|
if wave_context:
|
|
646
|
-
wave_id = f"wave-{wave_context.wave_number}"
|
|
647
|
+
wave_id = wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
647
648
|
|
|
648
649
|
try:
|
|
649
650
|
with ReportsClient(api_key=self._api_key) as client:
|
|
@@ -35,6 +35,8 @@ class PreparationResult:
|
|
|
35
35
|
is_worktree: bool
|
|
36
36
|
error: str | None = None
|
|
37
37
|
warnings: list[str] = field(default_factory=list)
|
|
38
|
+
repo_directories: list[Path] = field(default_factory=list)
|
|
39
|
+
"""For multi-repo workspaces, the list of prepared git repositories."""
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
async def _run_git(
|
|
@@ -145,13 +147,23 @@ class WorkspacePreparation:
|
|
|
145
147
|
default_branch: str,
|
|
146
148
|
wave_context: WaveContext | None,
|
|
147
149
|
) -> PreparationResult:
|
|
148
|
-
"""Prepare workspace in the main repository directory.
|
|
150
|
+
"""Prepare workspace in the main repository directory.
|
|
151
|
+
|
|
152
|
+
If the working directory is not itself a git repository, discovers
|
|
153
|
+
git repos in immediate subdirectories (multi-repo layout) and
|
|
154
|
+
prepares each one.
|
|
155
|
+
"""
|
|
149
156
|
cwd = self.working_directory
|
|
150
|
-
warnings: list[str] = []
|
|
151
157
|
|
|
152
|
-
# 1.
|
|
158
|
+
# 1. Check if working directory is a git repo
|
|
153
159
|
rc, _, _ = await _run_git("rev-parse", "--is-inside-work-tree", cwd=cwd, check=False)
|
|
154
|
-
if rc
|
|
160
|
+
if rc == 0:
|
|
161
|
+
# Single-repo layout: prepare in place
|
|
162
|
+
return await self._prepare_single_repo(cwd, target_branch, default_branch, wave_context)
|
|
163
|
+
|
|
164
|
+
# Not a git repo — check for multi-repo layout (subdirectories that are git repos)
|
|
165
|
+
repos = await self._discover_git_repos(cwd)
|
|
166
|
+
if not repos:
|
|
155
167
|
return PreparationResult(
|
|
156
168
|
success=False,
|
|
157
169
|
branch_name=target_branch,
|
|
@@ -161,10 +173,120 @@ class WorkspacePreparation:
|
|
|
161
173
|
error=f"Not a git repository: {cwd}",
|
|
162
174
|
)
|
|
163
175
|
|
|
164
|
-
#
|
|
176
|
+
# Multi-repo layout: prepare each discovered repo
|
|
177
|
+
return await self._prepare_multi_repo(repos, target_branch, default_branch, wave_context)
|
|
178
|
+
|
|
179
|
+
async def _discover_git_repos(self, directory: Path) -> list[Path]:
|
|
180
|
+
"""Discover git repositories in immediate subdirectories.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
directory: Parent directory to scan.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Sorted list of paths that are git repositories.
|
|
187
|
+
"""
|
|
188
|
+
repos: list[Path] = []
|
|
189
|
+
try:
|
|
190
|
+
for entry in sorted(directory.iterdir()):
|
|
191
|
+
if entry.is_dir() and not entry.name.startswith("."):
|
|
192
|
+
rc, _, _ = await _run_git(
|
|
193
|
+
"rev-parse", "--is-inside-work-tree", cwd=entry, check=False
|
|
194
|
+
)
|
|
195
|
+
if rc == 0:
|
|
196
|
+
repos.append(entry)
|
|
197
|
+
except OSError as exc:
|
|
198
|
+
logger.warning(f"Failed to scan directory {directory}: {exc}")
|
|
199
|
+
return repos
|
|
200
|
+
|
|
201
|
+
async def _prepare_multi_repo(
|
|
202
|
+
self,
|
|
203
|
+
repos: list[Path],
|
|
204
|
+
target_branch: str,
|
|
205
|
+
default_branch: str,
|
|
206
|
+
wave_context: WaveContext | None,
|
|
207
|
+
) -> PreparationResult:
|
|
208
|
+
"""Prepare multiple git repositories in a workspace directory.
|
|
209
|
+
|
|
210
|
+
Applies the same branch preparation to each discovered repo.
|
|
211
|
+
Returns success only if all repos are prepared successfully.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
repos: List of git repository paths to prepare.
|
|
215
|
+
target_branch: Target branch name for the task.
|
|
216
|
+
default_branch: Default branch to sync from.
|
|
217
|
+
wave_context: Optional wave context.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
PreparationResult with the parent directory as working_directory.
|
|
221
|
+
"""
|
|
222
|
+
parent_dir = self.working_directory
|
|
223
|
+
all_warnings: list[str] = []
|
|
224
|
+
prepared_repos: list[Path] = []
|
|
225
|
+
|
|
226
|
+
logger.info(
|
|
227
|
+
f"Multi-repo workspace detected at {parent_dir}: preparing {len(repos)} repositories"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
for repo_path in repos:
|
|
231
|
+
repo_name = repo_path.name
|
|
232
|
+
logger.info(f"Preparing repository: {repo_name}")
|
|
233
|
+
|
|
234
|
+
result = await self._prepare_single_repo(
|
|
235
|
+
repo_path, target_branch, default_branch, wave_context
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not result.success:
|
|
239
|
+
return PreparationResult(
|
|
240
|
+
success=False,
|
|
241
|
+
branch_name=target_branch,
|
|
242
|
+
working_directory=parent_dir,
|
|
243
|
+
default_branch=default_branch,
|
|
244
|
+
is_worktree=False,
|
|
245
|
+
error=f"Failed to prepare repo {repo_name}: {result.error}",
|
|
246
|
+
repo_directories=prepared_repos,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
prepared_repos.append(repo_path)
|
|
250
|
+
# Prefix per-repo warnings with the repo name for clarity
|
|
251
|
+
for warning in result.warnings:
|
|
252
|
+
all_warnings.append(f"[{repo_name}] {warning}")
|
|
253
|
+
|
|
254
|
+
logger.info(f"Multi-repo workspace ready: {len(prepared_repos)} repositories prepared")
|
|
255
|
+
|
|
256
|
+
return PreparationResult(
|
|
257
|
+
success=True,
|
|
258
|
+
branch_name=target_branch,
|
|
259
|
+
working_directory=parent_dir,
|
|
260
|
+
default_branch=default_branch,
|
|
261
|
+
is_worktree=False,
|
|
262
|
+
warnings=all_warnings,
|
|
263
|
+
repo_directories=prepared_repos,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def _prepare_single_repo(
|
|
267
|
+
self,
|
|
268
|
+
cwd: Path,
|
|
269
|
+
target_branch: str,
|
|
270
|
+
default_branch: str,
|
|
271
|
+
wave_context: WaveContext | None,
|
|
272
|
+
) -> PreparationResult:
|
|
273
|
+
"""Prepare a single git repository directory.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
cwd: Path to the git repository.
|
|
277
|
+
target_branch: Target branch name for the task.
|
|
278
|
+
default_branch: Default branch to sync from.
|
|
279
|
+
wave_context: Optional wave context.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
PreparationResult for this repository.
|
|
283
|
+
"""
|
|
284
|
+
warnings: list[str] = []
|
|
285
|
+
|
|
286
|
+
# 1. Check current branch
|
|
165
287
|
_, current_branch, _ = await _run_git("branch", "--show-current", cwd=cwd, check=False)
|
|
166
288
|
|
|
167
|
-
#
|
|
289
|
+
# 2. If continuing a wave on the same branch, just fetch
|
|
168
290
|
if wave_context and current_branch == target_branch:
|
|
169
291
|
logger.info(f"Continuing wave on branch {target_branch}, fetching only")
|
|
170
292
|
await _run_git("fetch", "origin", cwd=cwd, check=False)
|
|
@@ -176,17 +298,17 @@ class WorkspacePreparation:
|
|
|
176
298
|
is_worktree=False,
|
|
177
299
|
)
|
|
178
300
|
|
|
179
|
-
#
|
|
301
|
+
# 3. Stash if dirty
|
|
180
302
|
rc, status_output, _ = await _run_git("status", "--porcelain", cwd=cwd, check=False)
|
|
181
303
|
if status_output:
|
|
182
304
|
logger.info("Working directory dirty, stashing changes")
|
|
183
305
|
await _run_git("stash", "push", "-m", "steerdev-preflight", cwd=cwd, check=False)
|
|
184
306
|
warnings.append("Stashed uncommitted changes (steerdev-preflight)")
|
|
185
307
|
|
|
186
|
-
#
|
|
308
|
+
# 4. Fetch from origin
|
|
187
309
|
await _run_git("fetch", "origin", cwd=cwd, check=False)
|
|
188
310
|
|
|
189
|
-
#
|
|
311
|
+
# 5. Sync to default branch
|
|
190
312
|
rc, _, _ = await _run_git("checkout", default_branch, cwd=cwd, check=False)
|
|
191
313
|
if rc != 0:
|
|
192
314
|
return PreparationResult(
|
|
@@ -210,7 +332,7 @@ class WorkspacePreparation:
|
|
|
210
332
|
f"Fast-forward failed, reset {default_branch} to origin/{default_branch}"
|
|
211
333
|
)
|
|
212
334
|
|
|
213
|
-
#
|
|
335
|
+
# 6. Create or checkout target branch
|
|
214
336
|
rc, _, _ = await _run_git("rev-parse", "--verify", target_branch, cwd=cwd, check=False)
|
|
215
337
|
branch_exists = rc == 0
|
|
216
338
|
|
|
@@ -221,7 +343,7 @@ class WorkspacePreparation:
|
|
|
221
343
|
await _run_git("checkout", "-b", target_branch, cwd=cwd)
|
|
222
344
|
logger.info(f"Created new branch: {target_branch}")
|
|
223
345
|
|
|
224
|
-
#
|
|
346
|
+
# 7. Validate current branch
|
|
225
347
|
_, actual_branch, _ = await _run_git("branch", "--show-current", cwd=cwd, check=False)
|
|
226
348
|
if actual_branch != target_branch:
|
|
227
349
|
return PreparationResult(
|
|
@@ -213,7 +213,7 @@ def compute_branch_name(
|
|
|
213
213
|
"""Compute the branch name for a task or wave.
|
|
214
214
|
|
|
215
215
|
Naming conventions aligned with merge skill branch patterns:
|
|
216
|
-
- Wave: "
|
|
216
|
+
- Wave: "{wave_name}" (e.g., "wave-532d5c")
|
|
217
217
|
- Task with external ID: "task/{TICKET-ID}-{slug}" (e.g., "task/PROJ-123-add-auth")
|
|
218
218
|
- Task without external ID: "task/{short-id}-{slug}" (e.g., "task/abc12345-add-auth")
|
|
219
219
|
|
|
@@ -225,7 +225,7 @@ def compute_branch_name(
|
|
|
225
225
|
Branch name string.
|
|
226
226
|
"""
|
|
227
227
|
if wave_context:
|
|
228
|
-
return f"wave
|
|
228
|
+
return wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
229
229
|
|
|
230
230
|
# Try external ticket ID first (e.g., Linear "PROJ-123")
|
|
231
231
|
external_id = task.get("linear_identifier") or task.get("external_id")
|
|
@@ -211,7 +211,7 @@ class TestWorkspacePreparationInPlace:
|
|
|
211
211
|
"""Continuing a wave stays on the wave branch, only fetches."""
|
|
212
212
|
from steerdev_agent.prompt.builder import WaveContext
|
|
213
213
|
|
|
214
|
-
wave_ctx = WaveContext(wave_number=3, total_waves=5)
|
|
214
|
+
wave_ctx = WaveContext(wave_number=3, total_waves=5, wave_name="wave-a1b2c3")
|
|
215
215
|
|
|
216
216
|
prep = WorkspacePreparation(
|
|
217
217
|
working_directory=Path("/tmp/repo"),
|
|
@@ -227,7 +227,7 @@ class TestWorkspacePreparationInPlace:
|
|
|
227
227
|
if "rev-parse --is-inside-work-tree" in cmd:
|
|
228
228
|
return (0, "true", "")
|
|
229
229
|
if "branch --show-current" in cmd:
|
|
230
|
-
return (0, "wave
|
|
230
|
+
return (0, "wave-a1b2c3", "") # Already on wave branch
|
|
231
231
|
if "fetch origin" in cmd:
|
|
232
232
|
return (0, "", "")
|
|
233
233
|
return (0, "", "")
|
|
@@ -236,7 +236,7 @@ class TestWorkspacePreparationInPlace:
|
|
|
236
236
|
result = await prep.prepare(sample_task, wave_ctx)
|
|
237
237
|
|
|
238
238
|
assert result.success is True
|
|
239
|
-
assert result.branch_name == "wave
|
|
239
|
+
assert result.branch_name == "wave-a1b2c3"
|
|
240
240
|
# Should NOT have checkout commands (stayed on branch)
|
|
241
241
|
checkout_cmds = [c for c in calls if c[0] == "checkout"]
|
|
242
242
|
assert len(checkout_cmds) == 0
|
|
@@ -290,10 +290,12 @@ class TestWorkspacePreparationInPlace:
|
|
|
290
290
|
assert len(stash_cmds) > 0
|
|
291
291
|
|
|
292
292
|
@pytest.mark.asyncio
|
|
293
|
-
async def
|
|
294
|
-
|
|
293
|
+
async def test_not_git_repo_no_subrepos_fails(
|
|
294
|
+
self, worktree_disabled, default_config, sample_task, tmp_path
|
|
295
|
+
):
|
|
296
|
+
"""Non-git directory with no sub-repos returns failure."""
|
|
295
297
|
prep = WorkspacePreparation(
|
|
296
|
-
working_directory=
|
|
298
|
+
working_directory=tmp_path,
|
|
297
299
|
worktree_config=worktree_disabled,
|
|
298
300
|
branch_config=default_config,
|
|
299
301
|
)
|
|
@@ -356,3 +358,307 @@ class TestWorkspacePreparationInPlace:
|
|
|
356
358
|
reset_cmds = [c for c in calls if "reset" in " ".join(c)]
|
|
357
359
|
assert len(reset_cmds) > 0
|
|
358
360
|
assert any("fast-forward" in w.lower() or "reset" in w.lower() for w in result.warnings)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TestWorkspacePreparationMultiRepo:
|
|
364
|
+
"""Tests for multi-repo workspace preparation."""
|
|
365
|
+
|
|
366
|
+
@pytest.fixture
|
|
367
|
+
def worktree_disabled(self):
|
|
368
|
+
return WorktreeConfig(enabled=False)
|
|
369
|
+
|
|
370
|
+
@pytest.fixture
|
|
371
|
+
def default_config(self):
|
|
372
|
+
return BranchConfig(default_branch="main")
|
|
373
|
+
|
|
374
|
+
@pytest.fixture
|
|
375
|
+
def sample_task(self):
|
|
376
|
+
return {
|
|
377
|
+
"id": "abc12345-6789-0000-0000-000000000000",
|
|
378
|
+
"title": "Add user authentication",
|
|
379
|
+
"prompt": "Implement user auth",
|
|
380
|
+
"status": "started",
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
async def test_multi_repo_discovers_and_prepares_all(
|
|
385
|
+
self, worktree_disabled, default_config, sample_task, tmp_path
|
|
386
|
+
):
|
|
387
|
+
"""Multi-repo layout: discovers sub-repos and prepares each one."""
|
|
388
|
+
# Create subdirectories to simulate multi-repo layout
|
|
389
|
+
(tmp_path / "repo_a").mkdir()
|
|
390
|
+
(tmp_path / "repo_b").mkdir()
|
|
391
|
+
(tmp_path / ".hidden").mkdir() # should be skipped
|
|
392
|
+
|
|
393
|
+
prep = WorkspacePreparation(
|
|
394
|
+
working_directory=tmp_path,
|
|
395
|
+
worktree_config=worktree_disabled,
|
|
396
|
+
branch_config=default_config,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
calls: list[tuple[tuple[str, ...], Path]] = []
|
|
400
|
+
|
|
401
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
402
|
+
calls.append((args, Path(cwd)))
|
|
403
|
+
cmd = " ".join(args)
|
|
404
|
+
cwd_path = Path(cwd)
|
|
405
|
+
|
|
406
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
407
|
+
# Parent dir is NOT a repo; subdirs ARE repos
|
|
408
|
+
if cwd_path == tmp_path:
|
|
409
|
+
return (128, "", "fatal: not a git repository")
|
|
410
|
+
if cwd_path.name in ("repo_a", "repo_b"):
|
|
411
|
+
return (0, "true", "")
|
|
412
|
+
return (128, "", "fatal: not a git repository")
|
|
413
|
+
if "branch --show-current" in cmd:
|
|
414
|
+
# Track per-repo calls to return correct branch after checkout
|
|
415
|
+
show_current_calls = [
|
|
416
|
+
c
|
|
417
|
+
for c in calls
|
|
418
|
+
if "branch --show-current" in " ".join(c[0]) and c[1] == cwd_path
|
|
419
|
+
]
|
|
420
|
+
if len(show_current_calls) <= 1:
|
|
421
|
+
return (0, "main", "")
|
|
422
|
+
return (0, "task/abc12345-add-user-authentication", "")
|
|
423
|
+
if "status --porcelain" in cmd:
|
|
424
|
+
return (0, "", "")
|
|
425
|
+
if "fetch origin" in cmd:
|
|
426
|
+
return (0, "", "")
|
|
427
|
+
if "checkout main" in cmd:
|
|
428
|
+
return (0, "", "")
|
|
429
|
+
if "merge --ff-only" in cmd:
|
|
430
|
+
return (0, "", "")
|
|
431
|
+
if "rev-parse --verify" in cmd:
|
|
432
|
+
return (1, "", "") # branch doesn't exist
|
|
433
|
+
if "checkout -b" in cmd:
|
|
434
|
+
return (0, "", "")
|
|
435
|
+
if "checkout" in cmd:
|
|
436
|
+
return (0, "", "")
|
|
437
|
+
return (0, "", "")
|
|
438
|
+
|
|
439
|
+
with patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git):
|
|
440
|
+
result = await prep.prepare(sample_task)
|
|
441
|
+
|
|
442
|
+
assert result.success is True
|
|
443
|
+
assert result.working_directory == tmp_path # parent dir preserved
|
|
444
|
+
assert len(result.repo_directories) == 2
|
|
445
|
+
assert tmp_path / "repo_a" in result.repo_directories
|
|
446
|
+
assert tmp_path / "repo_b" in result.repo_directories
|
|
447
|
+
# Verify git operations ran against both repos
|
|
448
|
+
repo_a_checkouts = [
|
|
449
|
+
c for c in calls if c[1] == tmp_path / "repo_a" and "checkout -b" in " ".join(c[0])
|
|
450
|
+
]
|
|
451
|
+
repo_b_checkouts = [
|
|
452
|
+
c for c in calls if c[1] == tmp_path / "repo_b" and "checkout -b" in " ".join(c[0])
|
|
453
|
+
]
|
|
454
|
+
assert len(repo_a_checkouts) == 1
|
|
455
|
+
assert len(repo_b_checkouts) == 1
|
|
456
|
+
|
|
457
|
+
@pytest.mark.asyncio
|
|
458
|
+
async def test_multi_repo_fails_if_one_repo_fails(
|
|
459
|
+
self, worktree_disabled, default_config, sample_task, tmp_path
|
|
460
|
+
):
|
|
461
|
+
"""Multi-repo: failure in one repo fails the whole preparation."""
|
|
462
|
+
(tmp_path / "repo_a").mkdir()
|
|
463
|
+
(tmp_path / "repo_b").mkdir()
|
|
464
|
+
|
|
465
|
+
prep = WorkspacePreparation(
|
|
466
|
+
working_directory=tmp_path,
|
|
467
|
+
worktree_config=worktree_disabled,
|
|
468
|
+
branch_config=default_config,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Track per-repo branch --show-current calls
|
|
472
|
+
branch_calls_per_repo: dict[str, int] = {}
|
|
473
|
+
|
|
474
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
475
|
+
cmd = " ".join(args)
|
|
476
|
+
cwd_path = Path(cwd)
|
|
477
|
+
repo_name = cwd_path.name
|
|
478
|
+
|
|
479
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
480
|
+
if cwd_path == tmp_path:
|
|
481
|
+
return (128, "", "fatal: not a git repository")
|
|
482
|
+
if repo_name in ("repo_a", "repo_b"):
|
|
483
|
+
return (0, "true", "")
|
|
484
|
+
return (128, "", "")
|
|
485
|
+
if "branch --show-current" in cmd:
|
|
486
|
+
branch_calls_per_repo[repo_name] = branch_calls_per_repo.get(repo_name, 0) + 1
|
|
487
|
+
if branch_calls_per_repo[repo_name] <= 1:
|
|
488
|
+
return (0, "main", "")
|
|
489
|
+
return (0, "task/abc12345-add-user-authentication", "")
|
|
490
|
+
if "status --porcelain" in cmd:
|
|
491
|
+
return (0, "", "")
|
|
492
|
+
if "fetch origin" in cmd:
|
|
493
|
+
return (0, "", "")
|
|
494
|
+
if "checkout main" in cmd:
|
|
495
|
+
# repo_b fails to checkout default branch
|
|
496
|
+
if repo_name == "repo_b":
|
|
497
|
+
return (1, "", "error: pathspec 'main' did not match")
|
|
498
|
+
return (0, "", "")
|
|
499
|
+
if "merge --ff-only" in cmd:
|
|
500
|
+
return (0, "", "")
|
|
501
|
+
if "rev-parse --verify" in cmd:
|
|
502
|
+
return (1, "", "")
|
|
503
|
+
if "checkout -b" in cmd:
|
|
504
|
+
return (0, "", "")
|
|
505
|
+
return (0, "", "")
|
|
506
|
+
|
|
507
|
+
with patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git):
|
|
508
|
+
result = await prep.prepare(sample_task)
|
|
509
|
+
|
|
510
|
+
assert result.success is False
|
|
511
|
+
assert "repo_b" in (result.error or "")
|
|
512
|
+
# repo_a was prepared before repo_b failed
|
|
513
|
+
assert len(result.repo_directories) == 1
|
|
514
|
+
assert tmp_path / "repo_a" in result.repo_directories
|
|
515
|
+
|
|
516
|
+
@pytest.mark.asyncio
|
|
517
|
+
async def test_multi_repo_collects_warnings_with_prefix(
|
|
518
|
+
self, worktree_disabled, default_config, sample_task, tmp_path
|
|
519
|
+
):
|
|
520
|
+
"""Multi-repo: warnings are prefixed with repo name."""
|
|
521
|
+
(tmp_path / "repo_a").mkdir()
|
|
522
|
+
|
|
523
|
+
prep = WorkspacePreparation(
|
|
524
|
+
working_directory=tmp_path,
|
|
525
|
+
worktree_config=worktree_disabled,
|
|
526
|
+
branch_config=default_config,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
branch_call_count = [0]
|
|
530
|
+
|
|
531
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
532
|
+
cmd = " ".join(args)
|
|
533
|
+
cwd_path = Path(cwd)
|
|
534
|
+
|
|
535
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
536
|
+
if cwd_path == tmp_path:
|
|
537
|
+
return (128, "", "fatal: not a git repository")
|
|
538
|
+
return (0, "true", "")
|
|
539
|
+
if "branch --show-current" in cmd:
|
|
540
|
+
# Return target branch on validation check
|
|
541
|
+
branch_call_count[0] += 1
|
|
542
|
+
if branch_call_count[0] > 1:
|
|
543
|
+
return (0, "task/abc12345-add-user-authentication", "")
|
|
544
|
+
return (0, "main", "")
|
|
545
|
+
if "status --porcelain" in cmd:
|
|
546
|
+
return (0, "M dirty.py", "") # dirty repo
|
|
547
|
+
if "stash push" in cmd:
|
|
548
|
+
return (0, "", "")
|
|
549
|
+
if "fetch origin" in cmd:
|
|
550
|
+
return (0, "", "")
|
|
551
|
+
if "checkout main" in cmd:
|
|
552
|
+
return (0, "", "")
|
|
553
|
+
if "merge --ff-only" in cmd:
|
|
554
|
+
return (0, "", "")
|
|
555
|
+
if "rev-parse --verify" in cmd:
|
|
556
|
+
return (1, "", "")
|
|
557
|
+
if "checkout -b" in cmd:
|
|
558
|
+
return (0, "", "")
|
|
559
|
+
return (0, "", "")
|
|
560
|
+
|
|
561
|
+
with patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git):
|
|
562
|
+
result = await prep.prepare(sample_task)
|
|
563
|
+
|
|
564
|
+
assert result.success is True
|
|
565
|
+
assert any("[repo_a]" in w for w in result.warnings)
|
|
566
|
+
|
|
567
|
+
@pytest.mark.asyncio
|
|
568
|
+
async def test_multi_repo_skips_hidden_and_non_repo_dirs(
|
|
569
|
+
self, worktree_disabled, default_config, sample_task, tmp_path
|
|
570
|
+
):
|
|
571
|
+
"""Multi-repo: hidden dirs and non-repo dirs are skipped."""
|
|
572
|
+
(tmp_path / ".hidden_repo").mkdir()
|
|
573
|
+
(tmp_path / "not_a_repo").mkdir()
|
|
574
|
+
(tmp_path / "actual_repo").mkdir()
|
|
575
|
+
|
|
576
|
+
prep = WorkspacePreparation(
|
|
577
|
+
working_directory=tmp_path,
|
|
578
|
+
worktree_config=worktree_disabled,
|
|
579
|
+
branch_config=default_config,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
calls_by_cwd: dict[str, list[str]] = {}
|
|
583
|
+
|
|
584
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
585
|
+
cmd = " ".join(args)
|
|
586
|
+
cwd_path = Path(cwd)
|
|
587
|
+
calls_by_cwd.setdefault(cwd_path.name, []).append(cmd)
|
|
588
|
+
|
|
589
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
590
|
+
if cwd_path == tmp_path:
|
|
591
|
+
return (128, "", "fatal: not a git repository")
|
|
592
|
+
if cwd_path.name == "actual_repo":
|
|
593
|
+
return (0, "true", "")
|
|
594
|
+
return (128, "", "fatal: not a git repository")
|
|
595
|
+
if "branch --show-current" in cmd:
|
|
596
|
+
branch_calls = [
|
|
597
|
+
c for c in calls_by_cwd.get(cwd_path.name, []) if "branch --show-current" in c
|
|
598
|
+
]
|
|
599
|
+
if len(branch_calls) <= 1:
|
|
600
|
+
return (0, "main", "")
|
|
601
|
+
return (0, "task/abc12345-add-user-authentication", "")
|
|
602
|
+
if "status --porcelain" in cmd:
|
|
603
|
+
return (0, "", "")
|
|
604
|
+
if "fetch origin" in cmd:
|
|
605
|
+
return (0, "", "")
|
|
606
|
+
if "checkout main" in cmd:
|
|
607
|
+
return (0, "", "")
|
|
608
|
+
if "merge --ff-only" in cmd:
|
|
609
|
+
return (0, "", "")
|
|
610
|
+
if "rev-parse --verify" in cmd:
|
|
611
|
+
return (1, "", "")
|
|
612
|
+
if "checkout -b" in cmd:
|
|
613
|
+
return (0, "", "")
|
|
614
|
+
return (0, "", "")
|
|
615
|
+
|
|
616
|
+
with patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git):
|
|
617
|
+
result = await prep.prepare(sample_task)
|
|
618
|
+
|
|
619
|
+
assert result.success is True
|
|
620
|
+
assert len(result.repo_directories) == 1
|
|
621
|
+
assert tmp_path / "actual_repo" in result.repo_directories
|
|
622
|
+
# Hidden dir should never have been checked
|
|
623
|
+
assert ".hidden_repo" not in calls_by_cwd
|
|
624
|
+
|
|
625
|
+
@pytest.mark.asyncio
|
|
626
|
+
async def test_single_repo_still_works(self, worktree_disabled, default_config, sample_task):
|
|
627
|
+
"""Single-repo layout still works as before (no regressions)."""
|
|
628
|
+
prep = WorkspacePreparation(
|
|
629
|
+
working_directory=Path("/tmp/repo"),
|
|
630
|
+
worktree_config=worktree_disabled,
|
|
631
|
+
branch_config=default_config,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
calls = []
|
|
635
|
+
|
|
636
|
+
async def mock_run_git(*args, cwd, check=True):
|
|
637
|
+
calls.append(args)
|
|
638
|
+
cmd = " ".join(args)
|
|
639
|
+
if "rev-parse --is-inside-work-tree" in cmd:
|
|
640
|
+
return (0, "true", "")
|
|
641
|
+
if "branch --show-current" in cmd:
|
|
642
|
+
if len([c for c in calls if "branch --show-current" in " ".join(c)]) <= 1:
|
|
643
|
+
return (0, "main", "")
|
|
644
|
+
return (0, "task/abc12345-add-user-authentication", "")
|
|
645
|
+
if "status --porcelain" in cmd:
|
|
646
|
+
return (0, "", "")
|
|
647
|
+
if "fetch origin" in cmd:
|
|
648
|
+
return (0, "", "")
|
|
649
|
+
if "checkout main" in cmd:
|
|
650
|
+
return (0, "", "")
|
|
651
|
+
if "merge --ff-only" in cmd:
|
|
652
|
+
return (0, "", "")
|
|
653
|
+
if "rev-parse --verify" in cmd:
|
|
654
|
+
return (1, "", "")
|
|
655
|
+
if "checkout -b" in cmd:
|
|
656
|
+
return (0, "", "")
|
|
657
|
+
return (0, "", "")
|
|
658
|
+
|
|
659
|
+
with patch("steerdev_agent.workspace.preparation._run_git", side_effect=mock_run_git):
|
|
660
|
+
result = await prep.prepare(sample_task)
|
|
661
|
+
|
|
662
|
+
assert result.success is True
|
|
663
|
+
assert result.repo_directories == [] # single-repo: no repo_directories
|
|
664
|
+
assert result.working_directory == Path("/tmp/repo")
|
|
@@ -12,6 +12,7 @@ def _make_wave_context() -> WaveContext:
|
|
|
12
12
|
return WaveContext(
|
|
13
13
|
wave_number=3,
|
|
14
14
|
total_waves=5,
|
|
15
|
+
wave_name="wave-a1b2c3",
|
|
15
16
|
wave_description="User management improvements",
|
|
16
17
|
wave_tasks_summary=" [todo] Add user endpoint",
|
|
17
18
|
completed_waves_summary=" - Wave 2: Auth cleanup",
|
|
@@ -23,6 +24,7 @@ def _make_wave_response(task_id: str = "task-123") -> dict:
|
|
|
23
24
|
"wave": {
|
|
24
25
|
"wave_number": 3,
|
|
25
26
|
"total_waves": 5,
|
|
27
|
+
"name": "wave-a1b2c3",
|
|
26
28
|
"description": "User management improvements",
|
|
27
29
|
},
|
|
28
30
|
"context": {
|
|
@@ -25,13 +25,13 @@ class TestComputeBranchName:
|
|
|
25
25
|
|
|
26
26
|
def test_wave_based(self):
|
|
27
27
|
task = {"id": "abc123", "title": "Something"}
|
|
28
|
-
wave = WaveContext(wave_number=3, total_waves=5)
|
|
29
|
-
assert compute_branch_name(task, wave) == "wave
|
|
28
|
+
wave = WaveContext(wave_number=3, total_waves=5, wave_name="wave-a1b2c3")
|
|
29
|
+
assert compute_branch_name(task, wave) == "wave-a1b2c3"
|
|
30
30
|
|
|
31
31
|
def test_wave_takes_precedence(self):
|
|
32
32
|
task = {"id": "abc12345xyz", "title": "Add auth", "linear_identifier": "PROJ-123"}
|
|
33
|
-
wave = WaveContext(wave_number=2, total_waves=4)
|
|
34
|
-
assert compute_branch_name(task, wave) == "wave
|
|
33
|
+
wave = WaveContext(wave_number=2, total_waves=4, wave_name="wave-f1e2d3")
|
|
34
|
+
assert compute_branch_name(task, wave) == "wave-f1e2d3"
|
|
35
35
|
|
|
36
36
|
def test_title_slugified(self):
|
|
37
37
|
task = {"id": "abc12345xyz", "title": "Add User Authentication & OAuth 2.0"}
|
|
@@ -37,8 +37,8 @@ class TestComputeBranchName:
|
|
|
37
37
|
from steerdev_agent.prompt.builder import WaveContext
|
|
38
38
|
|
|
39
39
|
task = {"id": "abc", "title": "test"}
|
|
40
|
-
wave = WaveContext(wave_number=5, total_waves=10)
|
|
41
|
-
assert compute_branch_name(task, wave) == "wave
|
|
40
|
+
wave = WaveContext(wave_number=5, total_waves=10, wave_name="wave-d4e5f6")
|
|
41
|
+
assert compute_branch_name(task, wave) == "wave-d4e5f6"
|
|
42
42
|
|
|
43
43
|
def test_task_with_linear_id(self):
|
|
44
44
|
task = {"id": "abc12345xyz", "title": "Add auth", "linear_identifier": "PROJ-42"}
|
|
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.0.60 → steerdev-1.1.3}/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
|