steerdev 1.1.28__tar.gz → 1.1.29__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.28 → steerdev-1.1.29}/PKG-INFO +1 -1
- {steerdev-1.1.28 → steerdev-1.1.29}/pyproject.toml +1 -1
- steerdev-1.1.29/src/steerdev_agent/git/__init__.py +1 -0
- steerdev-1.1.29/src/steerdev_agent/git/commit.py +96 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/runner.py +38 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +16 -24
- steerdev-1.1.29/tests/test_git_commit.py +127 -0
- steerdev-1.1.29/tests/test_runner_boundary_commit.py +74 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/.gitignore +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/AGENTS.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/CLAUDE.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/README.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/cli.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/preparation.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_api_client.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_config.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_context_search.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_executor.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_preparation.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_prompt.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_retry.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_runner_worktrees.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_tasks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_version.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workspace.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.29}/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.29
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Git helpers for the steerdev runtime."""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Deterministic per-task boundary commit for wave runs.
|
|
2
|
+
|
|
3
|
+
After each successful task in wave mode, the runner commits everything
|
|
4
|
+
outstanding with a task-scoped message, so the wave branch ends up with one
|
|
5
|
+
clean commit per task regardless of how the agent chose to commit (or didn't).
|
|
6
|
+
|
|
7
|
+
All functions are best-effort and never raise — a commit failure must never
|
|
8
|
+
break the run. Async, mirroring `workspace.preparation._run_git`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from steerdev_agent.git_ops import PROTECTED_BRANCHES
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_task_commit_message(task: dict[str, Any]) -> str:
|
|
25
|
+
"""Build a task-scoped commit message.
|
|
26
|
+
|
|
27
|
+
Prefix precedence mirrors `worktree.compute_branch_name`:
|
|
28
|
+
- external ticket ID (`linear_identifier`, then `external_id`) → `[PROJ-123]`
|
|
29
|
+
- otherwise the first 8 chars of the internal id → `[task:abc12345]`
|
|
30
|
+
|
|
31
|
+
The task title is appended when present.
|
|
32
|
+
"""
|
|
33
|
+
external_id = task.get("linear_identifier") or task.get("external_id")
|
|
34
|
+
if external_id:
|
|
35
|
+
prefix = f"[{external_id}]"
|
|
36
|
+
else:
|
|
37
|
+
task_id = task.get("id", "unknown")
|
|
38
|
+
short = task_id[:8] if len(task_id) > 8 else task_id
|
|
39
|
+
prefix = f"[task:{short}]"
|
|
40
|
+
|
|
41
|
+
title = (task.get("title") or "").strip()
|
|
42
|
+
return f"{prefix} {title}" if title else prefix
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _run_git(cwd: str | Path, *args: str) -> tuple[int, str]:
|
|
46
|
+
"""Run git, returning (returncode, stdout+stderr). Never raises for git errors."""
|
|
47
|
+
proc = await asyncio.create_subprocess_exec(
|
|
48
|
+
"git",
|
|
49
|
+
*args,
|
|
50
|
+
cwd=str(cwd),
|
|
51
|
+
stdout=asyncio.subprocess.PIPE,
|
|
52
|
+
stderr=asyncio.subprocess.PIPE,
|
|
53
|
+
)
|
|
54
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
55
|
+
out = (stdout_bytes.decode("utf-8", errors="replace") or "").strip()
|
|
56
|
+
err = (stderr_bytes.decode("utf-8", errors="replace") or "").strip()
|
|
57
|
+
return proc.returncode or 0, out or err
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def commit_task_boundary(cwd: str | Path, task: dict[str, Any]) -> str:
|
|
61
|
+
"""Stage and commit everything outstanding as one task-scoped commit.
|
|
62
|
+
|
|
63
|
+
Best-effort; never raises. Returns one of:
|
|
64
|
+
"committed", "nothing_to_commit", "skipped_protected", "failed".
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
# Resolve current branch; skip protected branches and detached/non-repo.
|
|
68
|
+
rc, branch = await _run_git(cwd, "rev-parse", "--abbrev-ref", "HEAD")
|
|
69
|
+
if rc != 0 or not branch or branch == "HEAD":
|
|
70
|
+
logger.info("commit_task_boundary: not on a branch, skipping")
|
|
71
|
+
return "failed"
|
|
72
|
+
if branch in PROTECTED_BRANCHES:
|
|
73
|
+
logger.info(f"commit_task_boundary: protected branch {branch}, skipping")
|
|
74
|
+
return "skipped_protected"
|
|
75
|
+
|
|
76
|
+
rc, _ = await _run_git(cwd, "add", "-A")
|
|
77
|
+
if rc != 0:
|
|
78
|
+
logger.warning("commit_task_boundary: git add failed (non-fatal)")
|
|
79
|
+
return "failed"
|
|
80
|
+
|
|
81
|
+
# Anything staged? `diff --cached --quiet` exits 1 when there are changes.
|
|
82
|
+
rc, _ = await _run_git(cwd, "diff", "--cached", "--quiet")
|
|
83
|
+
if rc == 0:
|
|
84
|
+
return "nothing_to_commit"
|
|
85
|
+
|
|
86
|
+
message = build_task_commit_message(task)
|
|
87
|
+
rc, out = await _run_git(cwd, "commit", "-m", message)
|
|
88
|
+
if rc != 0:
|
|
89
|
+
logger.warning(f"commit_task_boundary: commit failed (non-fatal): {out}")
|
|
90
|
+
return "failed"
|
|
91
|
+
|
|
92
|
+
logger.info(f"commit_task_boundary: committed on {branch}: {message}")
|
|
93
|
+
return "committed"
|
|
94
|
+
except Exception as exc: # broad catch intentional: durability must never break the run
|
|
95
|
+
logger.warning(f"commit_task_boundary: unexpected error (non-fatal): {exc}")
|
|
96
|
+
return "failed"
|
|
@@ -29,6 +29,8 @@ from steerdev_agent.config.models import (
|
|
|
29
29
|
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
|
+
from steerdev_agent.git.commit import commit_task_boundary
|
|
33
|
+
from steerdev_agent.git_ops import checkpoint_push
|
|
32
34
|
from steerdev_agent.prompt.builder import (
|
|
33
35
|
ProjectContext,
|
|
34
36
|
PromptBuilder,
|
|
@@ -200,6 +202,27 @@ async def _get_modified_files(working_dir: str | Path) -> list[str]:
|
|
|
200
202
|
return []
|
|
201
203
|
|
|
202
204
|
|
|
205
|
+
async def maybe_commit_task_boundary(
|
|
206
|
+
working_dir: str | Path,
|
|
207
|
+
task: dict[str, Any],
|
|
208
|
+
wave_context: WaveContext | None,
|
|
209
|
+
dry_run: bool,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Commit a wave task's work at its completion boundary, then checkpoint-push.
|
|
212
|
+
|
|
213
|
+
Only fires in wave mode (single-task mode keeps the agent-driven,
|
|
214
|
+
one-PR-per-task flow). Best-effort: `commit_task_boundary` and
|
|
215
|
+
`checkpoint_push` never raise. Pushes only when a commit was actually made,
|
|
216
|
+
so the wave branch lands on origin for the next wave to stack on.
|
|
217
|
+
"""
|
|
218
|
+
if dry_run or wave_context is None:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
result = await commit_task_boundary(working_dir, task)
|
|
222
|
+
if result == "committed":
|
|
223
|
+
checkpoint_push(working_dir)
|
|
224
|
+
|
|
225
|
+
|
|
203
226
|
def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None) -> str:
|
|
204
227
|
"""Compute the git worktree name for a task execution.
|
|
205
228
|
|
|
@@ -924,6 +947,14 @@ class Runner:
|
|
|
924
947
|
except Exception:
|
|
925
948
|
logger.debug("Failed to report modified files", exc_info=True)
|
|
926
949
|
|
|
950
|
+
# Deterministic per-task commit boundary (wave mode only): commit
|
|
951
|
+
# everything outstanding as one task-scoped commit and checkpoint-push,
|
|
952
|
+
# so the wave branch has one clean commit per task and lands on origin
|
|
953
|
+
# for the next wave to stack on. No-op in single-task/dry-run mode.
|
|
954
|
+
await maybe_commit_task_boundary(
|
|
955
|
+
effective_working_dir, task, wave_context, self.dry_run
|
|
956
|
+
)
|
|
957
|
+
|
|
927
958
|
if self._evidence_config.enabled and not self.dry_run:
|
|
928
959
|
await self._submit_evidence_report(
|
|
929
960
|
task=task,
|
|
@@ -1072,6 +1103,13 @@ class Runner:
|
|
|
1072
1103
|
else:
|
|
1073
1104
|
logger.warning(f"Failed to mark task {task_id} as completed")
|
|
1074
1105
|
|
|
1106
|
+
# Deterministic per-task commit boundary (wave mode only; successful
|
|
1107
|
+
# tasks only). See execute_task for rationale.
|
|
1108
|
+
if success:
|
|
1109
|
+
await maybe_commit_task_boundary(
|
|
1110
|
+
effective_working_dir, task, wave_context, self.dry_run
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1075
1113
|
if self._evidence_config.enabled and not self.dry_run:
|
|
1076
1114
|
await self._submit_evidence_report(
|
|
1077
1115
|
task=task,
|
|
@@ -31,15 +31,7 @@ git checkout -b wave/<wave-number>
|
|
|
31
31
|
2. For each task in the wave:
|
|
32
32
|
- Mark the task `started`
|
|
33
33
|
- Implement the requested change
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# With external ticket ID (preferred):
|
|
38
|
-
git commit -m "[PROJ-123] feat: add user endpoint"
|
|
39
|
-
|
|
40
|
-
# Fallback (no external ID):
|
|
41
|
-
git commit -m "[task:abc12345] feat: add user endpoint"
|
|
42
|
-
```
|
|
34
|
+
- **Do not commit.** The steerdev runtime automatically commits each task's work at the task boundary with a task-prefixed message (`[PROJ-123] <title>`, or `[task:abc12345] <title>` when there is no external ID) and pushes the wave branch. You just implement; the runtime owns the per-task commit.
|
|
43
35
|
|
|
44
36
|
3. After the wave work is ready for review, rebase onto the latest default branch:
|
|
45
37
|
|
|
@@ -80,22 +72,25 @@ steerdev git pr --title "Wave <N>: <description>" --body "..."
|
|
|
80
72
|
5. Update all related tasks with the shared PR URL
|
|
81
73
|
6. Log progress and completion in the root `docs/` progress files
|
|
82
74
|
|
|
83
|
-
## Commit
|
|
75
|
+
## Commit Convention (handled by the runtime)
|
|
84
76
|
|
|
85
|
-
|
|
77
|
+
You do **not** commit task work yourself. The steerdev runtime commits each task
|
|
78
|
+
automatically at its completion boundary, using an external-ticket-ID-prefixed
|
|
79
|
+
message so GitHub integrations can link commits to issues:
|
|
86
80
|
|
|
87
81
|
| Has External ID? | Commit Prefix | Example |
|
|
88
82
|
|---|---|---|
|
|
89
|
-
| Yes | `[<TICKET-ID>]` | `[PROJ-123]
|
|
90
|
-
| No | `[task:<short-uuid>]` | `[task:abc12345]
|
|
83
|
+
| Yes | `[<TICKET-ID>]` | `[PROJ-123] <task title>` |
|
|
84
|
+
| No | `[task:<short-uuid>]` | `[task:abc12345] <task title>` |
|
|
91
85
|
|
|
92
|
-
|
|
86
|
+
It also pushes the wave branch after each commit. Your job is to implement and,
|
|
87
|
+
once the wave is done, rebase and open the PR (steps 3–5).
|
|
93
88
|
|
|
94
89
|
## Conventions
|
|
95
90
|
|
|
96
91
|
- Use one branch per wave: `wave/<wave-number>`
|
|
92
|
+
- Do not commit task work — the runtime commits and pushes each task automatically
|
|
97
93
|
- Do not create per-task PRs inside the wave
|
|
98
|
-
- Keep commits traceable by including the external ticket ID (or internal ID fallback) in each commit
|
|
99
94
|
- Include the shared PR URL in every related task result
|
|
100
95
|
- Replace `<default-branch>` in commands above with the actual branch name from the Git Branch Configuration section
|
|
101
96
|
|
|
@@ -105,18 +100,15 @@ Run `steerdev tasks get TASK_ID` to find the task's `linear_identifier` before c
|
|
|
105
100
|
git checkout -b wave/3
|
|
106
101
|
|
|
107
102
|
steerdev tasks update task-aaa... --status started
|
|
108
|
-
#
|
|
109
|
-
# ... implement task 1 ...
|
|
110
|
-
git add .
|
|
111
|
-
git commit -m "[PROJ-123] feat: add user endpoint"
|
|
103
|
+
# ... implement task 1 ... (runtime commits "[PROJ-123] <title>" at the boundary)
|
|
112
104
|
|
|
113
105
|
steerdev tasks update task-bbb... --status started
|
|
114
|
-
#
|
|
115
|
-
# ... implement task 2 ...
|
|
116
|
-
git add .
|
|
117
|
-
git commit -m "[PROJ-124] fix: validate email format"
|
|
106
|
+
# ... implement task 2 ... (runtime commits "[PROJ-124] <title>" at the boundary)
|
|
118
107
|
|
|
119
|
-
|
|
108
|
+
# Wave done: rebase onto the default branch (see step 3), then open one PR.
|
|
109
|
+
git fetch origin <default-branch>
|
|
110
|
+
git rebase origin/<default-branch>
|
|
111
|
+
git push --force-with-lease origin HEAD
|
|
120
112
|
steerdev git pr --title "Wave 3: User management improvements" --body "..."
|
|
121
113
|
steerdev tasks update task-aaa... --result "PR: https://github.com/org/repo/pull/42"
|
|
122
114
|
steerdev tasks update task-bbb... --result "PR: https://github.com/org/repo/pull/42"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Tests for the git/commit module (per-task boundary commit)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from steerdev_agent.git.commit import build_task_commit_message, commit_task_boundary
|
|
8
|
+
|
|
9
|
+
# ── build_task_commit_message ───────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_message_uses_linear_identifier():
|
|
13
|
+
task = {"id": "abc12345xyz", "linear_identifier": "PROJ-123", "title": "Add auth"}
|
|
14
|
+
assert build_task_commit_message(task) == "[PROJ-123] Add auth"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_message_falls_back_to_external_id():
|
|
18
|
+
task = {"id": "abc12345xyz", "external_id": "JIRA-9", "title": "Add auth"}
|
|
19
|
+
assert build_task_commit_message(task) == "[JIRA-9] Add auth"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_message_prefers_linear_over_external():
|
|
23
|
+
task = {
|
|
24
|
+
"id": "abc12345xyz",
|
|
25
|
+
"linear_identifier": "PROJ-123",
|
|
26
|
+
"external_id": "JIRA-9",
|
|
27
|
+
"title": "Add auth",
|
|
28
|
+
}
|
|
29
|
+
assert build_task_commit_message(task) == "[PROJ-123] Add auth"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_message_falls_back_to_short_id():
|
|
33
|
+
task = {"id": "abc12345xyz9999", "title": "Add auth"}
|
|
34
|
+
assert build_task_commit_message(task) == "[task:abc12345] Add auth"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_message_handles_missing_title():
|
|
38
|
+
task = {"id": "abc12345xyz", "linear_identifier": "PROJ-123"}
|
|
39
|
+
assert build_task_commit_message(task) == "[PROJ-123]"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── commit_task_boundary ─────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _git(cwd: Path, *args: str) -> None:
|
|
46
|
+
subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _make_repo(tmp_path: Path) -> Path:
|
|
50
|
+
work = tmp_path / "work"
|
|
51
|
+
work.mkdir()
|
|
52
|
+
_git(work, "init")
|
|
53
|
+
_git(work, "config", "user.email", "t@t.dev")
|
|
54
|
+
_git(work, "config", "user.name", "t")
|
|
55
|
+
(work / "f.txt").write_text("a")
|
|
56
|
+
_git(work, "add", "-A")
|
|
57
|
+
_git(work, "commit", "-m", "init")
|
|
58
|
+
return work
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _head_message(work: Path) -> str:
|
|
62
|
+
out = subprocess.run(
|
|
63
|
+
["git", "log", "-1", "--pretty=%s"],
|
|
64
|
+
cwd=work,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
check=True,
|
|
68
|
+
)
|
|
69
|
+
return out.stdout.strip()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _commit_count(work: Path) -> int:
|
|
73
|
+
out = subprocess.run(
|
|
74
|
+
["git", "rev-list", "--count", "HEAD"],
|
|
75
|
+
cwd=work,
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
check=True,
|
|
79
|
+
)
|
|
80
|
+
return int(out.stdout.strip())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_commits_outstanding_changes(tmp_path):
|
|
84
|
+
work = _make_repo(tmp_path)
|
|
85
|
+
_git(work, "checkout", "-b", "wave-1")
|
|
86
|
+
(work / "f.txt").write_text("changed")
|
|
87
|
+
(work / "new.txt").write_text("new file")
|
|
88
|
+
|
|
89
|
+
task = {"id": "abc12345xyz", "linear_identifier": "PROJ-7", "title": "do work"}
|
|
90
|
+
result = asyncio.run(commit_task_boundary(work, task))
|
|
91
|
+
|
|
92
|
+
assert result == "committed"
|
|
93
|
+
assert _head_message(work) == "[PROJ-7] do work"
|
|
94
|
+
# Both the modified and the untracked file are committed (git add -A).
|
|
95
|
+
assert _commit_count(work) == 2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_nothing_to_commit_when_clean(tmp_path):
|
|
99
|
+
work = _make_repo(tmp_path)
|
|
100
|
+
_git(work, "checkout", "-b", "wave-1")
|
|
101
|
+
|
|
102
|
+
task = {"id": "abc12345xyz", "title": "nothing"}
|
|
103
|
+
result = asyncio.run(commit_task_boundary(work, task))
|
|
104
|
+
|
|
105
|
+
assert result == "nothing_to_commit"
|
|
106
|
+
assert _commit_count(work) == 1
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_skips_protected_branch(tmp_path):
|
|
110
|
+
work = _make_repo(tmp_path) # stays on default branch (main/master)
|
|
111
|
+
(work / "f.txt").write_text("changed on protected")
|
|
112
|
+
|
|
113
|
+
task = {"id": "abc12345xyz", "title": "do work"}
|
|
114
|
+
result = asyncio.run(commit_task_boundary(work, task))
|
|
115
|
+
|
|
116
|
+
assert result == "skipped_protected"
|
|
117
|
+
assert _commit_count(work) == 1 # nothing committed
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_never_raises_on_non_repo(tmp_path):
|
|
121
|
+
not_a_repo = tmp_path / "plain"
|
|
122
|
+
not_a_repo.mkdir()
|
|
123
|
+
|
|
124
|
+
task = {"id": "abc12345xyz", "title": "do work"}
|
|
125
|
+
# Must not raise; returns a status marker.
|
|
126
|
+
result = asyncio.run(commit_task_boundary(not_a_repo, task))
|
|
127
|
+
assert result == "failed"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Tests for the runner's per-task boundary-commit gate."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from steerdev_agent.runner import maybe_commit_task_boundary
|
|
7
|
+
from tests.test_runner_merge_modes import _make_wave_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(coro):
|
|
11
|
+
return asyncio.run(coro)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_commits_and_pushes_in_wave_mode(tmp_path):
|
|
15
|
+
task = {"id": "abc12345xyz", "title": "do work"}
|
|
16
|
+
with (
|
|
17
|
+
patch("steerdev_agent.runner.commit_task_boundary", return_value="committed") as commit,
|
|
18
|
+
patch("steerdev_agent.runner.checkpoint_push", return_value="pushed") as push,
|
|
19
|
+
):
|
|
20
|
+
_run(
|
|
21
|
+
maybe_commit_task_boundary(
|
|
22
|
+
tmp_path, task, wave_context=_make_wave_context(), dry_run=False
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
commit.assert_awaited_once_with(tmp_path, task)
|
|
27
|
+
push.assert_called_once_with(tmp_path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_noop_without_wave_context(tmp_path):
|
|
31
|
+
task = {"id": "abc12345xyz", "title": "do work"}
|
|
32
|
+
with (
|
|
33
|
+
patch("steerdev_agent.runner.commit_task_boundary") as commit,
|
|
34
|
+
patch("steerdev_agent.runner.checkpoint_push") as push,
|
|
35
|
+
):
|
|
36
|
+
_run(maybe_commit_task_boundary(tmp_path, task, wave_context=None, dry_run=False))
|
|
37
|
+
|
|
38
|
+
commit.assert_not_called()
|
|
39
|
+
push.assert_not_called()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_noop_in_dry_run(tmp_path):
|
|
43
|
+
task = {"id": "abc12345xyz", "title": "do work"}
|
|
44
|
+
with (
|
|
45
|
+
patch("steerdev_agent.runner.commit_task_boundary") as commit,
|
|
46
|
+
patch("steerdev_agent.runner.checkpoint_push") as push,
|
|
47
|
+
):
|
|
48
|
+
_run(
|
|
49
|
+
maybe_commit_task_boundary(
|
|
50
|
+
tmp_path, task, wave_context=_make_wave_context(), dry_run=True
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
commit.assert_not_called()
|
|
55
|
+
push.assert_not_called()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_does_not_push_when_nothing_committed(tmp_path):
|
|
59
|
+
task = {"id": "abc12345xyz", "title": "do work"}
|
|
60
|
+
with (
|
|
61
|
+
patch(
|
|
62
|
+
"steerdev_agent.runner.commit_task_boundary", return_value="nothing_to_commit"
|
|
63
|
+
) as commit,
|
|
64
|
+
patch("steerdev_agent.runner.checkpoint_push") as push,
|
|
65
|
+
):
|
|
66
|
+
_run(
|
|
67
|
+
maybe_commit_task_boundary(
|
|
68
|
+
tmp_path, task, wave_context=_make_wave_context(), dry_run=False
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
commit.assert_awaited_once_with(tmp_path, task)
|
|
73
|
+
# No new commit → no point pushing.
|
|
74
|
+
push.assert_not_called()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/ci/canal-integration.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/worktrunk.config.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|