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.
- {steerdev-1.1.31 → steerdev-1.1.44}/PKG-INFO +1 -1
- {steerdev-1.1.31 → steerdev-1.1.44}/pyproject.toml +1 -1
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/cli.py +31 -39
- steerdev-1.1.44/src/steerdev_agent/git/pr.py +192 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/runner.py +72 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +17 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/preparation.py +196 -61
- steerdev-1.1.44/tests/test_git_pr.py +210 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_preparation.py +418 -0
- steerdev-1.1.44/tests/test_runner_wave_pr.py +83 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_runner_worktrees.py +8 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/.gitignore +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/AGENTS.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/CLAUDE.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/README.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/git/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/git/commit.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/__init__.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_api_client.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_config.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_context_search.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_executor.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_git_commit.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_prompt.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_retry.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_runner_boundary_commit.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_tasks.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_version.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workspace.py +0 -0
- {steerdev-1.1.31 → steerdev-1.1.44}/tests/test_workspace_extended.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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 +=
|
|
1381
|
+
pr_body += STEERDEV_SIGNATURE
|
|
1378
1382
|
|
|
1379
1383
|
# Prefer the persisted base branch so the PR targets the correct base.
|
|
1380
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|