steerdev 1.0.52__tar.gz → 1.0.56__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 (109) hide show
  1. {steerdev-1.0.52 → steerdev-1.0.56}/PKG-INFO +1 -1
  2. {steerdev-1.0.52 → steerdev-1.0.56}/pyproject.toml +1 -1
  3. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/agent_loop.py +2 -0
  4. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/tasks.py +100 -2
  5. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/cli.py +48 -0
  6. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/runner.py +61 -7
  7. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +20 -0
  8. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +26 -2
  9. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +26 -2
  10. steerdev-1.0.56/tests/test_conflict_mitigation.py +622 -0
  11. {steerdev-1.0.52 → steerdev-1.0.56}/.github/workflows/pre-commit.yml +0 -0
  12. {steerdev-1.0.52 → steerdev-1.0.56}/.github/workflows/publish.yml +0 -0
  13. {steerdev-1.0.52 → steerdev-1.0.56}/.gitignore +0 -0
  14. {steerdev-1.0.52 → steerdev-1.0.56}/.pre-commit-config.yaml +0 -0
  15. {steerdev-1.0.52 → steerdev-1.0.56}/AGENTS.md +0 -0
  16. {steerdev-1.0.52 → steerdev-1.0.56}/CLAUDE.md +0 -0
  17. {steerdev-1.0.52 → steerdev-1.0.56}/README.md +0 -0
  18. {steerdev-1.0.52 → steerdev-1.0.56}/scripts/pre-commit-version-bump.sh +0 -0
  19. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/__init__.py +0 -0
  20. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/__init__.py +0 -0
  21. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/activity.py +0 -0
  22. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/agents.py +0 -0
  23. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/canals.py +0 -0
  24. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/client.py +0 -0
  25. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/commands.py +0 -0
  26. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/configs.py +0 -0
  27. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/context.py +0 -0
  28. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/events.py +0 -0
  29. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/hooks.py +0 -0
  30. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/implementation_plan.py +0 -0
  31. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/messages.py +0 -0
  32. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/prd.py +0 -0
  33. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/reports.py +0 -0
  34. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/runs.py +0 -0
  35. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/sessions.py +0 -0
  36. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/specs.py +0 -0
  37. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/workflow_runs.py +0 -0
  38. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/api/workflows.py +0 -0
  39. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/config/__init__.py +0 -0
  40. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/config/models.py +0 -0
  41. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/config/platform.py +0 -0
  42. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/config/settings.py +0 -0
  43. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/evidence.py +0 -0
  44. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/executor/__init__.py +0 -0
  45. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/executor/base.py +0 -0
  46. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/executor/claude.py +0 -0
  47. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/executor/stream.py +0 -0
  48. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/handlers/__init__.py +0 -0
  49. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/handlers/prd.py +0 -0
  50. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/integration.py +0 -0
  51. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/prompt/__init__.py +0 -0
  52. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/prompt/builder.py +0 -0
  53. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/prompt/templates.py +0 -0
  54. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  55. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/py.typed +0 -0
  56. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/retry.py +0 -0
  57. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/__init__.py +0 -0
  58. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/claude_setup.py +0 -0
  59. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/repo_setup.py +0 -0
  60. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  61. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  62. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/settings.json +0 -0
  63. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  64. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  65. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  66. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  67. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  68. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  69. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  70. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
  71. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  72. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/update_check.py +0 -0
  73. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/version.py +0 -0
  74. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workflow/__init__.py +0 -0
  75. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workflow/context.py +0 -0
  76. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workflow/executor.py +0 -0
  77. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workflow/memory.py +0 -0
  78. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workspace/__init__.py +0 -0
  79. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workspace/project_manager.py +0 -0
  80. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  81. {steerdev-1.0.52 → steerdev-1.0.56}/src/steerdev_agent/worktree.py +0 -0
  82. {steerdev-1.0.52 → steerdev-1.0.56}/tests/__init__.py +0 -0
  83. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_agent_loop.py +0 -0
  84. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_agent_loop_extended.py +0 -0
  85. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_agents_api.py +0 -0
  86. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_api_client.py +0 -0
  87. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_claude_executor.py +0 -0
  88. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_claude_setup.py +0 -0
  89. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_client_methods.py +0 -0
  90. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_commands_api.py +0 -0
  91. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_config.py +0 -0
  92. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_config_extended.py +0 -0
  93. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_context_search.py +0 -0
  94. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_executor.py +0 -0
  95. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_platform_config.py +0 -0
  96. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_prompt.py +0 -0
  97. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_reports_client.py +0 -0
  98. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_retry.py +0 -0
  99. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_runner_merge_modes.py +0 -0
  100. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_runner_worktrees.py +0 -0
  101. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_stream_parser.py +0 -0
  102. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_tasks.py +0 -0
  103. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_version.py +0 -0
  104. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_workflow_context.py +0 -0
  105. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_workflow_memory.py +0 -0
  106. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_workflow_prompt_template.py +0 -0
  107. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_workspace.py +0 -0
  108. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_workspace_extended.py +0 -0
  109. {steerdev-1.0.52 → steerdev-1.0.56}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerdev
3
- Version: 1.0.52
3
+ Version: 1.0.56
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.52"
3
+ version = "1.0.56"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -85,6 +85,7 @@ class CommandExecutor:
85
85
  _shutdown_event: asyncio.Event
86
86
  _current_task: asyncio.Task[bool] | None
87
87
  _signal_count: int
88
+ _agent_id: str | None
88
89
  _commands_executed: int
89
90
  _commands_succeeded: int
90
91
  _commands_failed: int
@@ -179,6 +180,7 @@ class CommandExecutor:
179
180
  executor_config=self._executor_config,
180
181
  force_workflow_id=None,
181
182
  shutdown_event=self._shutdown_event,
183
+ agent_id=self._agent_id,
182
184
  )
183
185
 
184
186
  # Fetch task details
@@ -65,7 +65,11 @@ def validate_status_transition(current: str, new: str) -> tuple[bool, str | None
65
65
  class TasksClient(SteerDevClient):
66
66
  """Client for task management operations."""
67
67
 
68
- def get_next_task(self, project_id: str | None = None) -> dict[str, Any] | None:
68
+ def get_next_task(
69
+ self,
70
+ project_id: str | None = None,
71
+ agent_id: str | None = None,
72
+ ) -> dict[str, Any] | None:
69
73
  """Fetch the next task to work on (single-task mode, no waves).
70
74
 
71
75
  Returns the highest priority task in order:
@@ -75,6 +79,7 @@ class TasksClient(SteerDevClient):
75
79
 
76
80
  Args:
77
81
  project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
82
+ agent_id: Agent ID for conflict-aware scheduling.
78
83
 
79
84
  Returns:
80
85
  Task data dict or None if no tasks available.
@@ -84,6 +89,8 @@ class TasksClient(SteerDevClient):
84
89
  params: dict[str, str] = {"waves": "false"}
85
90
  if effective_project_id:
86
91
  params["project_id"] = effective_project_id
92
+ if agent_id:
93
+ params["agent_id"] = agent_id
87
94
 
88
95
  console.print(f"Fetching next task from {self.api_base}/tasks/next")
89
96
  response = self.get("/tasks/next", params=params if params else None)
@@ -97,12 +104,19 @@ class TasksClient(SteerDevClient):
97
104
 
98
105
  return response.json()
99
106
 
100
- def get_next_wave(self, project_id: str | None = None) -> dict[str, Any] | None:
107
+ def get_next_wave(
108
+ self,
109
+ project_id: str | None = None,
110
+ agent_id: str | None = None,
111
+ ) -> dict[str, Any] | None:
101
112
  """Fetch the next task with wave-aware scheduling.
102
113
 
103
114
  Calls the API with waves=true. The API tries wave scheduling first,
104
115
  then falls back to returning a plain task if no waves exist.
105
116
 
117
+ When agent_id is provided, the server uses conflict-aware scheduling
118
+ to avoid assigning tasks that overlap files with in-flight tasks.
119
+
106
120
  Returns either:
107
121
  - A wave response dict (has "wave" and "context" keys) with full wave context
108
122
  - A plain task dict (no wave keys) when no waves exist for the project
@@ -110,6 +124,7 @@ class TasksClient(SteerDevClient):
110
124
 
111
125
  Args:
112
126
  project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
127
+ agent_id: Agent ID for conflict-aware scheduling.
113
128
 
114
129
  Returns:
115
130
  Wave response, plain task, or None.
@@ -119,6 +134,8 @@ class TasksClient(SteerDevClient):
119
134
  params: dict[str, str] = {"waves": "true"}
120
135
  if effective_project_id:
121
136
  params["project_id"] = effective_project_id
137
+ if agent_id:
138
+ params["agent_id"] = agent_id
122
139
 
123
140
  console.print(f"Fetching next task from {self.api_base}/tasks/next")
124
141
  response = self.get("/tasks/next", params=params)
@@ -413,6 +430,87 @@ class TasksClient(SteerDevClient):
413
430
  """
414
431
  return self.update_task(task_id, status="canceled", error_message=error_message)
415
432
 
433
+ def report_files(
434
+ self,
435
+ task_id: str,
436
+ actual_files: list[str] | None = None,
437
+ predicted_files: list[str] | None = None,
438
+ ) -> bool:
439
+ """Report files modified by this task for conflict detection.
440
+
441
+ Args:
442
+ task_id: Task ID to update.
443
+ actual_files: Files actually modified (from git diff).
444
+ predicted_files: Predicted files (from pre-analysis).
445
+
446
+ Returns:
447
+ True if update succeeded.
448
+ """
449
+ payload: dict[str, Any] = {}
450
+ if actual_files is not None:
451
+ payload["actual_files"] = actual_files
452
+ if predicted_files is not None:
453
+ payload["predicted_files"] = predicted_files
454
+ payload["source"] = "agent_reported"
455
+
456
+ if not payload:
457
+ return False
458
+
459
+ response = self.patch(f"/tasks/{task_id}/files", json=payload)
460
+ if response.status_code not in (200, 204):
461
+ logger.warning(f"Failed to report files for task {task_id}: {response.status_code}")
462
+ return False
463
+
464
+ logger.info(f"Reported {len(actual_files or [])} actual files for task {task_id}")
465
+ return True
466
+
467
+ def requeue_task(
468
+ self,
469
+ task_id: str,
470
+ conflict_files: list[str],
471
+ conflicting_task_id: str | None = None,
472
+ previous_branch: str | None = None,
473
+ ) -> bool:
474
+ """Re-queue a task due to merge conflict.
475
+
476
+ The server will increment the retry count and reset the task to unstarted.
477
+ Returns False if max retries exceeded (409 status).
478
+
479
+ Args:
480
+ task_id: Task ID to re-queue.
481
+ conflict_files: Files that caused the conflict.
482
+ conflicting_task_id: ID of the conflicting task, if known.
483
+ previous_branch: Branch name from the failed attempt.
484
+
485
+ Returns:
486
+ True if re-queue succeeded, False if max retries exceeded or error.
487
+ """
488
+ payload: dict[str, Any] = {
489
+ "conflict_files": conflict_files,
490
+ }
491
+ if conflicting_task_id:
492
+ payload["conflicting_task_id"] = conflicting_task_id
493
+ if previous_branch:
494
+ payload["previous_branch"] = previous_branch
495
+
496
+ response = self.post(f"/tasks/{task_id}/requeue", json=payload)
497
+
498
+ if response.status_code == 409:
499
+ logger.warning(f"Task {task_id} exceeded max conflict retries")
500
+ console.print(
501
+ f"[red]Task {task_id} has exceeded max conflict retries. "
502
+ f"Manual intervention required.[/red]"
503
+ )
504
+ return False
505
+
506
+ if response.status_code not in (200, 201):
507
+ logger.warning(f"Failed to requeue task {task_id}: {response.status_code}")
508
+ return False
509
+
510
+ logger.info(f"Task {task_id} re-queued due to merge conflict")
511
+ console.print(f"[yellow]Task {task_id} re-queued due to merge conflict[/yellow]")
512
+ return True
513
+
416
514
 
417
515
  def get_priority_display(priority: int | None) -> str:
418
516
  """Get human-readable priority display.
@@ -349,6 +349,54 @@ def tasks_create(
349
349
  raise typer.Exit(1) from None
350
350
 
351
351
 
352
+ @tasks_app.command("requeue")
353
+ def tasks_requeue(
354
+ task_id: Annotated[
355
+ str,
356
+ typer.Argument(help="Task ID (UUID) to re-queue"),
357
+ ],
358
+ conflict_files: Annotated[
359
+ str,
360
+ typer.Option("--files", "-f", help="Comma-separated list of conflicting file paths"),
361
+ ],
362
+ conflicting_task_id: Annotated[
363
+ str | None,
364
+ typer.Option("--conflicting-task-id", help="ID of the conflicting task"),
365
+ ] = None,
366
+ previous_branch: Annotated[
367
+ str | None,
368
+ typer.Option("--branch", "-b", help="Branch name from the failed attempt"),
369
+ ] = None,
370
+ ) -> None:
371
+ """Re-queue a task due to merge conflict.
372
+
373
+ Resets the task to 'unstarted' so it can be retried on a fresh base.
374
+ Fails with exit code 1 if max retries exceeded (requires manual intervention).
375
+ """
376
+ from steerdev_agent.api.tasks import TasksClient
377
+
378
+ files = [f.strip() for f in conflict_files.split(",") if f.strip()]
379
+ if not files:
380
+ console.print("[red]Error: At least one conflict file is required[/red]")
381
+ raise typer.Exit(1)
382
+
383
+ with TasksClient() as client:
384
+ if not client.check_api_key():
385
+ raise typer.Exit(1)
386
+
387
+ success = client.requeue_task(
388
+ task_id=task_id,
389
+ conflict_files=files,
390
+ conflicting_task_id=conflicting_task_id,
391
+ previous_branch=previous_branch,
392
+ )
393
+
394
+ if not success:
395
+ raise typer.Exit(1)
396
+
397
+ console.print(f"[green]Task {task_id} re-queued for retry[/green]")
398
+
399
+
352
400
  # ============================================================================
353
401
  # Hooks Command Group
354
402
  # ============================================================================
@@ -151,6 +151,32 @@ def resolve_merge_flow(
151
151
  return "single_task", "single-task-merge"
152
152
 
153
153
 
154
+ async def _get_modified_files(working_dir: str | Path) -> list[str]:
155
+ """Get files modified in the current branch compared to the default branch.
156
+
157
+ Uses git diff to find files changed in the working tree and staged area.
158
+
159
+ Returns:
160
+ List of file paths relative to the repo root.
161
+ """
162
+ try:
163
+ proc = await asyncio.create_subprocess_exec(
164
+ "git",
165
+ "diff",
166
+ "--name-only",
167
+ "HEAD",
168
+ cwd=str(working_dir),
169
+ stdout=asyncio.subprocess.PIPE,
170
+ stderr=asyncio.subprocess.PIPE,
171
+ )
172
+ stdout, _ = await proc.communicate()
173
+ files = [f for f in stdout.decode().strip().split("\n") if f]
174
+ return files
175
+ except Exception:
176
+ logger.debug("Failed to get modified files via git diff", exc_info=True)
177
+ return []
178
+
179
+
154
180
  def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None) -> str:
155
181
  """Compute the git worktree name for a task execution.
156
182
 
@@ -252,6 +278,7 @@ class Runner:
252
278
  dry_run: bool = False,
253
279
  retry_config: RetryConfig | None = None,
254
280
  shutdown_event: asyncio.Event | None = None,
281
+ agent_id: str | None = None,
255
282
  ) -> None:
256
283
  """Initialize the runner.
257
284
 
@@ -273,6 +300,7 @@ class Runner:
273
300
  force_workflow_id: Workflow ID override for multi-phase execution.
274
301
  dry_run: If True, print the command without executing it.
275
302
  retry_config: Retry configuration for failed tasks.
303
+ agent_id: Database agent ID for conflict-aware scheduling.
276
304
  """
277
305
  self.project_id = project_id
278
306
  self.working_directory = Path(working_directory or Path.cwd())
@@ -296,6 +324,7 @@ class Runner:
296
324
  self._evidence_config = evidence_config or EvidenceConfig()
297
325
  self._enable_waves = enable_waves
298
326
  self._enable_canals = enable_canals
327
+ self._agent_id = agent_id
299
328
 
300
329
  # State
301
330
  self._state = RunState.STOPPED
@@ -590,11 +619,22 @@ class Runner:
590
619
  current_status = task.get("status", "")
591
620
  if current_status in ("unstarted", "backlog"):
592
621
  with TasksClient(api_key=self._api_key) as tasks_client:
593
- if tasks_client.update_task(task_id, status="started"):
594
- logger.info(f"Task {task_id} marked as started")
595
- task["status"] = "started"
596
- else:
597
- logger.warning(f"Failed to mark task {task_id} as started")
622
+ # backlog -> unstarted -> started (two-step transition)
623
+ if current_status == "backlog":
624
+ if tasks_client.update_task(task_id, status="unstarted"):
625
+ logger.info(f"Task {task_id} moved from backlog to unstarted")
626
+ task["status"] = "unstarted"
627
+ else:
628
+ logger.warning(
629
+ f"Failed to move task {task_id} from backlog to unstarted"
630
+ )
631
+
632
+ if task.get("status") == "unstarted":
633
+ if tasks_client.update_task(task_id, status="started"):
634
+ logger.info(f"Task {task_id} marked as started")
635
+ task["status"] = "started"
636
+ else:
637
+ logger.warning(f"Failed to mark task {task_id} as started")
598
638
 
599
639
  # Notify observer
600
640
  if self._observer:
@@ -762,6 +802,14 @@ class Runner:
762
802
  else:
763
803
  logger.warning(f"Failed to mark task {task_id} as completed")
764
804
 
805
+ # Report actual files modified (for conflict-aware scheduling)
806
+ try:
807
+ actual_files = await _get_modified_files(effective_working_dir)
808
+ if actual_files:
809
+ tasks_client.report_files(task_id, actual_files=actual_files)
810
+ except Exception:
811
+ logger.debug("Failed to report modified files", exc_info=True)
812
+
765
813
  if self._evidence_config.enabled and not self.dry_run:
766
814
  await self._submit_evidence_report(
767
815
  task=task,
@@ -979,7 +1027,10 @@ class Runner:
979
1027
  if self._enable_waves:
980
1028
  try:
981
1029
  task, wave_ctx, suggested_workflow_id = extract_task_and_wave_context(
982
- client.get_next_wave(project_id=self.project_id)
1030
+ client.get_next_wave(
1031
+ project_id=self.project_id,
1032
+ agent_id=self._agent_id,
1033
+ )
983
1034
  )
984
1035
  except Exception:
985
1036
  logger.debug(
@@ -988,7 +1039,10 @@ class Runner:
988
1039
  )
989
1040
 
990
1041
  if not task:
991
- task = client.get_next_task(project_id=self.project_id)
1042
+ task = client.get_next_task(
1043
+ project_id=self.project_id,
1044
+ agent_id=self._agent_id,
1045
+ )
992
1046
 
993
1047
  if not task:
994
1048
  if self._tasks_executed == 0:
@@ -20,6 +20,26 @@ Use this skill when a workflow phase tells you to merge the **current task branc
20
20
 
21
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.
22
22
 
23
+ ## Rebase Before Canal Merge
24
+
25
+ Before merging into the canal, ensure your branch is up to date:
26
+
27
+ ```bash
28
+ git fetch origin main
29
+ git merge-tree --write-tree origin/main HEAD
30
+ ```
31
+
32
+ - **If clean** (exit 0): Rebase and force-push with lease:
33
+ ```bash
34
+ git rebase origin/main
35
+ git push --force-with-lease origin HEAD
36
+ ```
37
+ - **If conflicts**: Attempt rebase. If it fails on files you did not modify, abort (`git rebase --abort`) and re-queue the task:
38
+ ```bash
39
+ steerdev tasks requeue TASK_ID --files "conflicting/file1.ts,conflicting/file2.ts" --branch "$(git branch --show-current)"
40
+ ```
41
+ Do NOT force-push to resolve canal conflicts. If re-queue fails, report as a blocker.
42
+
23
43
  ## Workflow
24
44
 
25
45
  1. Confirm the workflow phase says the branch is ready for canal integration
@@ -56,12 +56,36 @@ This fetches the task from the API and builds the branch name automatically:
56
56
 
57
57
  3. Implement the requested changes
58
58
  4. Commit with a conventional commit message that explains why the change exists
59
- 5. Push the current branch:
59
+ 5. Rebase onto the latest target branch before pushing:
60
60
 
61
61
  ```bash
62
- git push -u origin HEAD
62
+ git fetch origin main
63
63
  ```
64
64
 
65
+ Dry-run merge check (requires git 2.38+):
66
+ ```bash
67
+ git merge-tree --write-tree origin/main HEAD
68
+ ```
69
+
70
+ - **If exit code 0** (clean merge): Rebase and push:
71
+ ```bash
72
+ git rebase origin/main
73
+ git push -u origin HEAD
74
+ ```
75
+
76
+ - **If exit code non-zero** (conflicts detected): Attempt rebase:
77
+ ```bash
78
+ git rebase origin/main
79
+ ```
80
+ - If rebase succeeds after resolving conflicts in files you modified: `git push --force-with-lease origin HEAD`
81
+ - If conflicts are in files you did NOT modify: abort the rebase (`git rebase --abort`) and re-queue the task for a fresh attempt:
82
+ ```bash
83
+ steerdev tasks requeue TASK_ID --files "path/to/file1.ts,path/to/file2.ts" --branch "$(git branch --show-current)"
84
+ ```
85
+ If re-queue fails (max retries exceeded), report as a blocker using `steerdev activity report`.
86
+
87
+ **Fallback** (if `git merge-tree` is unavailable): Skip the dry-run and rebase directly. If rebase fails, abort and re-queue as above.
88
+
65
89
  6. Create the pull request:
66
90
 
67
91
  ```bash
@@ -54,12 +54,36 @@ git commit -m "[PROJ-123] feat: add user endpoint"
54
54
  git commit -m "[task:abc12345] feat: add user endpoint"
55
55
  ```
56
56
 
57
- 3. After the wave work is ready for review, push the shared branch:
57
+ 3. After the wave work is ready for review, rebase onto the latest target branch:
58
58
 
59
59
  ```bash
60
- git push -u origin HEAD
60
+ git fetch origin main
61
61
  ```
62
62
 
63
+ Dry-run merge check (requires git 2.38+):
64
+ ```bash
65
+ git merge-tree --write-tree origin/main HEAD
66
+ ```
67
+
68
+ - **If exit code 0** (clean merge): Rebase and push:
69
+ ```bash
70
+ git rebase origin/main
71
+ git push -u origin HEAD
72
+ ```
73
+
74
+ - **If exit code non-zero** (conflicts detected): Attempt rebase:
75
+ ```bash
76
+ git rebase origin/main
77
+ ```
78
+ - If rebase succeeds after resolving conflicts in files you modified: `git push --force-with-lease origin HEAD`
79
+ - If conflicts are in files you did NOT modify: abort the rebase (`git rebase --abort`) and re-queue the current task:
80
+ ```bash
81
+ steerdev tasks requeue TASK_ID --files "path/to/file1.ts,path/to/file2.ts" --branch "$(git branch --show-current)"
82
+ ```
83
+ If re-queue fails (max retries exceeded), report as a blocker using `steerdev activity report`.
84
+
85
+ **Fallback** (if `git merge-tree` is unavailable): Skip the dry-run and rebase directly. If rebase fails, abort and re-queue as above.
86
+
63
87
  4. Create one pull request for the wave:
64
88
 
65
89
  ```bash