steerdev 1.1.31__tar.gz → 1.1.44__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.
Files changed (132) hide show
  1. {steerdev-1.1.31 → steerdev-1.1.44}/PKG-INFO +1 -1
  2. {steerdev-1.1.31 → steerdev-1.1.44}/pyproject.toml +1 -1
  3. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/cli.py +31 -39
  4. steerdev-1.1.44/src/steerdev_agent/git/pr.py +192 -0
  5. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/runner.py +72 -0
  6. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +17 -0
  7. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/preparation.py +196 -61
  8. steerdev-1.1.44/tests/test_git_pr.py +210 -0
  9. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_preparation.py +418 -0
  10. steerdev-1.1.44/tests/test_runner_wave_pr.py +83 -0
  11. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_runner_worktrees.py +8 -0
  12. {steerdev-1.1.31 → steerdev-1.1.44}/.github/workflows/pre-commit.yml +0 -0
  13. {steerdev-1.1.31 → steerdev-1.1.44}/.github/workflows/publish.yml +0 -0
  14. {steerdev-1.1.31 → steerdev-1.1.44}/.gitignore +0 -0
  15. {steerdev-1.1.31 → steerdev-1.1.44}/.pre-commit-config.yaml +0 -0
  16. {steerdev-1.1.31 → steerdev-1.1.44}/AGENTS.md +0 -0
  17. {steerdev-1.1.31 → steerdev-1.1.44}/CLAUDE.md +0 -0
  18. {steerdev-1.1.31 → steerdev-1.1.44}/README.md +0 -0
  19. {steerdev-1.1.31 → steerdev-1.1.44}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
  20. {steerdev-1.1.31 → steerdev-1.1.44}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
  21. {steerdev-1.1.31 → steerdev-1.1.44}/scripts/pre-commit-version-bump.sh +0 -0
  22. {steerdev-1.1.31 → steerdev-1.1.44}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
  23. {steerdev-1.1.31 → steerdev-1.1.44}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
  24. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/__init__.py +0 -0
  25. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/agent_loop.py +0 -0
  26. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/__init__.py +0 -0
  27. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/activity.py +0 -0
  28. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/agents.py +0 -0
  29. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/canals.py +0 -0
  30. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/client.py +0 -0
  31. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/commands.py +0 -0
  32. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/configs.py +0 -0
  33. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/context.py +0 -0
  34. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/events.py +0 -0
  35. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/hooks.py +0 -0
  36. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/implementation_plan.py +0 -0
  37. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/merger.py +0 -0
  38. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/messages.py +0 -0
  39. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/prd.py +0 -0
  40. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/reports.py +0 -0
  41. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/runs.py +0 -0
  42. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/sessions.py +0 -0
  43. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/specs.py +0 -0
  44. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/tasks.py +0 -0
  45. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/workflow_runs.py +0 -0
  46. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/workflows.py +0 -0
  47. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/__init__.py +0 -0
  48. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/models.py +0 -0
  49. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/platform.py +0 -0
  50. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/settings.py +0 -0
  51. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/evidence.py +0 -0
  52. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/evidence_assets.py +0 -0
  53. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/__init__.py +0 -0
  54. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/base.py +0 -0
  55. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/claude.py +0 -0
  56. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/stream.py +0 -0
  57. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/git/__init__.py +0 -0
  58. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/git/commit.py +0 -0
  59. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/git_ops.py +0 -0
  60. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/handlers/__init__.py +0 -0
  61. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/handlers/prd.py +0 -0
  62. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/integration.py +0 -0
  63. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/__init__.py +0 -0
  64. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/builder.py +0 -0
  65. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/templates.py +0 -0
  66. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  67. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/py.typed +0 -0
  68. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/retry.py +0 -0
  69. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/__init__.py +0 -0
  70. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/claude_setup.py +0 -0
  71. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/repo_setup.py +0 -0
  72. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  73. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  74. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/settings.json +0 -0
  75. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  76. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  77. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  78. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  79. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
  80. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  81. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
  82. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  83. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  84. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
  85. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  86. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/update_check.py +0 -0
  87. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/version.py +0 -0
  88. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/__init__.py +0 -0
  89. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/context.py +0 -0
  90. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/executor.py +0 -0
  91. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/memory.py +0 -0
  92. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/__init__.py +0 -0
  93. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/project_manager.py +0 -0
  94. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  95. {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/worktree.py +0 -0
  96. {steerdev-1.1.31 → steerdev-1.1.44}/tests/__init__.py +0 -0
  97. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_agent_loop.py +0 -0
  98. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_agent_loop_extended.py +0 -0
  99. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_agents_api.py +0 -0
  100. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_api_client.py +0 -0
  101. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_claude_executor.py +0 -0
  102. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_claude_setup.py +0 -0
  103. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_client_methods.py +0 -0
  104. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_commands_api.py +0 -0
  105. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_config.py +0 -0
  106. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_config_extended.py +0 -0
  107. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_conflict_mitigation.py +0 -0
  108. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_context_search.py +0 -0
  109. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_executor.py +0 -0
  110. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_git_commit.py +0 -0
  111. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_git_ops.py +0 -0
  112. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_hooks_post_tool_use.py +0 -0
  113. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_platform_config.py +0 -0
  114. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_preparation_provenance.py +0 -0
  115. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_prompt.py +0 -0
  116. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_reports_client.py +0 -0
  117. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_resolve_max_tasks.py +0 -0
  118. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_retry.py +0 -0
  119. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_runner_boundary_commit.py +0 -0
  120. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_runner_merge_modes.py +0 -0
  121. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_stream_parser.py +0 -0
  122. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_tasks.py +0 -0
  123. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_tasks_update_branch.py +0 -0
  124. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_version.py +0 -0
  125. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_wave_scoped_fetch.py +0 -0
  126. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_context.py +0 -0
  127. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_memory.py +0 -0
  128. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_prompt_template.py +0 -0
  129. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_runs_api.py +0 -0
  130. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workspace.py +0 -0
  131. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workspace_extended.py +0 -0
  132. {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerdev
3
- Version: 1.1.31
3
+ Version: 1.1.44
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerdev"
3
- version = "1.1.31"
3
+ version = "1.1.44"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1365,7 +1365,11 @@ def git_pr(
1365
1365
  steerdev git pr --title "Add auth" --body "Implements JWT auth"
1366
1366
  steerdev git pr --title "Add auth" --task-id abc123...
1367
1367
  """
1368
- import subprocess
1368
+ from steerdev_agent.git.pr import (
1369
+ STEERDEV_SIGNATURE,
1370
+ create_pr,
1371
+ resolve_task_base_branch,
1372
+ )
1369
1373
 
1370
1374
  # Build PR body
1371
1375
  pr_body = body or ""
@@ -1374,49 +1378,23 @@ def git_pr(
1374
1378
  pr_body += task_ref
1375
1379
 
1376
1380
  # Add steerdev signature
1377
- pr_body += "\n\n---\nCreated with [steerdev](https://steerdev.com)"
1381
+ pr_body += STEERDEV_SIGNATURE
1378
1382
 
1379
1383
  # Prefer the persisted base branch so the PR targets the correct base.
1380
- from steerdev_agent.api.tasks import TasksClient
1381
-
1382
- base_branch: str | None = None
1383
- if task_id:
1384
- try:
1385
- with TasksClient() as pr_client:
1386
- if pr_client.check_api_key():
1387
- task = pr_client.get_task(task_id)
1388
- if task:
1389
- base_branch = task.get("base_branch")
1390
- except Exception: # fall back to gh default base
1391
- base_branch = None
1384
+ base_branch = resolve_task_base_branch(task_id)
1392
1385
 
1393
- try:
1394
- # Build the gh pr create command
1395
- cmd = ["gh", "pr", "create", "--title", title, "--body", pr_body]
1396
- if base_branch:
1397
- cmd += ["--base", base_branch]
1398
- if draft:
1399
- cmd.append("--draft")
1400
-
1401
- result = subprocess.run(cmd, capture_output=True, text=True)
1402
-
1403
- if result.returncode != 0:
1404
- console.print(f"[red]Error creating PR: {result.stderr}[/red]")
1405
- raise typer.Exit(1)
1386
+ result = create_pr(title, pr_body, base_branch=base_branch, draft=draft)
1387
+ if not result.created:
1388
+ console.print(f"[red]Error creating PR: {result.error}[/red]")
1389
+ raise typer.Exit(1)
1406
1390
 
1407
- # Extract PR URL from output
1408
- pr_url = result.stdout.strip()
1409
- console.print(
1410
- Panel(
1411
- f"[bold green]Pull request created[/bold green]\n\nTitle: {title}\nURL: {pr_url}",
1412
- title="Success",
1413
- border_style="green",
1414
- )
1391
+ console.print(
1392
+ Panel(
1393
+ f"[bold green]Pull request created[/bold green]\n\nTitle: {title}\nURL: {result.url}",
1394
+ title="Success",
1395
+ border_style="green",
1415
1396
  )
1416
-
1417
- except FileNotFoundError:
1418
- console.print("[red]Error: gh not found. Install GitHub CLI: https://cli.github.com[/red]")
1419
- raise typer.Exit(1) from None
1397
+ )
1420
1398
 
1421
1399
 
1422
1400
  @git_app.command("status")
@@ -1806,6 +1784,16 @@ def run(
1806
1784
  ),
1807
1785
  ),
1808
1786
  ] = None,
1787
+ base_branch: Annotated[
1788
+ str | None,
1789
+ typer.Option(
1790
+ "--base-branch",
1791
+ help=(
1792
+ "Branch to cut a new wave/task branch from (overrides the "
1793
+ "auto-chosen predecessor/default). Falls back to STEERDEV_BASE_BRANCH."
1794
+ ),
1795
+ ),
1796
+ ] = None,
1809
1797
  ) -> None:
1810
1798
  """Run the agent for a project!
1811
1799
 
@@ -1841,6 +1829,9 @@ def run(
1841
1829
  console.print("[red]Error: --project-id required (or set via env/config)[/red]")
1842
1830
  raise typer.Exit(1)
1843
1831
 
1832
+ # Resolve base branch override: CLI > env > None
1833
+ resolved_base_branch = base_branch or os.environ.get("STEERDEV_BASE_BRANCH") or None
1834
+
1844
1835
  # Resolve other options: CLI > config > hardcoded default
1845
1836
  resolved_model = model if model is not None else config.agent.model
1846
1837
  resolved_max_turns = max_turns if max_turns is not None else config.agent.max_turns
@@ -1937,6 +1928,7 @@ def run(
1937
1928
  task_id=task_id,
1938
1929
  agent_id=agent_id,
1939
1930
  wave_id=wave_id,
1931
+ base_branch_override=resolved_base_branch,
1940
1932
  )
1941
1933
  )
1942
1934
 
@@ -0,0 +1,192 @@
1
+ """Pull-request helpers shared by the CLI and the runner's wave PR safety net.
2
+
3
+ The wave-tasks-merge skill instructs the agent to open a PR at wave end, but
4
+ nothing guarantees the agent does. `ensure_wave_pr` is the deterministic
5
+ fallback: at wave completion the runner checks whether a PR already exists for
6
+ the wave branch and creates one if not.
7
+
8
+ All functions are best-effort and never raise — a PR failure must never break
9
+ the run. Synchronous, mirroring `git_ops` (these run once at wave end, outside
10
+ the hot path).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import shutil
17
+ import subprocess
18
+ from dataclasses import dataclass
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from loguru import logger
22
+
23
+ from steerdev_agent.git_ops import current_branch
24
+
25
+ if TYPE_CHECKING:
26
+ from pathlib import Path
27
+
28
+ from steerdev_agent.prompt.builder import WaveContext
29
+
30
+ _GH_TIMEOUT = 60 # seconds; gh talks to the GitHub API
31
+
32
+ STEERDEV_SIGNATURE = "\n\n---\nCreated with [steerdev](https://steerdev.com)"
33
+
34
+
35
+ @dataclass
36
+ class PrResult:
37
+ """Outcome of a `gh pr create` invocation."""
38
+
39
+ created: bool
40
+ url: str | None = None
41
+ error: str | None = None
42
+
43
+
44
+ def _run_gh(cwd: str | Path | None, *args: str) -> tuple[int, str, str]:
45
+ """Run gh, returning (returncode, stdout, stderr). Raises only on missing gh/timeout."""
46
+ proc = subprocess.run(
47
+ ["gh", *args],
48
+ cwd=str(cwd) if cwd else None,
49
+ capture_output=True,
50
+ text=True,
51
+ timeout=_GH_TIMEOUT,
52
+ )
53
+ return proc.returncode, (proc.stdout or "").strip(), (proc.stderr or "").strip()
54
+
55
+
56
+ def find_existing_pr(cwd: str | Path | None, branch: str) -> str:
57
+ """Check whether a PR (any state) already exists for *branch*.
58
+
59
+ Returns one of:
60
+ "exists", "none", "unknown" (gh missing or the lookup failed).
61
+ """
62
+ try:
63
+ if shutil.which("gh") is None:
64
+ logger.warning("find_existing_pr: gh not found")
65
+ return "unknown"
66
+ rc, out, err = _run_gh(
67
+ cwd, "pr", "list", "--head", branch, "--state", "all", "--json", "number,state"
68
+ )
69
+ if rc != 0:
70
+ logger.warning(f"find_existing_pr: gh pr list failed (non-fatal): {err}")
71
+ return "unknown"
72
+ return "exists" if json.loads(out or "[]") else "none"
73
+ except Exception as exc: # broad catch intentional: durability must never break the run
74
+ logger.warning(f"find_existing_pr: unexpected error (non-fatal): {exc}")
75
+ return "unknown"
76
+
77
+
78
+ def create_pr(
79
+ title: str,
80
+ body: str,
81
+ *,
82
+ cwd: str | Path | None = None,
83
+ base_branch: str | None = None,
84
+ draft: bool = False,
85
+ ) -> PrResult:
86
+ """Create a PR for the current branch via `gh pr create`. Never raises."""
87
+ try:
88
+ cmd = ["pr", "create", "--title", title, "--body", body]
89
+ if base_branch:
90
+ cmd += ["--base", base_branch]
91
+ if draft:
92
+ cmd.append("--draft")
93
+
94
+ rc, out, err = _run_gh(cwd, *cmd)
95
+ if rc != 0:
96
+ return PrResult(created=False, error=err or out)
97
+ return PrResult(created=True, url=out)
98
+ except FileNotFoundError:
99
+ return PrResult(
100
+ created=False, error="gh not found. Install GitHub CLI: https://cli.github.com"
101
+ )
102
+ except Exception as exc: # broad catch intentional: durability must never break the run
103
+ return PrResult(created=False, error=str(exc))
104
+
105
+
106
+ def resolve_task_base_branch(task_id: str | None, api_key: str | None = None) -> str | None:
107
+ """Resolve the persisted PR base from a task's `base_branch`, if available.
108
+
109
+ For a stacked wave this is the predecessor wave's branch. Falls back to
110
+ None (gh's default base) on any failure.
111
+ """
112
+ if not task_id:
113
+ return None
114
+ try:
115
+ from steerdev_agent.api.tasks import TasksClient
116
+
117
+ with TasksClient(api_key=api_key) as client:
118
+ if not client.check_api_key():
119
+ return None
120
+ task = client.get_task(task_id)
121
+ return task.get("base_branch") if task else None
122
+ except Exception: # fall back to gh default base
123
+ logger.debug("resolve_task_base_branch: lookup failed", exc_info=True)
124
+ return None
125
+
126
+
127
+ def build_wave_pr_title(wave_context: WaveContext) -> str:
128
+ """Build the fallback PR title for a completed wave."""
129
+ name = (wave_context.wave_name or "").strip()
130
+ if name and name != f"wave-{wave_context.wave_number}":
131
+ return f"Wave {wave_context.wave_number}: {name}"
132
+ return f"Wave {wave_context.wave_number}"
133
+
134
+
135
+ def build_wave_pr_body(completed_tasks: list[dict[str, Any]]) -> str:
136
+ """Build the fallback PR body: one bullet per completed task, plus signature."""
137
+ lines = ["## Completed tasks", ""]
138
+ for task in completed_tasks:
139
+ task_id = task.get("id", "unknown")
140
+ short = task_id[:8] if len(task_id) > 8 else task_id
141
+ title = (task.get("title") or "").strip() or "(untitled)"
142
+ lines.append(f"- `{short}` {title}")
143
+ if not completed_tasks:
144
+ lines.append("- (no task summary available)")
145
+ return "\n".join(lines) + STEERDEV_SIGNATURE
146
+
147
+
148
+ def ensure_wave_pr(
149
+ cwd: str | Path,
150
+ wave_context: WaveContext,
151
+ completed_tasks: list[dict[str, Any]],
152
+ api_key: str | None = None,
153
+ ) -> str:
154
+ """Create the wave PR if the agent didn't. Best-effort; never raises.
155
+
156
+ Returns one of:
157
+ "created", "already_exists", "skipped_detached", "failed".
158
+ """
159
+ try:
160
+ branch = current_branch(cwd)
161
+ if branch is None:
162
+ logger.info("ensure_wave_pr: detached HEAD, skipping")
163
+ return "skipped_detached"
164
+
165
+ existing = find_existing_pr(cwd, branch)
166
+ if existing == "exists":
167
+ logger.info(f"ensure_wave_pr: PR already exists for {branch}, skipping")
168
+ return "already_exists"
169
+ if existing == "unknown":
170
+ logger.warning("ensure_wave_pr: could not check for existing PR, skipping")
171
+ return "failed"
172
+
173
+ last_task = completed_tasks[-1] if completed_tasks else None
174
+ base_branch = resolve_task_base_branch(
175
+ last_task.get("id") if last_task else None, api_key=api_key
176
+ )
177
+
178
+ result = create_pr(
179
+ build_wave_pr_title(wave_context),
180
+ build_wave_pr_body(completed_tasks),
181
+ cwd=cwd,
182
+ base_branch=base_branch,
183
+ )
184
+ if not result.created:
185
+ logger.warning(f"ensure_wave_pr: PR creation failed (non-fatal): {result.error}")
186
+ return "failed"
187
+
188
+ logger.info(f"ensure_wave_pr: created PR for {branch}: {result.url}")
189
+ return "created"
190
+ except Exception as exc: # broad catch intentional: durability must never break the run
191
+ logger.warning(f"ensure_wave_pr: unexpected error (non-fatal): {exc}")
192
+ return "failed"
@@ -30,6 +30,7 @@ from steerdev_agent.executor import ExecutorFactory
30
30
  from steerdev_agent.executor.base import AgentExecutor, EventType, StreamEvent
31
31
  from steerdev_agent.executor.claude import ClaudeExecutorError
32
32
  from steerdev_agent.git.commit import commit_task_boundary
33
+ from steerdev_agent.git.pr import ensure_wave_pr
33
34
  from steerdev_agent.git_ops import checkpoint_push
34
35
  from steerdev_agent.prompt.builder import (
35
36
  ProjectContext,
@@ -248,6 +249,11 @@ def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None)
248
249
  return f"task-{task_short}"
249
250
 
250
251
 
252
+ def wave_identity(wave_context: WaveContext) -> str:
253
+ """Stable identity for a wave; matches the wave branch/worktree naming."""
254
+ return wave_context.wave_name or f"wave-{wave_context.wave_number}"
255
+
256
+
251
257
  def build_workflow_task_context(
252
258
  task: dict[str, Any],
253
259
  wave_context: WaveContext | None,
@@ -338,6 +344,7 @@ class Runner:
338
344
  agent_id: str | None = None,
339
345
  task_id: str | None = None,
340
346
  wave_id: str | None = None,
347
+ base_branch_override: str | None = None,
341
348
  ) -> None:
342
349
  """Initialize the runner.
343
350
 
@@ -363,6 +370,8 @@ class Runner:
363
370
  task_id: Run a single specific task (bypasses queue/wave scheduling).
364
371
  wave_id: Scope scheduling to a single wave; the loop processes every
365
372
  task of that wave and stops when it is exhausted.
373
+ base_branch_override: Override the base branch used when preparing
374
+ each task's workspace.
366
375
  """
367
376
  self.project_id = project_id
368
377
  self.working_directory = Path(working_directory or Path.cwd())
@@ -390,6 +399,7 @@ class Runner:
390
399
  self._agent_id = agent_id
391
400
  self._target_task_id = task_id
392
401
  self._target_wave_id = wave_id
402
+ self._base_branch_override = base_branch_override
393
403
 
394
404
  # State
395
405
  self._state = RunState.STOPPED
@@ -405,6 +415,11 @@ class Runner:
405
415
  self._tasks_failed = 0
406
416
  self._events_sent = 0
407
417
 
418
+ # Wave PR safety-net state (see _maybe_create_wave_pr)
419
+ self._last_wave_ctx: WaveContext | None = None
420
+ self._wave_completed_tasks: list[dict[str, Any]] = []
421
+ self._wave_working_dir: str | None = None
422
+
408
423
  # Components
409
424
  self._sessions_client: SessionsClient | None = None
410
425
  self._events_client: EventsClient | None = None
@@ -836,6 +851,7 @@ class Runner:
836
851
  working_directory=self.working_directory,
837
852
  worktree_config=self._worktree_config,
838
853
  branch_config=self._branch_config,
854
+ base_branch_override=self._base_branch_override,
839
855
  )
840
856
  prep_result = await preparation.prepare(task, wave_context)
841
857
  if not prep_result.success:
@@ -978,6 +994,7 @@ class Runner:
978
994
  "exit_code": exit_code,
979
995
  "agent_session_id": agent_session_id,
980
996
  "events_sent": self._events_sent,
997
+ "working_dir": str(effective_working_dir),
981
998
  }
982
999
 
983
1000
  except (KeyboardInterrupt, asyncio.CancelledError):
@@ -1053,6 +1070,7 @@ class Runner:
1053
1070
  working_directory=self.working_directory,
1054
1071
  worktree_config=self._worktree_config,
1055
1072
  branch_config=self._branch_config,
1073
+ base_branch_override=self._base_branch_override,
1056
1074
  )
1057
1075
  prep_result = await preparation.prepare(task, wave_context)
1058
1076
  if not prep_result.success:
@@ -1134,6 +1152,7 @@ class Runner:
1134
1152
  "phases_completed": phases_completed,
1135
1153
  "phases_failed": phases_failed,
1136
1154
  "total_phases": total_phases,
1155
+ "working_dir": str(effective_working_dir),
1137
1156
  }
1138
1157
 
1139
1158
  except Exception as e:
@@ -1261,8 +1280,20 @@ class Runner:
1261
1280
  console.print("[yellow]No tasks available[/yellow]")
1262
1281
  else:
1263
1282
  console.print("[yellow]No more tasks available[/yellow]")
1283
+ # No more wave tasks → the wave is done; make sure its PR exists.
1284
+ self._maybe_create_wave_pr()
1264
1285
  break
1265
1286
 
1287
+ # Moving on to a different wave means the previous one
1288
+ # completed — flush its PR safety net before tracking the new
1289
+ # wave.
1290
+ if (
1291
+ wave_ctx is not None
1292
+ and self._last_wave_ctx is not None
1293
+ and wave_identity(wave_ctx) != wave_identity(self._last_wave_ctx)
1294
+ ):
1295
+ self._maybe_create_wave_pr()
1296
+
1266
1297
  # Resolve workflow: force override > API suggestion > none
1267
1298
  per_task_workflow_id = self.workflow_id or suggested_workflow_id
1268
1299
 
@@ -1290,6 +1321,14 @@ class Runner:
1290
1321
  if result.get("success"):
1291
1322
  self._tasks_succeeded += 1
1292
1323
  console.print("[green]Task completed successfully[/green]")
1324
+ if wave_ctx is not None:
1325
+ # Track wave progress for the PR safety net.
1326
+ self._last_wave_ctx = wave_ctx
1327
+ self._wave_completed_tasks.append(
1328
+ {"id": task.get("id"), "title": task.get("title")}
1329
+ )
1330
+ if result.get("working_dir"):
1331
+ self._wave_working_dir = result["working_dir"]
1293
1332
  else:
1294
1333
  self._tasks_failed += 1
1295
1334
  console.print(f"[red]Task failed: {result.get('error', 'Unknown')}[/red]")
@@ -1382,6 +1421,37 @@ class Runner:
1382
1421
  "error": str(run_error) if run_error else None,
1383
1422
  }
1384
1423
 
1424
+ def _maybe_create_wave_pr(self) -> None:
1425
+ """Wave PR safety net: create the wave PR if the agent didn't.
1426
+
1427
+ Fires when a wave finishes — either no more wave tasks come back, or
1428
+ the run moves on to a different wave. The wave-tasks-merge skill
1429
+ normally has the agent create a richer PR; this only acts when no PR
1430
+ exists for the wave branch yet. Best-effort: never raises, never fails
1431
+ the run. Always resets the tracked wave state.
1432
+ """
1433
+ wave_ctx = self._last_wave_ctx
1434
+ try:
1435
+ if wave_ctx is None or self.dry_run:
1436
+ return
1437
+ working_dir = self._wave_working_dir or str(self.working_directory)
1438
+ status = ensure_wave_pr(
1439
+ working_dir,
1440
+ wave_ctx,
1441
+ self._wave_completed_tasks,
1442
+ api_key=self._api_key,
1443
+ )
1444
+ if status == "created":
1445
+ console.print("[green]Wave PR created (safety net)[/green]")
1446
+ else:
1447
+ logger.info(f"Wave PR safety net: {status}")
1448
+ except Exception: # broad catch intentional: safety net must never break the run
1449
+ logger.warning("Wave PR safety net failed (non-fatal)", exc_info=True)
1450
+ finally:
1451
+ self._last_wave_ctx = None
1452
+ self._wave_completed_tasks = []
1453
+ self._wave_working_dir = None
1454
+
1385
1455
  async def resume(
1386
1456
  self,
1387
1457
  session_id: str,
@@ -1510,6 +1580,7 @@ async def run_agent(
1510
1580
  task_id: str | None = None,
1511
1581
  agent_id: str | None = None,
1512
1582
  wave_id: str | None = None,
1583
+ base_branch_override: str | None = None,
1513
1584
  ) -> dict[str, Any]:
1514
1585
  """Run the steerdev agent.
1515
1586
 
@@ -1560,6 +1631,7 @@ async def run_agent(
1560
1631
  task_id=task_id,
1561
1632
  agent_id=agent_id,
1562
1633
  wave_id=wave_id,
1634
+ base_branch_override=base_branch_override,
1563
1635
  )
1564
1636
 
1565
1637
  # 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