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.
Files changed (129) hide show
  1. {steerdev-1.1.28 → steerdev-1.1.29}/PKG-INFO +1 -1
  2. {steerdev-1.1.28 → steerdev-1.1.29}/pyproject.toml +1 -1
  3. steerdev-1.1.29/src/steerdev_agent/git/__init__.py +1 -0
  4. steerdev-1.1.29/src/steerdev_agent/git/commit.py +96 -0
  5. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/runner.py +38 -0
  6. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +16 -24
  7. steerdev-1.1.29/tests/test_git_commit.py +127 -0
  8. steerdev-1.1.29/tests/test_runner_boundary_commit.py +74 -0
  9. {steerdev-1.1.28 → steerdev-1.1.29}/.github/workflows/pre-commit.yml +0 -0
  10. {steerdev-1.1.28 → steerdev-1.1.29}/.github/workflows/publish.yml +0 -0
  11. {steerdev-1.1.28 → steerdev-1.1.29}/.gitignore +0 -0
  12. {steerdev-1.1.28 → steerdev-1.1.29}/.pre-commit-config.yaml +0 -0
  13. {steerdev-1.1.28 → steerdev-1.1.29}/AGENTS.md +0 -0
  14. {steerdev-1.1.28 → steerdev-1.1.29}/CLAUDE.md +0 -0
  15. {steerdev-1.1.28 → steerdev-1.1.29}/README.md +0 -0
  16. {steerdev-1.1.28 → steerdev-1.1.29}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
  17. {steerdev-1.1.28 → steerdev-1.1.29}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
  18. {steerdev-1.1.28 → steerdev-1.1.29}/scripts/pre-commit-version-bump.sh +0 -0
  19. {steerdev-1.1.28 → steerdev-1.1.29}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
  20. {steerdev-1.1.28 → steerdev-1.1.29}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
  21. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/__init__.py +0 -0
  22. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/agent_loop.py +0 -0
  23. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/__init__.py +0 -0
  24. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/activity.py +0 -0
  25. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/agents.py +0 -0
  26. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/canals.py +0 -0
  27. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/client.py +0 -0
  28. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/commands.py +0 -0
  29. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/configs.py +0 -0
  30. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/context.py +0 -0
  31. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/events.py +0 -0
  32. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/hooks.py +0 -0
  33. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/implementation_plan.py +0 -0
  34. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/merger.py +0 -0
  35. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/messages.py +0 -0
  36. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/prd.py +0 -0
  37. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/reports.py +0 -0
  38. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/runs.py +0 -0
  39. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/sessions.py +0 -0
  40. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/specs.py +0 -0
  41. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/tasks.py +0 -0
  42. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/workflow_runs.py +0 -0
  43. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/api/workflows.py +0 -0
  44. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/cli.py +0 -0
  45. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/__init__.py +0 -0
  46. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/models.py +0 -0
  47. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/platform.py +0 -0
  48. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/config/settings.py +0 -0
  49. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/evidence.py +0 -0
  50. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/evidence_assets.py +0 -0
  51. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/__init__.py +0 -0
  52. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/base.py +0 -0
  53. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/claude.py +0 -0
  54. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/executor/stream.py +0 -0
  55. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/git_ops.py +0 -0
  56. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/handlers/__init__.py +0 -0
  57. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/handlers/prd.py +0 -0
  58. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/integration.py +0 -0
  59. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/__init__.py +0 -0
  60. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/builder.py +0 -0
  61. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/templates.py +0 -0
  62. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  63. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/py.typed +0 -0
  64. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/retry.py +0 -0
  65. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/__init__.py +0 -0
  66. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/claude_setup.py +0 -0
  67. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/repo_setup.py +0 -0
  68. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  69. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  70. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/settings.json +0 -0
  71. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  72. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  73. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  74. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  75. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
  76. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  77. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
  78. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  79. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  80. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
  81. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  82. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/update_check.py +0 -0
  83. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/version.py +0 -0
  84. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/__init__.py +0 -0
  85. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/context.py +0 -0
  86. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/executor.py +0 -0
  87. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workflow/memory.py +0 -0
  88. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/__init__.py +0 -0
  89. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/preparation.py +0 -0
  90. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/project_manager.py +0 -0
  91. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  92. {steerdev-1.1.28 → steerdev-1.1.29}/src/steerdev_agent/worktree.py +0 -0
  93. {steerdev-1.1.28 → steerdev-1.1.29}/tests/__init__.py +0 -0
  94. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_agent_loop.py +0 -0
  95. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_agent_loop_extended.py +0 -0
  96. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_agents_api.py +0 -0
  97. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_api_client.py +0 -0
  98. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_claude_executor.py +0 -0
  99. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_claude_setup.py +0 -0
  100. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_client_methods.py +0 -0
  101. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_commands_api.py +0 -0
  102. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_config.py +0 -0
  103. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_config_extended.py +0 -0
  104. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_conflict_mitigation.py +0 -0
  105. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_context_search.py +0 -0
  106. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_executor.py +0 -0
  107. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_git_ops.py +0 -0
  108. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_hooks_post_tool_use.py +0 -0
  109. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_platform_config.py +0 -0
  110. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_preparation.py +0 -0
  111. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_preparation_provenance.py +0 -0
  112. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_prompt.py +0 -0
  113. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_reports_client.py +0 -0
  114. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_resolve_max_tasks.py +0 -0
  115. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_retry.py +0 -0
  116. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_runner_merge_modes.py +0 -0
  117. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_runner_worktrees.py +0 -0
  118. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_stream_parser.py +0 -0
  119. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_tasks.py +0 -0
  120. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_tasks_update_branch.py +0 -0
  121. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_version.py +0 -0
  122. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_wave_scoped_fetch.py +0 -0
  123. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_context.py +0 -0
  124. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_memory.py +0 -0
  125. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_prompt_template.py +0 -0
  126. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workflow_runs_api.py +0 -0
  127. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workspace.py +0 -0
  128. {steerdev-1.1.28 → steerdev-1.1.29}/tests/test_workspace_extended.py +0 -0
  129. {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.28
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerdev"
3
- version = "1.1.28"
3
+ version = "1.1.29"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- - Commit with a task-prefixed conventional commit. **Use the external ticket ID when available** (e.g. Linear ID), falling back to the internal short ID:
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 Prefix Convention
75
+ ## Commit Convention (handled by the runtime)
84
76
 
85
- Use external ticket IDs in commits so GitHub integrations can link commits to issues:
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] feat: add user endpoint` |
90
- | No | `[task:<short-uuid>]` | `[task:abc12345] feat: add user endpoint` |
83
+ | Yes | `[<TICKET-ID>]` | `[PROJ-123] <task title>` |
84
+ | No | `[task:<short-uuid>]` | `[task:abc12345] <task title>` |
91
85
 
92
- Run `steerdev tasks get TASK_ID` to find the task's `linear_identifier` before committing.
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
- # Check: steerdev tasks get task-aaa... Linear ID: PROJ-123
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
- # Check: steerdev tasks get task-bbb... Linear ID: PROJ-124
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
- git push -u origin HEAD
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