steerdev 1.1.43__tar.gz → 1.1.45__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.43 → steerdev-1.1.45}/PKG-INFO +1 -1
- {steerdev-1.1.43 → steerdev-1.1.45}/pyproject.toml +1 -1
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/cli.py +42 -40
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/executor/__init__.py +9 -1
- steerdev-1.1.45/src/steerdev_agent/git/pr.py +192 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/runner.py +88 -1
- steerdev-1.1.45/tests/test_git_pr.py +210 -0
- steerdev-1.1.45/tests/test_resolve_executor_config.py +85 -0
- steerdev-1.1.45/tests/test_runner_wave_pr.py +83 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/.gitignore +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/AGENTS.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/CLAUDE.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/README.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/git/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/git/commit.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workspace/preparation.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/__init__.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_api_client.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_config.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_context_search.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_executor.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_git_commit.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_preparation.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_prompt.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_retry.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_runner_boundary_commit.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_runner_worktrees.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_tasks.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_version.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_workspace.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.43 → steerdev-1.1.45}/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.45
|
|
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
|
|
1392
|
-
|
|
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)
|
|
1384
|
+
base_branch = resolve_task_base_branch(task_id)
|
|
1402
1385
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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")
|
|
@@ -1676,6 +1654,18 @@ def run(
|
|
|
1676
1654
|
envvar="STEERDEV_AGENT_ID",
|
|
1677
1655
|
),
|
|
1678
1656
|
] = None,
|
|
1657
|
+
provider: Annotated[
|
|
1658
|
+
str | None,
|
|
1659
|
+
typer.Option(
|
|
1660
|
+
"--provider",
|
|
1661
|
+
help=(
|
|
1662
|
+
"AI provider / executor type to run tasks with (e.g. claude)."
|
|
1663
|
+
" Overrides steerdev.yaml executor.type."
|
|
1664
|
+
" Reads from STEERDEV_PROVIDER env var."
|
|
1665
|
+
),
|
|
1666
|
+
envvar="STEERDEV_PROVIDER",
|
|
1667
|
+
),
|
|
1668
|
+
] = None,
|
|
1679
1669
|
model: Annotated[
|
|
1680
1670
|
str | None,
|
|
1681
1671
|
typer.Option(
|
|
@@ -1837,7 +1827,11 @@ def run(
|
|
|
1837
1827
|
steerdev run --project-id abc123 --workflow-id wf-abc123
|
|
1838
1828
|
steerdev run --config custom.yaml --project-id abc123
|
|
1839
1829
|
"""
|
|
1840
|
-
from steerdev_agent.runner import
|
|
1830
|
+
from steerdev_agent.runner import (
|
|
1831
|
+
resolve_executor_config,
|
|
1832
|
+
resolve_max_tasks,
|
|
1833
|
+
run_agent,
|
|
1834
|
+
)
|
|
1841
1835
|
|
|
1842
1836
|
# Get config from context (loaded in app callback)
|
|
1843
1837
|
config: SteerDevConfig = ctx.obj or SteerDevConfig()
|
|
@@ -1866,6 +1860,12 @@ def run(
|
|
|
1866
1860
|
resolved_evidence_config = config.evidence.model_copy()
|
|
1867
1861
|
if evidence is not None:
|
|
1868
1862
|
resolved_evidence_config.enabled = evidence
|
|
1863
|
+
# Resolve executor config: --provider/STEERDEV_PROVIDER overrides executor.type
|
|
1864
|
+
try:
|
|
1865
|
+
resolved_executor_config = resolve_executor_config(config.executor, provider)
|
|
1866
|
+
except ValueError as e:
|
|
1867
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
1868
|
+
raise typer.Exit(1) from e
|
|
1869
1869
|
|
|
1870
1870
|
# API key: CLI > env (via envvar) > config env var
|
|
1871
1871
|
resolved_api_key = api_key
|
|
@@ -1897,6 +1897,7 @@ def run(
|
|
|
1897
1897
|
f"Project ID: {resolved_project_id}\n"
|
|
1898
1898
|
f"Agent Name: {agent_name_display}\n"
|
|
1899
1899
|
f"Working Directory: {working_dir or 'current'}\n"
|
|
1900
|
+
f"Provider: {resolved_executor_config.type}\n"
|
|
1900
1901
|
f"Model: {resolved_model or 'default'}\n"
|
|
1901
1902
|
f"Max Tasks: {max_tasks_display}\n"
|
|
1902
1903
|
f"Timeout: {resolved_timeout}s\n"
|
|
@@ -1943,6 +1944,7 @@ def run(
|
|
|
1943
1944
|
enable_canals=canals,
|
|
1944
1945
|
worktree_config=resolved_worktree_config,
|
|
1945
1946
|
evidence_config=resolved_evidence_config,
|
|
1947
|
+
executor_config=resolved_executor_config,
|
|
1946
1948
|
force_workflow_id=resolved_workflow_id,
|
|
1947
1949
|
dry_run=dry_run,
|
|
1948
1950
|
retry_config=retry_config,
|
|
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from steerdev_agent.config.models import ExecutorConfig
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
|
+
"SUPPORTED_EXECUTOR_TYPES",
|
|
15
16
|
"AgentExecutor",
|
|
16
17
|
"ClaudeExecutor",
|
|
17
18
|
"ExecutorFactory",
|
|
@@ -19,6 +20,10 @@ __all__ = [
|
|
|
19
20
|
"StreamParser",
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
# Executor types ExecutorFactory can build. `steerdev run --provider` and
|
|
24
|
+
# steerdev.yaml's executor.type are validated against this.
|
|
25
|
+
SUPPORTED_EXECUTOR_TYPES: tuple[str, ...] = ("claude",)
|
|
26
|
+
|
|
22
27
|
|
|
23
28
|
class ExecutorFactory:
|
|
24
29
|
"""Factory for creating agent executors based on configuration."""
|
|
@@ -62,4 +67,7 @@ class ExecutorFactory:
|
|
|
62
67
|
extra_args=config.args or None,
|
|
63
68
|
)
|
|
64
69
|
|
|
65
|
-
raise ValueError(
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Unsupported executor type: {config.type} "
|
|
72
|
+
f"(supported: {', '.join(SUPPORTED_EXECUTOR_TYPES)})"
|
|
73
|
+
)
|
|
@@ -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"
|
|
@@ -26,10 +26,11 @@ from steerdev_agent.config.models import (
|
|
|
26
26
|
RetryConfig,
|
|
27
27
|
WorktreeConfig,
|
|
28
28
|
)
|
|
29
|
-
from steerdev_agent.executor import ExecutorFactory
|
|
29
|
+
from steerdev_agent.executor import SUPPORTED_EXECUTOR_TYPES, 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,
|
|
@@ -164,6 +165,29 @@ def resolve_max_tasks(max_tasks: int | None, wave_id: str | None) -> int:
|
|
|
164
165
|
return 0 if wave_id else 1
|
|
165
166
|
|
|
166
167
|
|
|
168
|
+
def resolve_executor_config(
|
|
169
|
+
base: ExecutorConfig,
|
|
170
|
+
provider: str | None,
|
|
171
|
+
) -> ExecutorConfig:
|
|
172
|
+
"""Resolve the effective executor config for a run.
|
|
173
|
+
|
|
174
|
+
- `provider` (from --provider / STEERDEV_PROVIDER) overrides the config
|
|
175
|
+
file's executor.type; None keeps it.
|
|
176
|
+
- The resolved type is validated against SUPPORTED_EXECUTOR_TYPES so an
|
|
177
|
+
unsupported provider fails fast (before task claiming), not deep inside
|
|
178
|
+
ExecutorFactory mid-run.
|
|
179
|
+
"""
|
|
180
|
+
resolved = base.model_copy()
|
|
181
|
+
if provider is not None:
|
|
182
|
+
resolved.type = provider
|
|
183
|
+
if resolved.type not in SUPPORTED_EXECUTOR_TYPES:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Unsupported provider: {resolved.type} "
|
|
186
|
+
f"(supported: {', '.join(SUPPORTED_EXECUTOR_TYPES)})"
|
|
187
|
+
)
|
|
188
|
+
return resolved
|
|
189
|
+
|
|
190
|
+
|
|
167
191
|
def resolve_merge_flow(
|
|
168
192
|
waves_enabled: bool,
|
|
169
193
|
canals_enabled: bool,
|
|
@@ -248,6 +272,11 @@ def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None)
|
|
|
248
272
|
return f"task-{task_short}"
|
|
249
273
|
|
|
250
274
|
|
|
275
|
+
def wave_identity(wave_context: WaveContext) -> str:
|
|
276
|
+
"""Stable identity for a wave; matches the wave branch/worktree naming."""
|
|
277
|
+
return wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
278
|
+
|
|
279
|
+
|
|
251
280
|
def build_workflow_task_context(
|
|
252
281
|
task: dict[str, Any],
|
|
253
282
|
wave_context: WaveContext | None,
|
|
@@ -409,6 +438,11 @@ class Runner:
|
|
|
409
438
|
self._tasks_failed = 0
|
|
410
439
|
self._events_sent = 0
|
|
411
440
|
|
|
441
|
+
# Wave PR safety-net state (see _maybe_create_wave_pr)
|
|
442
|
+
self._last_wave_ctx: WaveContext | None = None
|
|
443
|
+
self._wave_completed_tasks: list[dict[str, Any]] = []
|
|
444
|
+
self._wave_working_dir: str | None = None
|
|
445
|
+
|
|
412
446
|
# Components
|
|
413
447
|
self._sessions_client: SessionsClient | None = None
|
|
414
448
|
self._events_client: EventsClient | None = None
|
|
@@ -983,6 +1017,7 @@ class Runner:
|
|
|
983
1017
|
"exit_code": exit_code,
|
|
984
1018
|
"agent_session_id": agent_session_id,
|
|
985
1019
|
"events_sent": self._events_sent,
|
|
1020
|
+
"working_dir": str(effective_working_dir),
|
|
986
1021
|
}
|
|
987
1022
|
|
|
988
1023
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
@@ -1140,6 +1175,7 @@ class Runner:
|
|
|
1140
1175
|
"phases_completed": phases_completed,
|
|
1141
1176
|
"phases_failed": phases_failed,
|
|
1142
1177
|
"total_phases": total_phases,
|
|
1178
|
+
"working_dir": str(effective_working_dir),
|
|
1143
1179
|
}
|
|
1144
1180
|
|
|
1145
1181
|
except Exception as e:
|
|
@@ -1267,8 +1303,20 @@ class Runner:
|
|
|
1267
1303
|
console.print("[yellow]No tasks available[/yellow]")
|
|
1268
1304
|
else:
|
|
1269
1305
|
console.print("[yellow]No more tasks available[/yellow]")
|
|
1306
|
+
# No more wave tasks → the wave is done; make sure its PR exists.
|
|
1307
|
+
self._maybe_create_wave_pr()
|
|
1270
1308
|
break
|
|
1271
1309
|
|
|
1310
|
+
# Moving on to a different wave means the previous one
|
|
1311
|
+
# completed — flush its PR safety net before tracking the new
|
|
1312
|
+
# wave.
|
|
1313
|
+
if (
|
|
1314
|
+
wave_ctx is not None
|
|
1315
|
+
and self._last_wave_ctx is not None
|
|
1316
|
+
and wave_identity(wave_ctx) != wave_identity(self._last_wave_ctx)
|
|
1317
|
+
):
|
|
1318
|
+
self._maybe_create_wave_pr()
|
|
1319
|
+
|
|
1272
1320
|
# Resolve workflow: force override > API suggestion > none
|
|
1273
1321
|
per_task_workflow_id = self.workflow_id or suggested_workflow_id
|
|
1274
1322
|
|
|
@@ -1296,6 +1344,14 @@ class Runner:
|
|
|
1296
1344
|
if result.get("success"):
|
|
1297
1345
|
self._tasks_succeeded += 1
|
|
1298
1346
|
console.print("[green]Task completed successfully[/green]")
|
|
1347
|
+
if wave_ctx is not None:
|
|
1348
|
+
# Track wave progress for the PR safety net.
|
|
1349
|
+
self._last_wave_ctx = wave_ctx
|
|
1350
|
+
self._wave_completed_tasks.append(
|
|
1351
|
+
{"id": task.get("id"), "title": task.get("title")}
|
|
1352
|
+
)
|
|
1353
|
+
if result.get("working_dir"):
|
|
1354
|
+
self._wave_working_dir = result["working_dir"]
|
|
1299
1355
|
else:
|
|
1300
1356
|
self._tasks_failed += 1
|
|
1301
1357
|
console.print(f"[red]Task failed: {result.get('error', 'Unknown')}[/red]")
|
|
@@ -1388,6 +1444,37 @@ class Runner:
|
|
|
1388
1444
|
"error": str(run_error) if run_error else None,
|
|
1389
1445
|
}
|
|
1390
1446
|
|
|
1447
|
+
def _maybe_create_wave_pr(self) -> None:
|
|
1448
|
+
"""Wave PR safety net: create the wave PR if the agent didn't.
|
|
1449
|
+
|
|
1450
|
+
Fires when a wave finishes — either no more wave tasks come back, or
|
|
1451
|
+
the run moves on to a different wave. The wave-tasks-merge skill
|
|
1452
|
+
normally has the agent create a richer PR; this only acts when no PR
|
|
1453
|
+
exists for the wave branch yet. Best-effort: never raises, never fails
|
|
1454
|
+
the run. Always resets the tracked wave state.
|
|
1455
|
+
"""
|
|
1456
|
+
wave_ctx = self._last_wave_ctx
|
|
1457
|
+
try:
|
|
1458
|
+
if wave_ctx is None or self.dry_run:
|
|
1459
|
+
return
|
|
1460
|
+
working_dir = self._wave_working_dir or str(self.working_directory)
|
|
1461
|
+
status = ensure_wave_pr(
|
|
1462
|
+
working_dir,
|
|
1463
|
+
wave_ctx,
|
|
1464
|
+
self._wave_completed_tasks,
|
|
1465
|
+
api_key=self._api_key,
|
|
1466
|
+
)
|
|
1467
|
+
if status == "created":
|
|
1468
|
+
console.print("[green]Wave PR created (safety net)[/green]")
|
|
1469
|
+
else:
|
|
1470
|
+
logger.info(f"Wave PR safety net: {status}")
|
|
1471
|
+
except Exception: # broad catch intentional: safety net must never break the run
|
|
1472
|
+
logger.warning("Wave PR safety net failed (non-fatal)", exc_info=True)
|
|
1473
|
+
finally:
|
|
1474
|
+
self._last_wave_ctx = None
|
|
1475
|
+
self._wave_completed_tasks = []
|
|
1476
|
+
self._wave_working_dir = None
|
|
1477
|
+
|
|
1391
1478
|
async def resume(
|
|
1392
1479
|
self,
|
|
1393
1480
|
session_id: str,
|