steerdev 1.1.28__tar.gz → 1.1.31__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.31}/PKG-INFO +1 -1
- {steerdev-1.1.28 → steerdev-1.1.31}/pyproject.toml +1 -1
- steerdev-1.1.31/src/steerdev_agent/git/__init__.py +1 -0
- steerdev-1.1.31/src/steerdev_agent/git/commit.py +96 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/prompt/builder.py +4 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/runner.py +48 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +37 -24
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workspace/preparation.py +78 -25
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/worktree.py +11 -1
- steerdev-1.1.31/tests/test_git_commit.py +127 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_preparation.py +135 -0
- steerdev-1.1.31/tests/test_runner_boundary_commit.py +74 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_runner_merge_modes.py +25 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_runner_worktrees.py +17 -1
- {steerdev-1.1.28 → steerdev-1.1.31}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/.gitignore +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/AGENTS.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/CLAUDE.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/README.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/cli.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/__init__.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_api_client.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_config.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_context_search.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_executor.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_prompt.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_retry.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_tasks.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_version.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_workspace.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.28 → steerdev-1.1.31}/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.31
|
|
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"
|
|
@@ -28,6 +28,10 @@ class WaveContext(BaseModel):
|
|
|
28
28
|
wave_description: str = Field(default="", description="Description of this wave")
|
|
29
29
|
wave_tasks_summary: str = Field(default="", description="Summary of tasks in this wave")
|
|
30
30
|
completed_waves_summary: str = Field(default="", description="Summary of completed waves")
|
|
31
|
+
predecessor_branch: str | None = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description="Branch of the previous completed wave to stack this wave on",
|
|
34
|
+
)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class TaskContext(BaseModel):
|
|
@@ -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,
|
|
@@ -108,6 +110,15 @@ def build_wave_context(wave_response: dict[str, Any]) -> WaveContext | None:
|
|
|
108
110
|
]
|
|
109
111
|
completed_summary = "\n".join(completed_lines) if completed_lines else ""
|
|
110
112
|
|
|
113
|
+
# Stacked-wave base: the server sends the previous completed wave's branch
|
|
114
|
+
# NAME (never a number — the wave name IS the branch). A blank/absent name
|
|
115
|
+
# means no valid predecessor, so we leave the base unset and cut from default.
|
|
116
|
+
predecessor_branch: str | None = None
|
|
117
|
+
predecessor = wave_info.get("predecessor_wave")
|
|
118
|
+
if predecessor:
|
|
119
|
+
name = (predecessor.get("name") or "").strip()
|
|
120
|
+
predecessor_branch = name or None
|
|
121
|
+
|
|
111
122
|
return WaveContext(
|
|
112
123
|
wave_number=wave_info.get("wave_number", 1),
|
|
113
124
|
total_waves=wave_info.get("total_waves", 1),
|
|
@@ -115,6 +126,7 @@ def build_wave_context(wave_response: dict[str, Any]) -> WaveContext | None:
|
|
|
115
126
|
wave_description=wave_info.get("description", ""),
|
|
116
127
|
wave_tasks_summary=tasks_summary,
|
|
117
128
|
completed_waves_summary=completed_summary,
|
|
129
|
+
predecessor_branch=predecessor_branch,
|
|
118
130
|
)
|
|
119
131
|
|
|
120
132
|
|
|
@@ -200,6 +212,27 @@ async def _get_modified_files(working_dir: str | Path) -> list[str]:
|
|
|
200
212
|
return []
|
|
201
213
|
|
|
202
214
|
|
|
215
|
+
async def maybe_commit_task_boundary(
|
|
216
|
+
working_dir: str | Path,
|
|
217
|
+
task: dict[str, Any],
|
|
218
|
+
wave_context: WaveContext | None,
|
|
219
|
+
dry_run: bool,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Commit a wave task's work at its completion boundary, then checkpoint-push.
|
|
222
|
+
|
|
223
|
+
Only fires in wave mode (single-task mode keeps the agent-driven,
|
|
224
|
+
one-PR-per-task flow). Best-effort: `commit_task_boundary` and
|
|
225
|
+
`checkpoint_push` never raise. Pushes only when a commit was actually made,
|
|
226
|
+
so the wave branch lands on origin for the next wave to stack on.
|
|
227
|
+
"""
|
|
228
|
+
if dry_run or wave_context is None:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
result = await commit_task_boundary(working_dir, task)
|
|
232
|
+
if result == "committed":
|
|
233
|
+
checkpoint_push(working_dir)
|
|
234
|
+
|
|
235
|
+
|
|
203
236
|
def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None) -> str:
|
|
204
237
|
"""Compute the git worktree name for a task execution.
|
|
205
238
|
|
|
@@ -924,6 +957,14 @@ class Runner:
|
|
|
924
957
|
except Exception:
|
|
925
958
|
logger.debug("Failed to report modified files", exc_info=True)
|
|
926
959
|
|
|
960
|
+
# Deterministic per-task commit boundary (wave mode only): commit
|
|
961
|
+
# everything outstanding as one task-scoped commit and checkpoint-push,
|
|
962
|
+
# so the wave branch has one clean commit per task and lands on origin
|
|
963
|
+
# for the next wave to stack on. No-op in single-task/dry-run mode.
|
|
964
|
+
await maybe_commit_task_boundary(
|
|
965
|
+
effective_working_dir, task, wave_context, self.dry_run
|
|
966
|
+
)
|
|
967
|
+
|
|
927
968
|
if self._evidence_config.enabled and not self.dry_run:
|
|
928
969
|
await self._submit_evidence_report(
|
|
929
970
|
task=task,
|
|
@@ -1072,6 +1113,13 @@ class Runner:
|
|
|
1072
1113
|
else:
|
|
1073
1114
|
logger.warning(f"Failed to mark task {task_id} as completed")
|
|
1074
1115
|
|
|
1116
|
+
# Deterministic per-task commit boundary (wave mode only; successful
|
|
1117
|
+
# tasks only). See execute_task for rationale.
|
|
1118
|
+
if success:
|
|
1119
|
+
await maybe_commit_task_boundary(
|
|
1120
|
+
effective_working_dir, task, wave_context, self.dry_run
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1075
1123
|
if self._evidence_config.enabled and not self.dry_run:
|
|
1076
1124
|
await self._submit_evidence_report(
|
|
1077
1125
|
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
|
|
|
@@ -77,25 +69,49 @@ git merge-tree --write-tree origin/<default-branch> HEAD
|
|
|
77
69
|
steerdev git pr --title "Wave <N>: <description>" --body "..."
|
|
78
70
|
```
|
|
79
71
|
|
|
72
|
+
**Do not pass `--base`.** `steerdev git pr` sets the PR base automatically from
|
|
73
|
+
the task's recorded base branch. For a stacked wave (a wave that builds on a
|
|
74
|
+
previous wave of the same PRD) the base is the predecessor wave's branch, so the
|
|
75
|
+
PR shows only this wave's diff. Overriding `--base` would break the stack.
|
|
76
|
+
|
|
80
77
|
5. Update all related tasks with the shared PR URL
|
|
81
78
|
6. Log progress and completion in the root `docs/` progress files
|
|
82
79
|
|
|
83
|
-
## Commit
|
|
80
|
+
## Commit Convention (handled by the runtime)
|
|
84
81
|
|
|
85
|
-
|
|
82
|
+
You do **not** commit task work yourself. The steerdev runtime commits each task
|
|
83
|
+
automatically at its completion boundary, using an external-ticket-ID-prefixed
|
|
84
|
+
message so GitHub integrations can link commits to issues:
|
|
86
85
|
|
|
87
86
|
| Has External ID? | Commit Prefix | Example |
|
|
88
87
|
|---|---|---|
|
|
89
|
-
| Yes | `[<TICKET-ID>]` | `[PROJ-123]
|
|
90
|
-
| No | `[task:<short-uuid>]` | `[task:abc12345]
|
|
88
|
+
| Yes | `[<TICKET-ID>]` | `[PROJ-123] <task title>` |
|
|
89
|
+
| No | `[task:<short-uuid>]` | `[task:abc12345] <task title>` |
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
It also pushes the wave branch after each commit. Your job is to implement and,
|
|
92
|
+
once the wave is done, rebase and open the PR (steps 3–5).
|
|
93
|
+
|
|
94
|
+
## Stacked waves (same PRD)
|
|
95
|
+
|
|
96
|
+
Waves of one PRD are **stacked**: wave-2 is branched off wave-1, wave-3 off
|
|
97
|
+
wave-2, and so on. The CLI handles this for you — it cuts the wave branch off the
|
|
98
|
+
previous wave's branch and records that branch as the PR base. As a result:
|
|
99
|
+
|
|
100
|
+
- Each wave still gets its own PR, but its diff shows only that wave's changes.
|
|
101
|
+
- The PRs form a chain: `wave-1 → default`, `wave-2 → wave-1`, `wave-3 → wave-2`.
|
|
102
|
+
- **Do not delete a wave's branch until the next wave's PR has merged.** When
|
|
103
|
+
wave-1's PR merges into the default branch, GitHub automatically retargets
|
|
104
|
+
wave-2's PR — but only if wave-1's branch still exists.
|
|
105
|
+
- The first wave of a PRD (and any wave with no completed same-PRD predecessor)
|
|
106
|
+
is cut from the default branch as usual.
|
|
93
107
|
|
|
94
108
|
## Conventions
|
|
95
109
|
|
|
96
110
|
- Use one branch per wave: `wave/<wave-number>`
|
|
111
|
+
- Do not commit task work — the runtime commits and pushes each task automatically
|
|
97
112
|
- Do not create per-task PRs inside the wave
|
|
98
|
-
-
|
|
113
|
+
- Do not pass `--base` to `steerdev git pr` — the base is set automatically
|
|
114
|
+
- Do not delete wave branches until the dependent wave's PR has merged
|
|
99
115
|
- Include the shared PR URL in every related task result
|
|
100
116
|
- Replace `<default-branch>` in commands above with the actual branch name from the Git Branch Configuration section
|
|
101
117
|
|
|
@@ -105,18 +121,15 @@ Run `steerdev tasks get TASK_ID` to find the task's `linear_identifier` before c
|
|
|
105
121
|
git checkout -b wave/3
|
|
106
122
|
|
|
107
123
|
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"
|
|
124
|
+
# ... implement task 1 ... (runtime commits "[PROJ-123] <title>" at the boundary)
|
|
112
125
|
|
|
113
126
|
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"
|
|
127
|
+
# ... implement task 2 ... (runtime commits "[PROJ-124] <title>" at the boundary)
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
# Wave done: rebase onto the default branch (see step 3), then open one PR.
|
|
130
|
+
git fetch origin <default-branch>
|
|
131
|
+
git rebase origin/<default-branch>
|
|
132
|
+
git push --force-with-lease origin HEAD
|
|
120
133
|
steerdev git pr --title "Wave 3: User management improvements" --body "..."
|
|
121
134
|
steerdev tasks update task-aaa... --result "PR: https://github.com/org/repo/pull/42"
|
|
122
135
|
steerdev tasks update task-bbb... --result "PR: https://github.com/org/repo/pull/42"
|
|
@@ -347,38 +347,91 @@ class WorkspacePreparation:
|
|
|
347
347
|
# 4. Fetch from origin
|
|
348
348
|
await _run_git("fetch", "origin", cwd=cwd, check=False)
|
|
349
349
|
|
|
350
|
-
# 5.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
# Try fast-forward merge
|
|
363
|
-
rc, _, _ = await _run_git(
|
|
364
|
-
"merge", "--ff-only", f"origin/{default_branch}", cwd=cwd, check=False
|
|
365
|
-
)
|
|
366
|
-
if rc != 0:
|
|
367
|
-
# Fallback: hard reset to origin
|
|
368
|
-
logger.warning(f"Fast-forward merge failed for {default_branch}, resetting to origin")
|
|
369
|
-
await _run_git("reset", "--hard", f"origin/{default_branch}", cwd=cwd, check=False)
|
|
370
|
-
warnings.append(
|
|
371
|
-
f"Fast-forward failed, reset {default_branch} to origin/{default_branch}"
|
|
350
|
+
# 5. Resolve the base this wave stacks on. For a new wave with a
|
|
351
|
+
# predecessor (stacked-wave mode), branch directly off the predecessor's
|
|
352
|
+
# remote branch so the wave physically contains the previous wave's work.
|
|
353
|
+
# If that branch is missing from origin (never pushed / deleted), the
|
|
354
|
+
# chain is broken — fall back to the default branch and surface a loud
|
|
355
|
+
# warning rather than silently building on the wrong base.
|
|
356
|
+
predecessor_branch = wave_context.predecessor_branch if wave_context else None
|
|
357
|
+
effective_base = default_branch
|
|
358
|
+
if predecessor_branch:
|
|
359
|
+
rc, _, _ = await _run_git(
|
|
360
|
+
"rev-parse", "--verify", f"origin/{predecessor_branch}", cwd=cwd, check=False
|
|
372
361
|
)
|
|
362
|
+
if rc == 0:
|
|
363
|
+
effective_base = predecessor_branch
|
|
364
|
+
else:
|
|
365
|
+
logger.warning(
|
|
366
|
+
f"Predecessor branch origin/{predecessor_branch} missing — "
|
|
367
|
+
f"stacked-wave chain broken; cutting {target_branch} from "
|
|
368
|
+
f"{default_branch} instead"
|
|
369
|
+
)
|
|
370
|
+
warnings.append(
|
|
371
|
+
f"Predecessor branch '{predecessor_branch}' not found on origin; "
|
|
372
|
+
f"stacked-wave chain broken — cut from '{default_branch}'. This wave "
|
|
373
|
+
f"may not contain the previous wave's work."
|
|
374
|
+
)
|
|
373
375
|
|
|
374
|
-
# 6. Create or checkout target branch
|
|
376
|
+
# 6. Create or checkout the target (wave) branch.
|
|
375
377
|
rc, _, _ = await _run_git("rev-parse", "--verify", target_branch, cwd=cwd, check=False)
|
|
376
378
|
branch_exists = rc == 0
|
|
377
379
|
|
|
378
380
|
if branch_exists:
|
|
379
|
-
|
|
381
|
+
# Branch already present locally (e.g. a continuing wave on a fresh
|
|
382
|
+
# clone): just switch to it.
|
|
383
|
+
rc, _, _ = await _run_git("checkout", target_branch, cwd=cwd, check=False)
|
|
384
|
+
if rc != 0:
|
|
385
|
+
return PreparationResult(
|
|
386
|
+
success=False,
|
|
387
|
+
branch_name=target_branch,
|
|
388
|
+
working_directory=cwd,
|
|
389
|
+
default_branch=default_branch,
|
|
390
|
+
is_worktree=False,
|
|
391
|
+
error=f"Failed to checkout existing branch {target_branch}",
|
|
392
|
+
)
|
|
380
393
|
logger.info(f"Switched to existing branch: {target_branch}")
|
|
394
|
+
elif effective_base != default_branch:
|
|
395
|
+
# Stacked cut: branch straight off the predecessor's remote branch.
|
|
396
|
+
# No default-branch sync — the predecessor already carries the work.
|
|
397
|
+
rc, _, _ = await _run_git(
|
|
398
|
+
"checkout", "-b", target_branch, f"origin/{effective_base}", cwd=cwd, check=False
|
|
399
|
+
)
|
|
400
|
+
if rc != 0:
|
|
401
|
+
return PreparationResult(
|
|
402
|
+
success=False,
|
|
403
|
+
branch_name=target_branch,
|
|
404
|
+
working_directory=cwd,
|
|
405
|
+
default_branch=default_branch,
|
|
406
|
+
is_worktree=False,
|
|
407
|
+
error=f"Failed to cut {target_branch} from origin/{effective_base}",
|
|
408
|
+
)
|
|
409
|
+
logger.info(f"Created {target_branch} stacked on origin/{effective_base}")
|
|
381
410
|
else:
|
|
411
|
+
# Default cut: sync to the default branch, then branch off it.
|
|
412
|
+
rc, _, _ = await _run_git("checkout", default_branch, cwd=cwd, check=False)
|
|
413
|
+
if rc != 0:
|
|
414
|
+
return PreparationResult(
|
|
415
|
+
success=False,
|
|
416
|
+
branch_name=target_branch,
|
|
417
|
+
working_directory=cwd,
|
|
418
|
+
default_branch=default_branch,
|
|
419
|
+
is_worktree=False,
|
|
420
|
+
error=f"Failed to checkout {default_branch}. Does the branch exist?",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
rc, _, _ = await _run_git(
|
|
424
|
+
"merge", "--ff-only", f"origin/{default_branch}", cwd=cwd, check=False
|
|
425
|
+
)
|
|
426
|
+
if rc != 0:
|
|
427
|
+
logger.warning(
|
|
428
|
+
f"Fast-forward merge failed for {default_branch}, resetting to origin"
|
|
429
|
+
)
|
|
430
|
+
await _run_git("reset", "--hard", f"origin/{default_branch}", cwd=cwd, check=False)
|
|
431
|
+
warnings.append(
|
|
432
|
+
f"Fast-forward failed, reset {default_branch} to origin/{default_branch}"
|
|
433
|
+
)
|
|
434
|
+
|
|
382
435
|
await _run_git("checkout", "-b", target_branch, cwd=cwd)
|
|
383
436
|
logger.info(f"Created new branch: {target_branch}")
|
|
384
437
|
|
|
@@ -395,7 +448,7 @@ class WorkspacePreparation:
|
|
|
395
448
|
)
|
|
396
449
|
|
|
397
450
|
await persist_branch_provenance(
|
|
398
|
-
task_id=task_id, branch=target_branch, base_branch=
|
|
451
|
+
task_id=task_id, branch=target_branch, base_branch=effective_base
|
|
399
452
|
)
|
|
400
453
|
|
|
401
454
|
return PreparationResult(
|
|
@@ -206,6 +206,16 @@ class WorktrunkClient:
|
|
|
206
206
|
return None
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
def wave_branch_name(name: str | None, wave_number: int) -> str:
|
|
210
|
+
"""The single source of truth for a wave's git branch name.
|
|
211
|
+
|
|
212
|
+
A wave branch is its `name` (e.g. "wave-a1b2c3"), falling back to
|
|
213
|
+
"wave-{wave_number}". Used by `compute_branch_name` for the current wave and
|
|
214
|
+
by the runner to derive a predecessor wave's branch for stacked-wave cuts.
|
|
215
|
+
"""
|
|
216
|
+
return name or f"wave-{wave_number}"
|
|
217
|
+
|
|
218
|
+
|
|
209
219
|
def compute_branch_name(
|
|
210
220
|
task: dict[str, Any],
|
|
211
221
|
wave_context: WaveContext | None = None,
|
|
@@ -225,7 +235,7 @@ def compute_branch_name(
|
|
|
225
235
|
Branch name string.
|
|
226
236
|
"""
|
|
227
237
|
if wave_context:
|
|
228
|
-
return wave_context.wave_name
|
|
238
|
+
return wave_branch_name(wave_context.wave_name, wave_context.wave_number)
|
|
229
239
|
|
|
230
240
|
# Try external ticket ID first (e.g., Linear "PROJ-123")
|
|
231
241
|
external_id = task.get("linear_identifier") or task.get("external_id")
|
|
@@ -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"
|