steerdev 1.1.31__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.31 → steerdev-1.1.43}/PKG-INFO +1 -1
- {steerdev-1.1.31 → steerdev-1.1.43}/pyproject.toml +1 -1
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/cli.py +14 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/runner.py +8 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +17 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workspace/preparation.py +196 -61
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_preparation.py +418 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_runner_worktrees.py +8 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/.gitignore +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/AGENTS.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/CLAUDE.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/README.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/git/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/git/commit.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_api_client.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_config.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_context_search.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_executor.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_git_commit.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_prompt.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_retry.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_runner_boundary_commit.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_tasks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_version.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_workspace.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.43}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.31 → 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
|
|
|
@@ -338,6 +338,7 @@ class Runner:
|
|
|
338
338
|
agent_id: str | None = None,
|
|
339
339
|
task_id: str | None = None,
|
|
340
340
|
wave_id: str | None = None,
|
|
341
|
+
base_branch_override: str | None = None,
|
|
341
342
|
) -> None:
|
|
342
343
|
"""Initialize the runner.
|
|
343
344
|
|
|
@@ -363,6 +364,8 @@ class Runner:
|
|
|
363
364
|
task_id: Run a single specific task (bypasses queue/wave scheduling).
|
|
364
365
|
wave_id: Scope scheduling to a single wave; the loop processes every
|
|
365
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.
|
|
366
369
|
"""
|
|
367
370
|
self.project_id = project_id
|
|
368
371
|
self.working_directory = Path(working_directory or Path.cwd())
|
|
@@ -390,6 +393,7 @@ class Runner:
|
|
|
390
393
|
self._agent_id = agent_id
|
|
391
394
|
self._target_task_id = task_id
|
|
392
395
|
self._target_wave_id = wave_id
|
|
396
|
+
self._base_branch_override = base_branch_override
|
|
393
397
|
|
|
394
398
|
# State
|
|
395
399
|
self._state = RunState.STOPPED
|
|
@@ -836,6 +840,7 @@ class Runner:
|
|
|
836
840
|
working_directory=self.working_directory,
|
|
837
841
|
worktree_config=self._worktree_config,
|
|
838
842
|
branch_config=self._branch_config,
|
|
843
|
+
base_branch_override=self._base_branch_override,
|
|
839
844
|
)
|
|
840
845
|
prep_result = await preparation.prepare(task, wave_context)
|
|
841
846
|
if not prep_result.success:
|
|
@@ -1053,6 +1058,7 @@ class Runner:
|
|
|
1053
1058
|
working_directory=self.working_directory,
|
|
1054
1059
|
worktree_config=self._worktree_config,
|
|
1055
1060
|
branch_config=self._branch_config,
|
|
1061
|
+
base_branch_override=self._base_branch_override,
|
|
1056
1062
|
)
|
|
1057
1063
|
prep_result = await preparation.prepare(task, wave_context)
|
|
1058
1064
|
if not prep_result.success:
|
|
@@ -1510,6 +1516,7 @@ async def run_agent(
|
|
|
1510
1516
|
task_id: str | None = None,
|
|
1511
1517
|
agent_id: str | None = None,
|
|
1512
1518
|
wave_id: str | None = None,
|
|
1519
|
+
base_branch_override: str | None = None,
|
|
1513
1520
|
) -> dict[str, Any]:
|
|
1514
1521
|
"""Run the steerdev agent.
|
|
1515
1522
|
|
|
@@ -1560,6 +1567,7 @@ async def run_agent(
|
|
|
1560
1567
|
task_id=task_id,
|
|
1561
1568
|
agent_id=agent_id,
|
|
1562
1569
|
wave_id=wave_id,
|
|
1570
|
+
base_branch_override=base_branch_override,
|
|
1563
1571
|
)
|
|
1564
1572
|
|
|
1565
1573
|
# Install signal handler that kills the subprocess immediately on Ctrl+C
|
|
@@ -63,6 +63,23 @@ 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
|
|
@@ -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,39 +391,36 @@ 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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
)
|
|
375
|
-
|
|
376
|
-
# 6. Create or checkout the target (wave) branch.
|
|
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.
|
|
377
410
|
rc, _, _ = await _run_git("rev-parse", "--verify", target_branch, cwd=cwd, check=False)
|
|
378
|
-
|
|
411
|
+
branch_exists_local = rc == 0
|
|
412
|
+
|
|
413
|
+
rc, _, _ = await _run_git(
|
|
414
|
+
"rev-parse", "--verify", f"origin/{target_branch}", cwd=cwd, check=False
|
|
415
|
+
)
|
|
416
|
+
branch_exists_origin = rc == 0
|
|
379
417
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
418
|
+
is_continuation = False
|
|
419
|
+
effective_base = default_branch
|
|
420
|
+
|
|
421
|
+
if branch_exists_local:
|
|
422
|
+
# Branch already present locally (same VM, continuing the wave).
|
|
423
|
+
is_continuation = True
|
|
383
424
|
rc, _, _ = await _run_git("checkout", target_branch, cwd=cwd, check=False)
|
|
384
425
|
if rc != 0:
|
|
385
426
|
return PreparationResult(
|
|
@@ -391,11 +432,13 @@ class WorkspacePreparation:
|
|
|
391
432
|
error=f"Failed to checkout existing branch {target_branch}",
|
|
392
433
|
)
|
|
393
434
|
logger.info(f"Switched to existing branch: {target_branch}")
|
|
394
|
-
elif
|
|
395
|
-
#
|
|
396
|
-
#
|
|
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
|
|
397
440
|
rc, _, _ = await _run_git(
|
|
398
|
-
"checkout", "-b", target_branch, f"origin/{
|
|
441
|
+
"checkout", "-b", target_branch, f"origin/{target_branch}", cwd=cwd, check=False
|
|
399
442
|
)
|
|
400
443
|
if rc != 0:
|
|
401
444
|
return PreparationResult(
|
|
@@ -404,36 +447,124 @@ class WorkspacePreparation:
|
|
|
404
447
|
working_directory=cwd,
|
|
405
448
|
default_branch=default_branch,
|
|
406
449
|
is_worktree=False,
|
|
407
|
-
error=f"Failed to
|
|
450
|
+
error=f"Failed to track existing branch origin/{target_branch}",
|
|
408
451
|
)
|
|
409
|
-
logger.info(f"
|
|
452
|
+
logger.info(f"Continuing wave branch from origin/{target_branch}")
|
|
410
453
|
else:
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
error=f"Failed to checkout {default_branch}. Does the branch exist?",
|
|
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
|
|
421
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
|
+
)
|
|
422
495
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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,
|
|
429
506
|
)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
433
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
|
+
)
|
|
434
554
|
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
437
568
|
|
|
438
569
|
# 7. Validate current branch
|
|
439
570
|
_, actual_branch, _ = await _run_git("branch", "--show-current", cwd=cwd, check=False)
|
|
@@ -447,9 +578,13 @@ class WorkspacePreparation:
|
|
|
447
578
|
error=f"Branch mismatch: expected {target_branch}, got {actual_branch}",
|
|
448
579
|
)
|
|
449
580
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
+
)
|
|
453
588
|
|
|
454
589
|
return PreparationResult(
|
|
455
590
|
success=True,
|