steerdev 1.1.14__tar.gz → 1.1.25__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.14 → steerdev-1.1.25}/.github/workflows/publish.yml +3 -1
- {steerdev-1.1.14 → steerdev-1.1.25}/PKG-INFO +1 -1
- {steerdev-1.1.14 → steerdev-1.1.25}/pyproject.toml +1 -1
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/hooks.py +27 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/tasks.py +8 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/cli.py +48 -0
- steerdev-1.1.25/src/steerdev_agent/git_ops.py +124 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/settings.json +12 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workspace/preparation.py +40 -4
- steerdev-1.1.25/tests/test_git_ops.py +133 -0
- steerdev-1.1.25/tests/test_hooks_post_tool_use.py +78 -0
- steerdev-1.1.25/tests/test_preparation_provenance.py +42 -0
- steerdev-1.1.25/tests/test_tasks_update_branch.py +32 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/.gitignore +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/AGENTS.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/CLAUDE.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/README.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/runner.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/__init__.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_api_client.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_config.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_context_search.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_executor.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_preparation.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_prompt.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_retry.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_runner_worktrees.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_tasks.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_version.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_workspace.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.14 → steerdev-1.1.25}/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.25
|
|
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
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
10
|
from steerdev_agent.api.client import get_agent_name, get_api_endpoint, get_api_key
|
|
11
|
+
from steerdev_agent.git_ops import checkpoint_push, is_git_commit_command
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class HooksClient:
|
|
@@ -167,3 +168,29 @@ class HooksClient:
|
|
|
167
168
|
},
|
|
168
169
|
)
|
|
169
170
|
sys.exit(0)
|
|
171
|
+
|
|
172
|
+
def post_tool_use(self) -> None:
|
|
173
|
+
"""Process PostToolUse hook event for checkpoint pushes.
|
|
174
|
+
|
|
175
|
+
If the tool was a Bash `git commit`, push the current branch to
|
|
176
|
+
origin as a checkpoint so sandbox work survives a VM teardown.
|
|
177
|
+
Best-effort: always exits 0, never blocks the agent. Does not
|
|
178
|
+
report to the activity API.
|
|
179
|
+
"""
|
|
180
|
+
input_data = self.read_stdin_input()
|
|
181
|
+
if not isinstance(input_data, dict):
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
if input_data.get("tool_name") != "Bash":
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
|
|
187
|
+
tool_input = input_data.get("tool_input")
|
|
188
|
+
if not isinstance(tool_input, dict):
|
|
189
|
+
sys.exit(0)
|
|
190
|
+
command = tool_input.get("command")
|
|
191
|
+
if not is_git_commit_command(command):
|
|
192
|
+
sys.exit(0)
|
|
193
|
+
|
|
194
|
+
cwd = input_data.get("cwd") or "."
|
|
195
|
+
checkpoint_push(cwd)
|
|
196
|
+
sys.exit(0)
|
|
@@ -223,6 +223,8 @@ class TasksClient(SteerDevClient):
|
|
|
223
223
|
result_summary: str | None = None,
|
|
224
224
|
error_message: str | None = None,
|
|
225
225
|
comment: str | None = None,
|
|
226
|
+
branch: str | None = None,
|
|
227
|
+
base_branch: str | None = None,
|
|
226
228
|
) -> bool:
|
|
227
229
|
"""Update a task's fields.
|
|
228
230
|
|
|
@@ -235,6 +237,8 @@ class TasksClient(SteerDevClient):
|
|
|
235
237
|
result_summary: Result summary for completed tasks.
|
|
236
238
|
error_message: Error message for failed tasks.
|
|
237
239
|
comment: Comment to add.
|
|
240
|
+
branch: Task branch name (provenance).
|
|
241
|
+
base_branch: Branch the task branch was created from (provenance).
|
|
238
242
|
|
|
239
243
|
Returns:
|
|
240
244
|
True if update succeeded.
|
|
@@ -266,6 +270,10 @@ class TasksClient(SteerDevClient):
|
|
|
266
270
|
payload["error_message"] = error_message
|
|
267
271
|
if comment:
|
|
268
272
|
payload["comment"] = comment
|
|
273
|
+
if branch:
|
|
274
|
+
payload["branch"] = branch
|
|
275
|
+
if base_branch:
|
|
276
|
+
payload["base_branch"] = base_branch
|
|
269
277
|
|
|
270
278
|
if not payload:
|
|
271
279
|
return False
|
|
@@ -460,6 +460,20 @@ def hooks_subagent_stop() -> None:
|
|
|
460
460
|
client.subagent_stop()
|
|
461
461
|
|
|
462
462
|
|
|
463
|
+
@hooks_app.command("post-tool-use")
|
|
464
|
+
def hooks_post_tool_use() -> None:
|
|
465
|
+
"""Hook called after each Claude Code tool use.
|
|
466
|
+
|
|
467
|
+
Reads JSON from stdin. If the tool was a Bash `git commit`, pushes the
|
|
468
|
+
current branch to origin as a checkpoint so sandbox work survives a VM
|
|
469
|
+
teardown. Best-effort; always exits 0.
|
|
470
|
+
"""
|
|
471
|
+
from steerdev_agent.api.hooks import HooksClient
|
|
472
|
+
|
|
473
|
+
client = HooksClient()
|
|
474
|
+
client.post_tool_use()
|
|
475
|
+
|
|
476
|
+
|
|
463
477
|
# ============================================================================
|
|
464
478
|
# Sessions Command Group
|
|
465
479
|
# ============================================================================
|
|
@@ -1272,6 +1286,16 @@ def git_branch(
|
|
|
1272
1286
|
branch_name = f"task/{prefix}-{slug}" if slug else f"task/{prefix}"
|
|
1273
1287
|
|
|
1274
1288
|
try:
|
|
1289
|
+
# Capture the base branch (current HEAD) before we switch.
|
|
1290
|
+
base_result = subprocess.run(
|
|
1291
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
1292
|
+
capture_output=True,
|
|
1293
|
+
text=True,
|
|
1294
|
+
)
|
|
1295
|
+
base_branch = base_result.stdout.strip() if base_result.returncode == 0 else None
|
|
1296
|
+
if base_branch == "HEAD":
|
|
1297
|
+
base_branch = None
|
|
1298
|
+
|
|
1275
1299
|
# Check if branch already exists
|
|
1276
1300
|
result = subprocess.run(
|
|
1277
1301
|
["git", "rev-parse", "--verify", branch_name],
|
|
@@ -1297,6 +1321,14 @@ def git_branch(
|
|
|
1297
1321
|
)
|
|
1298
1322
|
)
|
|
1299
1323
|
|
|
1324
|
+
# Persist branch provenance (best-effort; never fails the command).
|
|
1325
|
+
try:
|
|
1326
|
+
with TasksClient() as prov_client:
|
|
1327
|
+
if prov_client.check_api_key():
|
|
1328
|
+
prov_client.update_task(task_id, branch=branch_name, base_branch=base_branch)
|
|
1329
|
+
except Exception as prov_exc: # provenance is best-effort
|
|
1330
|
+
console.print(f"[yellow]Could not persist branch provenance: {prov_exc}[/yellow]")
|
|
1331
|
+
|
|
1300
1332
|
except subprocess.CalledProcessError as e:
|
|
1301
1333
|
console.print(f"[red]Git error: {e}[/red]")
|
|
1302
1334
|
raise typer.Exit(1) from None
|
|
@@ -1344,9 +1376,25 @@ def git_pr(
|
|
|
1344
1376
|
# Add steerdev signature
|
|
1345
1377
|
pr_body += "\n\n---\nCreated with [steerdev](https://steerdev.com)"
|
|
1346
1378
|
|
|
1379
|
+
# Prefer the persisted base branch so the PR targets the correct base.
|
|
1380
|
+
from steerdev_agent.api.tasks import TasksClient
|
|
1381
|
+
|
|
1382
|
+
base_branch: str | None = None
|
|
1383
|
+
if task_id:
|
|
1384
|
+
try:
|
|
1385
|
+
with TasksClient() as pr_client:
|
|
1386
|
+
if pr_client.check_api_key():
|
|
1387
|
+
task = pr_client.get_task(task_id)
|
|
1388
|
+
if task:
|
|
1389
|
+
base_branch = task.get("base_branch")
|
|
1390
|
+
except Exception: # fall back to gh default base
|
|
1391
|
+
base_branch = None
|
|
1392
|
+
|
|
1347
1393
|
try:
|
|
1348
1394
|
# Build the gh pr create command
|
|
1349
1395
|
cmd = ["gh", "pr", "create", "--title", title, "--body", pr_body]
|
|
1396
|
+
if base_branch:
|
|
1397
|
+
cmd += ["--base", base_branch]
|
|
1350
1398
|
if draft:
|
|
1351
1399
|
cmd.append("--draft")
|
|
1352
1400
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Synchronous git helpers for checkpoint pushes from Claude Code hooks.
|
|
2
|
+
|
|
3
|
+
Used by the PostToolUse hook to push the current branch after every commit,
|
|
4
|
+
so sandbox work survives a mid-task VM teardown. All functions are
|
|
5
|
+
best-effort and never raise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Matches a `git commit` invocation that actually creates a commit.
|
|
21
|
+
# Requires `git` to be followed by whitespace (not a hyphen), so that
|
|
22
|
+
# hyphenated wrappers like `git-commit-hook` are not matched.
|
|
23
|
+
_COMMIT_RE = re.compile(r"(?:^|&&|;|\|)\s*git\s+[^&|;]*\bcommit\b")
|
|
24
|
+
|
|
25
|
+
# Strips quoted substrings ('...' and "...") so that --dry-run inside a
|
|
26
|
+
# commit message is not mistaken for the actual --dry-run flag.
|
|
27
|
+
_QUOTED_RE = re.compile(r"""(['"]).*?\1""")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _strip_quoted(command: str) -> str:
|
|
31
|
+
"""Remove all single- and double-quoted substrings from *command*."""
|
|
32
|
+
return _QUOTED_RE.sub("", command)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_git_commit_command(command: str | None) -> bool:
|
|
36
|
+
"""Return True if the shell command runs a real `git commit`.
|
|
37
|
+
|
|
38
|
+
Tolerant of flags, paths, and compound commands (&&, ;). Excludes
|
|
39
|
+
`--dry-run`, which does not create a commit.
|
|
40
|
+
|
|
41
|
+
A `--dry-run` flag (outside quoted text) is treated as a non-committing
|
|
42
|
+
command. If `--dry-run` appears only inside a quoted commit message it is
|
|
43
|
+
ignored and the command is still considered committing.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
command: The Bash command string from a PostToolUse payload.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if a committing `git commit` is present.
|
|
50
|
+
"""
|
|
51
|
+
if not command:
|
|
52
|
+
return False
|
|
53
|
+
if "--dry-run" in _strip_quoted(command):
|
|
54
|
+
return False
|
|
55
|
+
return _COMMIT_RE.search(command) is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
PROTECTED_BRANCHES: frozenset[str] = frozenset({"main", "master", "develop"})
|
|
59
|
+
|
|
60
|
+
_GIT_TIMEOUT = 25 # seconds; hook timeout is 30s, leave headroom
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _git_capture(cwd: str | Path, *args: str) -> tuple[int, str]:
|
|
64
|
+
"""Run git, returning (returncode, stdout). Never raises for git errors."""
|
|
65
|
+
proc = subprocess.run(
|
|
66
|
+
["git", *args],
|
|
67
|
+
cwd=str(cwd),
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=_GIT_TIMEOUT,
|
|
71
|
+
)
|
|
72
|
+
return proc.returncode, (proc.stdout or "").strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def current_branch(cwd: str | Path) -> str | None:
|
|
76
|
+
"""Return the current branch name, or None if detached/unavailable."""
|
|
77
|
+
rc, out = _git_capture(cwd, "rev-parse", "--abbrev-ref", "HEAD")
|
|
78
|
+
if rc != 0 or not out or out == "HEAD":
|
|
79
|
+
return None
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def checkpoint_push(cwd: str | Path) -> str:
|
|
84
|
+
"""Push the current branch to origin as a best-effort checkpoint.
|
|
85
|
+
|
|
86
|
+
Force-with-lease so rebases/amends still checkpoint without ever
|
|
87
|
+
overwriting others' work. Never raises; returns a status marker.
|
|
88
|
+
|
|
89
|
+
Returns one of:
|
|
90
|
+
"pushed", "skipped_detached", "skipped_protected",
|
|
91
|
+
"skipped_no_remote", "failed".
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
if shutil.which("git") is None:
|
|
95
|
+
logger.warning("checkpoint_push: git not found, skipping")
|
|
96
|
+
return "failed"
|
|
97
|
+
|
|
98
|
+
branch = current_branch(cwd)
|
|
99
|
+
if branch is None:
|
|
100
|
+
logger.info("checkpoint_push: detached HEAD, skipping")
|
|
101
|
+
return "skipped_detached"
|
|
102
|
+
|
|
103
|
+
if branch in PROTECTED_BRANCHES:
|
|
104
|
+
logger.info(f"checkpoint_push: protected branch {branch}, skipping")
|
|
105
|
+
return "skipped_protected"
|
|
106
|
+
|
|
107
|
+
rc, _ = _git_capture(cwd, "remote", "get-url", "origin")
|
|
108
|
+
if rc != 0:
|
|
109
|
+
logger.info("checkpoint_push: no origin remote, skipping")
|
|
110
|
+
return "skipped_no_remote"
|
|
111
|
+
|
|
112
|
+
rc, out = _git_capture(cwd, "push", "--force-with-lease", "-u", "origin", "HEAD")
|
|
113
|
+
if rc != 0:
|
|
114
|
+
logger.warning(f"checkpoint_push: push failed (non-fatal): {out}")
|
|
115
|
+
return "failed"
|
|
116
|
+
|
|
117
|
+
logger.info(f"checkpoint_push: pushed {branch}")
|
|
118
|
+
return "pushed"
|
|
119
|
+
except subprocess.TimeoutExpired:
|
|
120
|
+
logger.warning("checkpoint_push: push timed out (non-fatal)")
|
|
121
|
+
return "failed"
|
|
122
|
+
except Exception as exc: # broad catch intentional: durability must never break the agent
|
|
123
|
+
logger.warning(f"checkpoint_push: unexpected error (non-fatal): {exc}")
|
|
124
|
+
return "failed"
|
|
@@ -13,6 +13,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
13
13
|
|
|
14
14
|
from loguru import logger
|
|
15
15
|
|
|
16
|
+
from steerdev_agent.api.tasks import TasksClient
|
|
17
|
+
|
|
16
18
|
if TYPE_CHECKING:
|
|
17
19
|
from pathlib import Path
|
|
18
20
|
|
|
@@ -78,6 +80,27 @@ async def _run_git(
|
|
|
78
80
|
return proc.returncode or 0, stdout, stderr
|
|
79
81
|
|
|
80
82
|
|
|
83
|
+
async def persist_branch_provenance(
|
|
84
|
+
task_id: str | None,
|
|
85
|
+
branch: str,
|
|
86
|
+
base_branch: str,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Persist the task branch and its base to the API, best-effort.
|
|
89
|
+
|
|
90
|
+
Written at branch-creation time so an orphaned task (sandbox killed
|
|
91
|
+
mid-run) remains recoverable. Never raises.
|
|
92
|
+
"""
|
|
93
|
+
if not task_id:
|
|
94
|
+
return
|
|
95
|
+
try:
|
|
96
|
+
with TasksClient() as client:
|
|
97
|
+
if not client.check_api_key():
|
|
98
|
+
return
|
|
99
|
+
client.update_task(task_id, branch=branch, base_branch=base_branch)
|
|
100
|
+
except Exception as exc: # provenance is best-effort
|
|
101
|
+
logger.warning(f"Failed to persist branch provenance for {task_id}: {exc}")
|
|
102
|
+
|
|
103
|
+
|
|
81
104
|
class WorkspacePreparation:
|
|
82
105
|
"""Prepares the git workspace before task execution.
|
|
83
106
|
|
|
@@ -120,7 +143,9 @@ class WorkspacePreparation:
|
|
|
120
143
|
return await self._prepare_worktree(target_branch, default_branch)
|
|
121
144
|
|
|
122
145
|
# Non-worktree mode: prepare in-place
|
|
123
|
-
return await self._prepare_in_place(
|
|
146
|
+
return await self._prepare_in_place(
|
|
147
|
+
target_branch, default_branch, wave_context, task.get("id")
|
|
148
|
+
)
|
|
124
149
|
|
|
125
150
|
async def _prepare_worktree(
|
|
126
151
|
self,
|
|
@@ -146,6 +171,7 @@ class WorkspacePreparation:
|
|
|
146
171
|
target_branch: str,
|
|
147
172
|
default_branch: str,
|
|
148
173
|
wave_context: WaveContext | None,
|
|
174
|
+
task_id: str | None = None,
|
|
149
175
|
) -> PreparationResult:
|
|
150
176
|
"""Prepare workspace in the main repository directory.
|
|
151
177
|
|
|
@@ -159,7 +185,9 @@ class WorkspacePreparation:
|
|
|
159
185
|
rc, _, _ = await _run_git("rev-parse", "--is-inside-work-tree", cwd=cwd, check=False)
|
|
160
186
|
if rc == 0:
|
|
161
187
|
# Single-repo layout: prepare in place
|
|
162
|
-
return await self._prepare_single_repo(
|
|
188
|
+
return await self._prepare_single_repo(
|
|
189
|
+
cwd, target_branch, default_branch, wave_context, task_id
|
|
190
|
+
)
|
|
163
191
|
|
|
164
192
|
# Not a git repo — check for multi-repo layout (subdirectories that are git repos)
|
|
165
193
|
repos = await self._discover_git_repos(cwd)
|
|
@@ -174,7 +202,9 @@ class WorkspacePreparation:
|
|
|
174
202
|
)
|
|
175
203
|
|
|
176
204
|
# Multi-repo layout: prepare each discovered repo
|
|
177
|
-
return await self._prepare_multi_repo(
|
|
205
|
+
return await self._prepare_multi_repo(
|
|
206
|
+
repos, target_branch, default_branch, wave_context, task_id
|
|
207
|
+
)
|
|
178
208
|
|
|
179
209
|
async def _discover_git_repos(self, directory: Path) -> list[Path]:
|
|
180
210
|
"""Discover git repositories in immediate subdirectories.
|
|
@@ -204,6 +234,7 @@ class WorkspacePreparation:
|
|
|
204
234
|
target_branch: str,
|
|
205
235
|
default_branch: str,
|
|
206
236
|
wave_context: WaveContext | None,
|
|
237
|
+
task_id: str | None = None,
|
|
207
238
|
) -> PreparationResult:
|
|
208
239
|
"""Prepare multiple git repositories in a workspace directory.
|
|
209
240
|
|
|
@@ -232,7 +263,7 @@ class WorkspacePreparation:
|
|
|
232
263
|
logger.info(f"Preparing repository: {repo_name}")
|
|
233
264
|
|
|
234
265
|
result = await self._prepare_single_repo(
|
|
235
|
-
repo_path, target_branch, default_branch, wave_context
|
|
266
|
+
repo_path, target_branch, default_branch, wave_context, task_id
|
|
236
267
|
)
|
|
237
268
|
|
|
238
269
|
if not result.success:
|
|
@@ -269,6 +300,7 @@ class WorkspacePreparation:
|
|
|
269
300
|
target_branch: str,
|
|
270
301
|
default_branch: str,
|
|
271
302
|
wave_context: WaveContext | None,
|
|
303
|
+
task_id: str | None = None,
|
|
272
304
|
) -> PreparationResult:
|
|
273
305
|
"""Prepare a single git repository directory.
|
|
274
306
|
|
|
@@ -355,6 +387,10 @@ class WorkspacePreparation:
|
|
|
355
387
|
error=f"Branch mismatch: expected {target_branch}, got {actual_branch}",
|
|
356
388
|
)
|
|
357
389
|
|
|
390
|
+
await persist_branch_provenance(
|
|
391
|
+
task_id=task_id, branch=target_branch, base_branch=default_branch
|
|
392
|
+
)
|
|
393
|
+
|
|
358
394
|
return PreparationResult(
|
|
359
395
|
success=True,
|
|
360
396
|
branch_name=target_branch,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Tests for the git_ops module (commit detection and checkpoint push)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from steerdev_agent.git_ops import PROTECTED_BRANCHES, checkpoint_push, is_git_commit_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_detects_plain_commit():
|
|
10
|
+
assert is_git_commit_command("git commit -m 'msg'") is True
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_detects_commit_with_amend():
|
|
14
|
+
assert is_git_commit_command("git commit --amend --no-edit") is True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_detects_commit_in_compound_command():
|
|
18
|
+
assert is_git_commit_command("git add -A && git commit -m 'x'") is True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_ignores_dry_run():
|
|
22
|
+
assert is_git_commit_command("git commit --dry-run") is False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_ignores_non_commit_git():
|
|
26
|
+
assert is_git_commit_command("git status") is False
|
|
27
|
+
assert is_git_commit_command("git log --oneline") is False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_ignores_unrelated_command():
|
|
31
|
+
assert is_git_commit_command("echo git commit") is False
|
|
32
|
+
assert is_git_commit_command("ls -la") is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_handles_empty():
|
|
36
|
+
assert is_git_commit_command("") is False
|
|
37
|
+
assert is_git_commit_command(None) is False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_ignores_hyphenated_git_wrapper():
|
|
41
|
+
assert is_git_commit_command("git-commit-hook") is False
|
|
42
|
+
assert is_git_commit_command("build && git-commit-hook") is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_detects_commit_with_extra_spaces():
|
|
46
|
+
assert is_git_commit_command("git commit -m x") is True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_dry_run_inside_message_still_commits():
|
|
50
|
+
assert is_git_commit_command('git commit -m "talk about --dry-run"') is True
|
|
51
|
+
assert is_git_commit_command("git commit -m 'fix --dry-run bug'") is True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _git(cwd: Path, *args: str) -> None:
|
|
55
|
+
subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _make_repo_with_origin(tmp_path: Path) -> Path:
|
|
59
|
+
origin = tmp_path / "origin.git"
|
|
60
|
+
subprocess.run(["git", "init", "--bare", str(origin)], check=True, capture_output=True)
|
|
61
|
+
work = tmp_path / "work"
|
|
62
|
+
work.mkdir()
|
|
63
|
+
_git(work, "init")
|
|
64
|
+
_git(work, "config", "user.email", "t@t.dev")
|
|
65
|
+
_git(work, "config", "user.name", "t")
|
|
66
|
+
_git(work, "remote", "add", "origin", str(origin))
|
|
67
|
+
(work / "f.txt").write_text("a")
|
|
68
|
+
_git(work, "add", "-A")
|
|
69
|
+
_git(work, "commit", "-m", "init")
|
|
70
|
+
return work
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _remote_has_branch(work: Path, branch: str) -> bool:
|
|
74
|
+
out = subprocess.run(
|
|
75
|
+
["git", "ls-remote", "--heads", "origin", branch],
|
|
76
|
+
cwd=work,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
check=True,
|
|
80
|
+
)
|
|
81
|
+
return bool(out.stdout.strip())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_pushes_task_branch(tmp_path):
|
|
85
|
+
work = _make_repo_with_origin(tmp_path)
|
|
86
|
+
_git(work, "checkout", "-b", "task/PROJ-1-x")
|
|
87
|
+
(work / "f.txt").write_text("b")
|
|
88
|
+
_git(work, "add", "-A")
|
|
89
|
+
_git(work, "commit", "-m", "work")
|
|
90
|
+
|
|
91
|
+
assert checkpoint_push(work) == "pushed"
|
|
92
|
+
assert _remote_has_branch(work, "task/PROJ-1-x") is True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_skips_protected_branch(tmp_path):
|
|
96
|
+
work = _make_repo_with_origin(tmp_path)
|
|
97
|
+
current = subprocess.run(
|
|
98
|
+
["git", "branch", "--show-current"], cwd=work, capture_output=True, text=True, check=True
|
|
99
|
+
).stdout.strip()
|
|
100
|
+
assert current in PROTECTED_BRANCHES # main or master
|
|
101
|
+
assert checkpoint_push(work) == "skipped_protected"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_skips_detached_head(tmp_path):
|
|
105
|
+
work = _make_repo_with_origin(tmp_path)
|
|
106
|
+
sha = subprocess.run(
|
|
107
|
+
["git", "rev-parse", "HEAD"], cwd=work, capture_output=True, text=True, check=True
|
|
108
|
+
).stdout.strip()
|
|
109
|
+
_git(work, "checkout", sha) # detached
|
|
110
|
+
assert checkpoint_push(work) == "skipped_detached"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_skips_when_no_origin(tmp_path):
|
|
114
|
+
work = tmp_path / "noorigin"
|
|
115
|
+
work.mkdir()
|
|
116
|
+
_git(work, "init")
|
|
117
|
+
_git(work, "config", "user.email", "t@t.dev")
|
|
118
|
+
_git(work, "config", "user.name", "t")
|
|
119
|
+
(work / "f.txt").write_text("a")
|
|
120
|
+
_git(work, "add", "-A")
|
|
121
|
+
_git(work, "commit", "-m", "init")
|
|
122
|
+
_git(work, "checkout", "-b", "task/x")
|
|
123
|
+
assert checkpoint_push(work) == "skipped_no_remote"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_returns_failed_without_raising_on_unreachable_remote(tmp_path):
|
|
127
|
+
work = _make_repo_with_origin(tmp_path)
|
|
128
|
+
_git(work, "remote", "set-url", "origin", str(tmp_path / "does-not-exist.git"))
|
|
129
|
+
_git(work, "checkout", "-b", "task/x")
|
|
130
|
+
(work / "f.txt").write_text("b")
|
|
131
|
+
_git(work, "add", "-A")
|
|
132
|
+
_git(work, "commit", "-m", "work")
|
|
133
|
+
assert checkpoint_push(work) == "failed"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from steerdev_agent.api.hooks import HooksClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run_with_stdin(payload: dict, monkeypatch) -> str | None:
|
|
11
|
+
"""Drive post_tool_use with a fake stdin; capture the cwd it pushed (or None)."""
|
|
12
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload)))
|
|
13
|
+
captured: dict = {}
|
|
14
|
+
|
|
15
|
+
def fake_push(cwd):
|
|
16
|
+
captured["cwd"] = str(cwd)
|
|
17
|
+
return "pushed"
|
|
18
|
+
|
|
19
|
+
with (
|
|
20
|
+
patch("steerdev_agent.api.hooks.checkpoint_push", side_effect=fake_push),
|
|
21
|
+
pytest.raises(SystemExit) as exc,
|
|
22
|
+
):
|
|
23
|
+
HooksClient().post_tool_use()
|
|
24
|
+
assert exc.value.code == 0
|
|
25
|
+
return captured.get("cwd")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_pushes_on_git_commit(monkeypatch):
|
|
29
|
+
payload = {
|
|
30
|
+
"tool_name": "Bash",
|
|
31
|
+
"tool_input": {"command": "git commit -m 'x'"},
|
|
32
|
+
"cwd": "/work/repo",
|
|
33
|
+
}
|
|
34
|
+
assert _run_with_stdin(payload, monkeypatch) == "/work/repo"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_no_push_on_non_commit_bash(monkeypatch):
|
|
38
|
+
payload = {"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": "/work/repo"}
|
|
39
|
+
assert _run_with_stdin(payload, monkeypatch) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_no_push_on_non_bash_tool(monkeypatch):
|
|
43
|
+
payload = {"tool_name": "Edit", "tool_input": {"file_path": "a.py"}, "cwd": "/work/repo"}
|
|
44
|
+
assert _run_with_stdin(payload, monkeypatch) is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_exits_zero_on_malformed_stdin(monkeypatch):
|
|
48
|
+
monkeypatch.setattr("sys.stdin", io.StringIO("not json"))
|
|
49
|
+
with (
|
|
50
|
+
patch("steerdev_agent.api.hooks.checkpoint_push") as push,
|
|
51
|
+
pytest.raises(SystemExit) as exc,
|
|
52
|
+
):
|
|
53
|
+
HooksClient().post_tool_use()
|
|
54
|
+
assert exc.value.code == 0
|
|
55
|
+
push.assert_not_called()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_exits_zero_on_null_tool_input(monkeypatch):
|
|
59
|
+
payload = {"tool_name": "Bash", "tool_input": None, "cwd": "/work/repo"}
|
|
60
|
+
assert _run_with_stdin(payload, monkeypatch) is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_exits_zero_on_non_dict_stdin(monkeypatch):
|
|
64
|
+
"""A truthy non-dict JSON payload must not raise (never-block invariant)."""
|
|
65
|
+
monkeypatch.setattr("sys.stdin", io.StringIO("42"))
|
|
66
|
+
with (
|
|
67
|
+
patch("steerdev_agent.api.hooks.checkpoint_push") as push,
|
|
68
|
+
pytest.raises(SystemExit) as exc,
|
|
69
|
+
):
|
|
70
|
+
HooksClient().post_tool_use()
|
|
71
|
+
assert exc.value.code == 0
|
|
72
|
+
push.assert_not_called()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_exits_zero_on_string_tool_input(monkeypatch):
|
|
76
|
+
"""A truthy non-dict tool_input must not raise."""
|
|
77
|
+
payload = {"tool_name": "Bash", "tool_input": "git commit -m x", "cwd": "/work/repo"}
|
|
78
|
+
assert _run_with_stdin(payload, monkeypatch) is None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from steerdev_agent.workspace.preparation import persist_branch_provenance
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_persist_calls_update_task_with_branch_and_base():
|
|
8
|
+
fake_client = MagicMock()
|
|
9
|
+
fake_client.check_api_key.return_value = True
|
|
10
|
+
fake_client.__enter__.return_value = fake_client
|
|
11
|
+
fake_client.__exit__.return_value = False
|
|
12
|
+
|
|
13
|
+
with patch("steerdev_agent.workspace.preparation.TasksClient", return_value=fake_client):
|
|
14
|
+
asyncio.run(
|
|
15
|
+
persist_branch_provenance(
|
|
16
|
+
task_id="task-9", branch="task/PROJ-1-x", base_branch="develop"
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
fake_client.update_task.assert_called_once_with(
|
|
21
|
+
"task-9", branch="task/PROJ-1-x", base_branch="develop"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_persist_swallows_errors():
|
|
26
|
+
fake_client = MagicMock()
|
|
27
|
+
fake_client.check_api_key.return_value = True
|
|
28
|
+
fake_client.__enter__.return_value = fake_client
|
|
29
|
+
fake_client.__exit__.return_value = False
|
|
30
|
+
fake_client.update_task.side_effect = RuntimeError("network down")
|
|
31
|
+
|
|
32
|
+
with patch("steerdev_agent.workspace.preparation.TasksClient", return_value=fake_client):
|
|
33
|
+
# Must not raise.
|
|
34
|
+
asyncio.run(
|
|
35
|
+
persist_branch_provenance(task_id="task-9", branch="task/x", base_branch="main")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_persist_noop_without_task_id():
|
|
40
|
+
with patch("steerdev_agent.workspace.preparation.TasksClient") as cls:
|
|
41
|
+
asyncio.run(persist_branch_provenance(task_id=None, branch="b", base_branch="m"))
|
|
42
|
+
cls.assert_not_called()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from steerdev_agent.api.tasks import TasksClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _client() -> TasksClient:
|
|
7
|
+
# Bypass __init__ network/key setup; we only test payload building.
|
|
8
|
+
client = TasksClient.__new__(TasksClient)
|
|
9
|
+
client.api_base = "https://api.test/v1"
|
|
10
|
+
return client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_update_task_sends_branch_and_base():
|
|
14
|
+
client = _client()
|
|
15
|
+
resp = MagicMock(status_code=200)
|
|
16
|
+
with patch.object(client, "patch", return_value=resp) as mock_patch:
|
|
17
|
+
ok = client.update_task("task-123", branch="task/PROJ-1-x", base_branch="develop")
|
|
18
|
+
assert ok is True
|
|
19
|
+
args, kwargs = mock_patch.call_args
|
|
20
|
+
assert args[0] == "/tasks/task-123"
|
|
21
|
+
assert kwargs["json"]["branch"] == "task/PROJ-1-x"
|
|
22
|
+
assert kwargs["json"]["base_branch"] == "develop"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_update_task_omits_branch_when_not_given():
|
|
26
|
+
client = _client()
|
|
27
|
+
resp = MagicMock(status_code=200)
|
|
28
|
+
with patch.object(client, "patch", return_value=resp) as mock_patch:
|
|
29
|
+
client.update_task("task-123", result_summary="done")
|
|
30
|
+
_, kwargs = mock_patch.call_args
|
|
31
|
+
assert "branch" not in kwargs["json"]
|
|
32
|
+
assert "base_branch" not in kwargs["json"]
|
|
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.14 → steerdev-1.1.25}/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.14 → steerdev-1.1.25}/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
|