steerdev 1.0.58__tar.gz → 1.0.60__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 (116) hide show
  1. {steerdev-1.0.58 → steerdev-1.0.60}/PKG-INFO +1 -1
  2. {steerdev-1.0.58 → steerdev-1.0.60}/pyproject.toml +1 -1
  3. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/agent_loop.py +66 -0
  4. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/commands.py +1 -1
  5. steerdev-1.0.60/src/steerdev_agent/api/merger.py +71 -0
  6. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/cli.py +27 -0
  7. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/config/models.py +20 -0
  8. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/prompt/builder.py +15 -3
  9. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/prompt/templates.py +10 -3
  10. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/runner.py +66 -24
  11. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +6 -5
  12. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +10 -40
  13. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +10 -22
  14. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/steerdev.yaml +5 -0
  15. steerdev-1.0.60/src/steerdev_agent/workspace/preparation.py +243 -0
  16. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_config.py +29 -0
  17. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_conflict_mitigation.py +2 -0
  18. steerdev-1.0.60/tests/test_preparation.py +358 -0
  19. {steerdev-1.0.58 → steerdev-1.0.60}/.github/workflows/pre-commit.yml +0 -0
  20. {steerdev-1.0.58 → steerdev-1.0.60}/.github/workflows/publish.yml +0 -0
  21. {steerdev-1.0.58 → steerdev-1.0.60}/.gitignore +0 -0
  22. {steerdev-1.0.58 → steerdev-1.0.60}/.pre-commit-config.yaml +0 -0
  23. {steerdev-1.0.58 → steerdev-1.0.60}/AGENTS.md +0 -0
  24. {steerdev-1.0.58 → steerdev-1.0.60}/CLAUDE.md +0 -0
  25. {steerdev-1.0.58 → steerdev-1.0.60}/README.md +0 -0
  26. {steerdev-1.0.58 → steerdev-1.0.60}/scripts/pre-commit-version-bump.sh +0 -0
  27. {steerdev-1.0.58 → steerdev-1.0.60}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
  28. {steerdev-1.0.58 → steerdev-1.0.60}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
  29. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/__init__.py +0 -0
  30. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/__init__.py +0 -0
  31. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/activity.py +0 -0
  32. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/agents.py +0 -0
  33. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/canals.py +0 -0
  34. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/client.py +0 -0
  35. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/configs.py +0 -0
  36. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/context.py +0 -0
  37. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/events.py +0 -0
  38. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/hooks.py +0 -0
  39. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/implementation_plan.py +0 -0
  40. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/messages.py +0 -0
  41. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/prd.py +0 -0
  42. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/reports.py +0 -0
  43. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/runs.py +0 -0
  44. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/sessions.py +0 -0
  45. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/specs.py +0 -0
  46. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/tasks.py +0 -0
  47. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/workflow_runs.py +0 -0
  48. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/api/workflows.py +0 -0
  49. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/config/__init__.py +0 -0
  50. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/config/platform.py +0 -0
  51. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/config/settings.py +0 -0
  52. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/evidence.py +0 -0
  53. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/evidence_assets.py +0 -0
  54. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/executor/__init__.py +0 -0
  55. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/executor/base.py +0 -0
  56. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/executor/claude.py +0 -0
  57. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/executor/stream.py +0 -0
  58. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/handlers/__init__.py +0 -0
  59. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/handlers/prd.py +0 -0
  60. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/integration.py +0 -0
  61. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/prompt/__init__.py +0 -0
  62. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  63. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/py.typed +0 -0
  64. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/retry.py +0 -0
  65. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/__init__.py +0 -0
  66. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/claude_setup.py +0 -0
  67. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/repo_setup.py +0 -0
  68. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  69. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  70. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/settings.json +0 -0
  71. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  72. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  73. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  74. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  75. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  76. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  77. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  78. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  79. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/update_check.py +0 -0
  80. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/version.py +0 -0
  81. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workflow/__init__.py +0 -0
  82. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workflow/context.py +0 -0
  83. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workflow/executor.py +0 -0
  84. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workflow/memory.py +0 -0
  85. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workspace/__init__.py +0 -0
  86. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workspace/project_manager.py +0 -0
  87. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  88. {steerdev-1.0.58 → steerdev-1.0.60}/src/steerdev_agent/worktree.py +0 -0
  89. {steerdev-1.0.58 → steerdev-1.0.60}/tests/__init__.py +0 -0
  90. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_agent_loop.py +0 -0
  91. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_agent_loop_extended.py +0 -0
  92. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_agents_api.py +0 -0
  93. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_api_client.py +0 -0
  94. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_claude_executor.py +0 -0
  95. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_claude_setup.py +0 -0
  96. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_client_methods.py +0 -0
  97. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_commands_api.py +0 -0
  98. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_config_extended.py +0 -0
  99. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_context_search.py +0 -0
  100. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_executor.py +0 -0
  101. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_platform_config.py +0 -0
  102. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_prompt.py +0 -0
  103. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_reports_client.py +0 -0
  104. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_retry.py +0 -0
  105. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_runner_merge_modes.py +0 -0
  106. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_runner_worktrees.py +0 -0
  107. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_stream_parser.py +0 -0
  108. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_tasks.py +0 -0
  109. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_version.py +0 -0
  110. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_workflow_context.py +0 -0
  111. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_workflow_memory.py +0 -0
  112. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_workflow_prompt_template.py +0 -0
  113. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_workflow_runs_api.py +0 -0
  114. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_workspace.py +0 -0
  115. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_workspace_extended.py +0 -0
  116. {steerdev-1.0.58 → steerdev-1.0.60}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerdev
3
- Version: 1.0.58
3
+ Version: 1.0.60
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.0.58"
3
+ version = "1.0.60"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -36,6 +36,7 @@ from steerdev_agent.api.sessions import SessionCreateRequest, SessionsClient
36
36
  from steerdev_agent.api.tasks import TasksClient
37
37
  from steerdev_agent.config.models import (
38
38
  AgentLoopConfig,
39
+ BranchConfig,
39
40
  EvidenceConfig,
40
41
  ExecutorConfig,
41
42
  RetryConfig,
@@ -77,6 +78,7 @@ class CommandExecutor:
77
78
  _workflow_id: str | None
78
79
  _enable_waves: bool
79
80
  _enable_canals: bool
81
+ _branch_config: BranchConfig
80
82
  _worktree_config: WorktreeConfig
81
83
  _evidence_config: EvidenceConfig
82
84
  _agent_loop_config: AgentLoopConfig
@@ -126,6 +128,8 @@ class CommandExecutor:
126
128
  await self._execute_task_command(command, project_id, working_directory)
127
129
  elif command.command_type == "prompt":
128
130
  await self._execute_prompt_command(command, project_id, working_directory)
131
+ elif command.command_type == "merger":
132
+ await self._execute_merger_command(command, project_id, working_directory)
129
133
  else:
130
134
  await self._commands_client.mark_failed(
131
135
  command.id, error=f"Unknown command type: {command.command_type}"
@@ -179,6 +183,7 @@ class CommandExecutor:
179
183
  evidence_config=self._evidence_config,
180
184
  executor_config=self._executor_config,
181
185
  force_workflow_id=None,
186
+ branch_config=self._branch_config,
182
187
  shutdown_event=self._shutdown_event,
183
188
  agent_id=self._agent_id,
184
189
  )
@@ -403,6 +408,59 @@ class CommandExecutor:
403
408
  finally:
404
409
  await events_client.close()
405
410
 
411
+ async def _execute_merger_command(
412
+ self,
413
+ command: CommandResponse,
414
+ project_id: str,
415
+ working_directory: Path,
416
+ ) -> None:
417
+ """Execute a merger command - triggers merger run and polls for completion."""
418
+ from steerdev_agent.api.merger import MergerClient
419
+
420
+ assert self._commands_client is not None
421
+
422
+ merger_run_id = command.metadata.get("mergerRunId")
423
+ if not merger_run_id:
424
+ raise ValueError("merger command missing mergerRunId in metadata")
425
+
426
+ merger_client = MergerClient(api_key=self._api_key)
427
+
428
+ logger.info(f"Starting merger run: {merger_run_id}")
429
+
430
+ # Mark command as executing
431
+ await self._commands_client.mark_executing(command.id)
432
+
433
+ # Trigger execution
434
+ result = merger_client.execute_merger_run(merger_run_id)
435
+ if not result:
436
+ raise RuntimeError(f"Failed to trigger merger run {merger_run_id}")
437
+
438
+ # Poll for completion
439
+ max_polls = 60 # 30 minutes at 30s intervals
440
+ for _ in range(max_polls):
441
+ run = merger_client.get_merger_run(merger_run_id)
442
+ if not run:
443
+ raise RuntimeError("Failed to get merger run status")
444
+
445
+ if run.status in ("completed", "failed", "partial"):
446
+ logger.info(f"Merger run {merger_run_id} finished: {run.status}")
447
+ if run.status == "failed":
448
+ raise RuntimeError(f"Merger run failed: {run.error_message}")
449
+ await self._commands_client.mark_completed(
450
+ command.id,
451
+ result=f"Merger run {merger_run_id} {run.status}",
452
+ )
453
+ self._commands_succeeded += 1
454
+ return
455
+
456
+ if self._shutdown_event.is_set():
457
+ logger.info("Shutdown during merger polling, aborting")
458
+ raise RuntimeError("Shutdown requested during merger run")
459
+
460
+ await asyncio.sleep(30)
461
+
462
+ raise RuntimeError(f"Merger run {merger_run_id} timed out")
463
+
406
464
 
407
465
  # ---------------------------------------------------------------------------
408
466
  # Project-scoped agent (original)
@@ -443,6 +501,7 @@ class AgentLoop(CommandExecutor):
443
501
  worktree_config: WorktreeConfig | None = None,
444
502
  evidence_config: EvidenceConfig | None = None,
445
503
  retry_config: RetryConfig | None = None,
504
+ branch_config: BranchConfig | None = None,
446
505
  ) -> None:
447
506
  self.project_id = project_id
448
507
  self.agent_name = agent_name
@@ -454,6 +513,7 @@ class AgentLoop(CommandExecutor):
454
513
  self._agent_loop_config = agent_loop_config or AgentLoopConfig()
455
514
  self._executor_config = executor_config or ExecutorConfig()
456
515
  self._retry_config = retry_config or RetryConfig()
516
+ self._branch_config = branch_config or BranchConfig()
457
517
  self._workflow_id = force_workflow_id
458
518
  self._enable_waves = enable_waves
459
519
  self._enable_canals = enable_canals
@@ -810,6 +870,7 @@ class WorkspaceAgentLoop(CommandExecutor):
810
870
  worktree_config: WorktreeConfig | None = None,
811
871
  evidence_config: EvidenceConfig | None = None,
812
872
  retry_config: RetryConfig | None = None,
873
+ branch_config: BranchConfig | None = None,
813
874
  ) -> None:
814
875
  self.workspace_path = Path(workspace_path)
815
876
  self.agent_name = agent_name
@@ -820,6 +881,7 @@ class WorkspaceAgentLoop(CommandExecutor):
820
881
  self._agent_loop_config = agent_loop_config or AgentLoopConfig()
821
882
  self._executor_config = executor_config or ExecutorConfig()
822
883
  self._retry_config = retry_config or RetryConfig()
884
+ self._branch_config = branch_config or BranchConfig()
823
885
  self._workspace_config = workspace_config or WorkspaceConfig()
824
886
  self._workflow_id = force_workflow_id
825
887
  self._enable_waves = enable_waves
@@ -1232,6 +1294,7 @@ async def run_agent_loop(
1232
1294
  worktree_config: WorktreeConfig | None = None,
1233
1295
  evidence_config: EvidenceConfig | None = None,
1234
1296
  retry_config: RetryConfig | None = None,
1297
+ branch_config: BranchConfig | None = None,
1235
1298
  ) -> None:
1236
1299
  """Run the project-scoped agent loop.
1237
1300
 
@@ -1253,6 +1316,7 @@ async def run_agent_loop(
1253
1316
  worktree_config=worktree_config,
1254
1317
  evidence_config=evidence_config,
1255
1318
  retry_config=retry_config,
1319
+ branch_config=branch_config,
1256
1320
  )
1257
1321
  await agent.start()
1258
1322
 
@@ -1273,6 +1337,7 @@ async def run_workspace_agent_loop(
1273
1337
  worktree_config: WorktreeConfig | None = None,
1274
1338
  evidence_config: EvidenceConfig | None = None,
1275
1339
  retry_config: RetryConfig | None = None,
1340
+ branch_config: BranchConfig | None = None,
1276
1341
  ) -> None:
1277
1342
  """Run the workspace (multi-project) agent loop.
1278
1343
 
@@ -1294,5 +1359,6 @@ async def run_workspace_agent_loop(
1294
1359
  worktree_config=worktree_config,
1295
1360
  evidence_config=evidence_config,
1296
1361
  retry_config=retry_config,
1362
+ branch_config=branch_config,
1297
1363
  )
1298
1364
  await agent.start()
@@ -13,7 +13,7 @@ from steerdev_agent.api.client import get_agent_id, get_api_endpoint, get_api_ke
13
13
 
14
14
  console = Console()
15
15
 
16
- CommandType = Literal["task", "prompt", "control"]
16
+ CommandType = Literal["task", "prompt", "control", "merger"]
17
17
  CommandStatus = Literal[
18
18
  "pending", "claimed", "executing", "completed", "failed", "cancelled", "expired"
19
19
  ]
@@ -0,0 +1,71 @@
1
+ """Merger API client for merger agent operations."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from steerdev_agent.api.client import SteerDevClient
8
+
9
+
10
+ class MergerRunResponse(BaseModel):
11
+ id: str
12
+ status: str
13
+ wave_ids: list[str] = []
14
+ task_ids: list[str] = []
15
+ pr_ids: list[str] = []
16
+ evidence_report_ids: list[str] = []
17
+ summary: str | None = None
18
+ merged_branches: list[str] | None = None
19
+ error_message: str | None = None
20
+ started_at: str | None = None
21
+ completed_at: str | None = None
22
+
23
+
24
+ class GateResponse(BaseModel):
25
+ id: str
26
+ merger_run_id: str
27
+ canal_id: str
28
+ gate_number: int
29
+ status: str
30
+ started_at: str | None = None
31
+ completed_at: str | None = None
32
+ details: dict[str, Any] | None = None
33
+ error_message: str | None = None
34
+
35
+
36
+ class MergerClient(SteerDevClient):
37
+ """Client for merger agent API operations."""
38
+
39
+ def execute_merger_run(self, merger_run_id: str) -> dict[str, Any] | None:
40
+ """Trigger execution of a full merger run."""
41
+ response = self.post(f"/merger-runs/{merger_run_id}/execute")
42
+ if response and response.status_code == 200:
43
+ return response.json()
44
+ return None
45
+
46
+ def get_merger_run(self, merger_run_id: str) -> MergerRunResponse | None:
47
+ """Get merger run status."""
48
+ response = self.get(f"/merger-runs/{merger_run_id}")
49
+ if response and response.status_code == 200:
50
+ return MergerRunResponse(**response.json())
51
+ return None
52
+
53
+ def get_merger_gates(self, merger_run_id: str) -> list[GateResponse]:
54
+ """Get gate statuses for a merger run."""
55
+ response = self.get(f"/merger-runs/{merger_run_id}/gates")
56
+ if response and response.status_code == 200:
57
+ data = response.json()
58
+ return [GateResponse(**g) for g in data.get("gates", [])]
59
+ return []
60
+
61
+ def execute_gate(
62
+ self, canal_id: str, gate_number: int, merger_run_id: str, merge_id: str | None = None
63
+ ) -> dict[str, Any] | None:
64
+ """Trigger execution of a specific gate."""
65
+ payload: dict[str, Any] = {"merger_run_id": merger_run_id}
66
+ if merge_id:
67
+ payload["merge_id"] = merge_id
68
+ response = self.post(f"/canals/{canal_id}/gates/{gate_number}", json=payload)
69
+ if response and response.status_code == 200:
70
+ return response.json()
71
+ return None
@@ -1719,6 +1719,13 @@ def run(
1719
1719
  help="Delay between retries in seconds (default: from config or 3600)",
1720
1720
  ),
1721
1721
  ] = None,
1722
+ default_branch: Annotated[
1723
+ str | None,
1724
+ typer.Option(
1725
+ "--default-branch",
1726
+ help="Default branch for sync/rebase/PR targets (default: from config or 'main')",
1727
+ ),
1728
+ ] = None,
1722
1729
  ) -> None:
1723
1730
  """Run the agent for a project!
1724
1731
 
@@ -1820,6 +1827,11 @@ def run(
1820
1827
  if retry_delay is not None:
1821
1828
  retry_config.delay_seconds = retry_delay
1822
1829
 
1830
+ # Resolve branch config: CLI > config > default
1831
+ resolved_branch_config = config.branches.model_copy()
1832
+ if default_branch is not None:
1833
+ resolved_branch_config.default_branch = default_branch
1834
+
1823
1835
  try:
1824
1836
  result = asyncio.run(
1825
1837
  run_agent(
@@ -1838,6 +1850,7 @@ def run(
1838
1850
  force_workflow_id=resolved_workflow_id,
1839
1851
  dry_run=dry_run,
1840
1852
  retry_config=retry_config,
1853
+ branch_config=resolved_branch_config,
1841
1854
  )
1842
1855
  )
1843
1856
 
@@ -1998,6 +2011,13 @@ def agent(
1998
2011
  help="Submit evidence reports after task completion (default: from config or enabled)",
1999
2012
  ),
2000
2013
  ] = None,
2014
+ default_branch: Annotated[
2015
+ str | None,
2016
+ typer.Option(
2017
+ "--default-branch",
2018
+ help="Default branch for sync/rebase/PR targets (default: from config or 'main')",
2019
+ ),
2020
+ ] = None,
2001
2021
  ) -> None:
2002
2022
  """Run the agent in persistent mode.
2003
2023
 
@@ -2075,6 +2095,11 @@ def agent(
2075
2095
  if retry_delay is not None:
2076
2096
  retry_config.delay_seconds = retry_delay
2077
2097
 
2098
+ # Resolve branch config: CLI > config > default
2099
+ resolved_branch_config = config.branches.model_copy()
2100
+ if default_branch is not None:
2101
+ resolved_branch_config.default_branch = default_branch
2102
+
2078
2103
  # Resolve workspace path: CLI > config
2079
2104
  resolved_workspace = workspace or config.workspace.workspace_dir
2080
2105
 
@@ -2099,6 +2124,7 @@ def agent(
2099
2124
  worktree_config=resolved_worktree_config,
2100
2125
  evidence_config=resolved_evidence_config,
2101
2126
  retry_config=retry_config,
2127
+ branch_config=resolved_branch_config,
2102
2128
  )
2103
2129
  )
2104
2130
  except KeyboardInterrupt:
@@ -2140,6 +2166,7 @@ def agent(
2140
2166
  worktree_config=resolved_worktree_config,
2141
2167
  evidence_config=resolved_evidence_config,
2142
2168
  retry_config=retry_config,
2169
+ branch_config=resolved_branch_config,
2143
2170
  )
2144
2171
  )
2145
2172
  except KeyboardInterrupt:
@@ -246,6 +246,22 @@ class CanalConfig(BaseModel):
246
246
  ]
247
247
 
248
248
 
249
+ class BranchConfig(BaseModel):
250
+ """Branch configuration for git operations.
251
+
252
+ Controls which branch is used for pre-flight sync, rebase targets, and PR base.
253
+ Users set default_branch to whatever their project uses — main, dev, develop, etc.
254
+ """
255
+
256
+ default_branch: Annotated[
257
+ str,
258
+ Field(
259
+ default="main",
260
+ description="Default branch for sync, rebase, and PR targets (e.g. main, dev, develop)",
261
+ ),
262
+ ]
263
+
264
+
249
265
  class EvidenceConfig(BaseModel):
250
266
  """Evidence report configuration.
251
267
 
@@ -341,6 +357,10 @@ class SteerDevConfig(BaseModel):
341
357
  CanalConfig,
342
358
  Field(default_factory=CanalConfig, description="Canal workflow configuration"),
343
359
  ]
360
+ branches: Annotated[
361
+ BranchConfig,
362
+ Field(default_factory=BranchConfig, description="Branch configuration for git operations"),
363
+ ]
344
364
  workspace: Annotated[
345
365
  WorkspaceConfig,
346
366
  Field(default_factory=WorkspaceConfig, description="Workspace agent configuration"),
@@ -68,6 +68,10 @@ class PromptContext(BaseModel):
68
68
  resume_message: str | None = Field(
69
69
  default=None, description="Message for resume (if resuming session)"
70
70
  )
71
+ default_branch: str | None = Field(
72
+ default=None,
73
+ description="Default branch for git operations (e.g. main, dev). Falls back to 'main'.",
74
+ )
71
75
 
72
76
 
73
77
  class PromptBuilder:
@@ -105,7 +109,7 @@ class PromptBuilder:
105
109
 
106
110
  # Build components
107
111
  project_info = self._build_project_info(context.project)
108
- task_info = self._build_task_info(context.task, context.project)
112
+ task_info = self._build_task_info(context.task, context.project, context.default_branch)
109
113
  instructions = self._build_instructions(context)
110
114
 
111
115
  # Add workflow phase if present
@@ -157,12 +161,14 @@ class PromptBuilder:
157
161
  self,
158
162
  task: TaskContext | None,
159
163
  project: ProjectContext | None,
164
+ default_branch: str | None = None,
160
165
  ) -> str:
161
166
  """Build task information section.
162
167
 
163
168
  Args:
164
169
  task: Task context or None.
165
170
  project: Project context for working directory fallback.
171
+ default_branch: Default branch for git operations (falls back to "main").
166
172
 
167
173
  Returns:
168
174
  Formatted task information.
@@ -204,8 +210,14 @@ class PromptBuilder:
204
210
  wave_section += f"\n\n**Tasks in This Wave:**\n{task.wave.wave_tasks_summary}"
205
211
  task_info = f"{task_info}{wave_section}"
206
212
 
207
- # Add git pre-flight sync instructions for new tasks (not resumed sessions)
208
- task_info = f"{task_info}\n\n{self.templates.GIT_PREFLIGHT_SYNC}"
213
+ # Add git branch configuration and pre-flight sync instructions
214
+ branch = default_branch or "main"
215
+ task_info = (
216
+ f"{task_info}\n\n{self.templates.GIT_BRANCH_CONFIG.format(default_branch=branch)}"
217
+ )
218
+ task_info = (
219
+ f"{task_info}\n{self.templates.GIT_PREFLIGHT_SYNC.format(default_branch=branch)}"
220
+ )
209
221
 
210
222
  return task_info
211
223
 
@@ -246,13 +246,20 @@ Create tasks that can be completed by a single developer in a reasonable time (1
246
246
  Larger work should be broken into multiple tasks.
247
247
  """
248
248
 
249
- # Git pre-flight sync instructions injected into task prompts
249
+ # Git pre-flight sync instructions injected into task prompts.
250
+ # Parameterized with {default_branch} — call .format(default_branch=...) before use.
250
251
  GIT_PREFLIGHT_SYNC = """## Git Pre-flight Sync
251
252
  Before starting this task, ensure you are on an up-to-date default branch:
252
253
  1. `git stash push -m "steerdev-preflight" 2>/dev/null || true`
253
- 2. `git checkout main && git pull origin main --ff-only`
254
- 3. If ff-only fails: `git fetch origin && git reset --hard origin/main`
254
+ 2. `git checkout {default_branch} && git pull origin {default_branch} --ff-only`
255
+ 3. If ff-only fails: `git fetch origin && git reset --hard origin/{default_branch}`
255
256
  Only skip this if you are continuing mid-wave on an existing wave branch.
257
+ """
258
+
259
+ # Git branch configuration note injected into task prompts
260
+ GIT_BRANCH_CONFIG = """## Git Branch Configuration
261
+ - Default branch: `{default_branch}`
262
+ Use this branch for all fetch, rebase, and merge operations.
256
263
  """
257
264
 
258
265
  # Phase context instructions appended to phase prompts
@@ -19,7 +19,13 @@ from steerdev_agent.api.events import EventsClient
19
19
  from steerdev_agent.api.runs import RunCreateRequest, RunsClient
20
20
  from steerdev_agent.api.sessions import SessionCreateRequest, SessionsClient
21
21
  from steerdev_agent.api.tasks import TasksClient
22
- from steerdev_agent.config.models import EvidenceConfig, ExecutorConfig, RetryConfig, WorktreeConfig
22
+ from steerdev_agent.config.models import (
23
+ BranchConfig,
24
+ EvidenceConfig,
25
+ ExecutorConfig,
26
+ RetryConfig,
27
+ WorktreeConfig,
28
+ )
23
29
  from steerdev_agent.executor import ExecutorFactory
24
30
  from steerdev_agent.executor.base import AgentExecutor, EventType, StreamEvent
25
31
  from steerdev_agent.executor.claude import ClaudeExecutorError
@@ -277,6 +283,7 @@ class Runner:
277
283
  force_workflow_id: str | None = None,
278
284
  dry_run: bool = False,
279
285
  retry_config: RetryConfig | None = None,
286
+ branch_config: BranchConfig | None = None,
280
287
  shutdown_event: asyncio.Event | None = None,
281
288
  agent_id: str | None = None,
282
289
  ) -> None:
@@ -314,6 +321,7 @@ class Runner:
314
321
  self.workflow_id = force_workflow_id
315
322
  self.dry_run = dry_run
316
323
  self._retry_config = retry_config or RetryConfig()
324
+ self._branch_config = branch_config or BranchConfig()
317
325
  self._shutdown_event = shutdown_event
318
326
 
319
327
  # Executor configuration
@@ -723,6 +731,7 @@ class Runner:
723
731
  linear_identifier=task.get("linear_identifier"),
724
732
  wave=wave_context,
725
733
  ),
734
+ default_branch=self._branch_config.default_branch,
726
735
  )
727
736
  prompt = self._prompt_builder.build(context)
728
737
 
@@ -753,25 +762,45 @@ class Runner:
753
762
  if self._sessions_client and not self.dry_run:
754
763
  await self._sessions_client.mark_running(session_id)
755
764
 
756
- # Set up worktree isolation if enabled
765
+ # Prepare workspace (git fetch, checkout, branch creation)
757
766
  effective_working_dir = self.working_directory
758
767
  worktree_branch: str | None = None
759
768
  legacy_worktree_name: str | None = None
760
769
 
761
- if self._worktree_config.enabled:
762
- if self._worktree_config.provider == "worktrunk":
763
- from steerdev_agent.worktree import WorktrunkClient, compute_branch_name
770
+ if self._worktree_config.enabled and self._worktree_config.provider == "claude":
771
+ # Legacy: pass --worktree to Claude CLI (bypasses preparation)
772
+ legacy_worktree_name = compute_worktree_name(task_id, wave_context)
773
+ console.print(f"[dim]Using legacy worktree: {legacy_worktree_name}[/dim]")
774
+ else:
775
+ from steerdev_agent.workspace.preparation import WorkspacePreparation
764
776
 
765
- wt_client = WorktrunkClient(self.working_directory)
766
- worktree_branch = compute_branch_name(task, wave_context)
767
- effective_working_dir = await wt_client.switch(worktree_branch, create=True)
768
- console.print(
769
- f"[dim]Worktree: {effective_working_dir} (branch: {worktree_branch})[/dim]"
770
- )
771
- else:
772
- # Legacy: pass --worktree to Claude CLI
773
- legacy_worktree_name = compute_worktree_name(task_id, wave_context)
774
- console.print(f"[dim]Using legacy worktree: {legacy_worktree_name}[/dim]")
777
+ preparation = WorkspacePreparation(
778
+ working_directory=self.working_directory,
779
+ worktree_config=self._worktree_config,
780
+ branch_config=self._branch_config,
781
+ )
782
+ prep_result = await preparation.prepare(task, wave_context)
783
+ if not prep_result.success:
784
+ error_msg = f"Workspace preparation failed: {prep_result.error}"
785
+ logger.error(error_msg)
786
+ if self._sessions_client and not self.dry_run:
787
+ await self._sessions_client.mark_failed(
788
+ session_id, metadata={"error": error_msg}
789
+ )
790
+ return {
791
+ "success": False,
792
+ "error": error_msg,
793
+ "events_sent": self._events_sent,
794
+ }
795
+ effective_working_dir = prep_result.working_directory
796
+ if prep_result.is_worktree:
797
+ worktree_branch = prep_result.branch_name
798
+ console.print(
799
+ f"[dim]Workspace ready: {effective_working_dir} "
800
+ f"(branch: {prep_result.branch_name})[/dim]"
801
+ )
802
+ for warning in prep_result.warnings:
803
+ console.print(f"[yellow]Warning: {warning}[/yellow]")
775
804
 
776
805
  # Create executor using factory
777
806
  self._executor = ExecutorFactory.create(
@@ -948,19 +977,29 @@ class Runner:
948
977
  resolved_wf_id = workflow_id or self.workflow_id
949
978
  console.print(f"[dim]Using workflow: {resolved_wf_id}[/dim]")
950
979
 
951
- # Set up worktree isolation if enabled
980
+ # Prepare workspace (git fetch, checkout, branch creation)
952
981
  effective_working_dir = self.working_directory
953
982
  worktree_branch: str | None = None
954
983
 
955
- if self._worktree_config.enabled and self._worktree_config.provider == "worktrunk":
956
- from steerdev_agent.worktree import WorktrunkClient, compute_branch_name
984
+ from steerdev_agent.workspace.preparation import WorkspacePreparation
957
985
 
958
- wt_client = WorktrunkClient(self.working_directory)
959
- worktree_branch = compute_branch_name(task, wave_context)
960
- effective_working_dir = await wt_client.switch(worktree_branch, create=True)
961
- console.print(
962
- f"[dim]Worktree: {effective_working_dir} (branch: {worktree_branch})[/dim]"
963
- )
986
+ preparation = WorkspacePreparation(
987
+ working_directory=self.working_directory,
988
+ worktree_config=self._worktree_config,
989
+ branch_config=self._branch_config,
990
+ )
991
+ prep_result = await preparation.prepare(task, wave_context)
992
+ if not prep_result.success:
993
+ error_msg = f"Workspace preparation failed: {prep_result.error}"
994
+ logger.error(error_msg)
995
+ return {"success": False, "error": error_msg}
996
+ effective_working_dir = prep_result.working_directory
997
+ if prep_result.is_worktree:
998
+ worktree_branch = prep_result.branch_name
999
+ console.print(
1000
+ f"[dim]Workspace ready: {effective_working_dir} "
1001
+ f"(branch: {prep_result.branch_name})[/dim]"
1002
+ )
964
1003
 
965
1004
  # Build task context for workflow
966
1005
  task_context = build_workflow_task_context(
@@ -1354,6 +1393,7 @@ async def run_agent(
1354
1393
  force_workflow_id: str | None = None,
1355
1394
  dry_run: bool = False,
1356
1395
  retry_config: RetryConfig | None = None,
1396
+ branch_config: BranchConfig | None = None,
1357
1397
  ) -> dict[str, Any]:
1358
1398
  """Run the steerdev agent.
1359
1399
 
@@ -1377,6 +1417,7 @@ async def run_agent(
1377
1417
  force_workflow_id: Workflow ID override for multi-phase execution.
1378
1418
  dry_run: If True, print the command without executing it.
1379
1419
  retry_config: Retry configuration for failed tasks.
1420
+ branch_config: Branch configuration for git operations.
1380
1421
 
1381
1422
  Returns:
1382
1423
  Run result metadata.
@@ -1399,6 +1440,7 @@ async def run_agent(
1399
1440
  force_workflow_id=force_workflow_id,
1400
1441
  dry_run=dry_run,
1401
1442
  retry_config=retry_config,
1443
+ branch_config=branch_config,
1402
1444
  )
1403
1445
 
1404
1446
  # Install signal handler that kills the subprocess immediately on Ctrl+C
@@ -18,20 +18,20 @@ Use this skill when a workflow phase tells you to merge the **current task branc
18
18
 
19
19
  ## Pre-check
20
20
 
21
- Before merging into a canal, verify your current branch has been pushed and has a PR. Do NOT switch branches or sync to main — this skill operates on the current task/wave branch.
21
+ Before merging into a canal, verify your current branch has been pushed and has a PR. Do NOT switch branches or sync to the default branch — this skill operates on the current task/wave branch.
22
22
 
23
23
  ## Rebase Before Canal Merge
24
24
 
25
- Before merging into the canal, ensure your branch is up to date:
25
+ Before merging into the canal, ensure your branch is up to date. Refer to the **Git Branch Configuration** section in your prompt for the project's default branch name.
26
26
 
27
27
  ```bash
28
- git fetch origin main
29
- git merge-tree --write-tree origin/main HEAD
28
+ git fetch origin <default-branch>
29
+ git merge-tree --write-tree origin/<default-branch> HEAD
30
30
  ```
31
31
 
32
32
  - **If clean** (exit 0): Rebase and force-push with lease:
33
33
  ```bash
34
- git rebase origin/main
34
+ git rebase origin/<default-branch>
35
35
  git push --force-with-lease origin HEAD
36
36
  ```
37
37
  - **If conflicts**: Attempt rebase. If it fails on files you did not modify, abort (`git rebase --abort`) and re-queue the task:
@@ -75,6 +75,7 @@ steerdev canal pr CANAL_ID
75
75
  - Do not force-push to resolve canal conflicts
76
76
  - Do not manually resolve canal merge conflicts outside the supported tooling
77
77
  - If canal merge fails, report the blocker with task comments or status updates
78
+ - Replace `<default-branch>` in commands above with the actual branch name from the Git Branch Configuration section
78
79
 
79
80
  ## Example
80
81