steerdev 1.1.2__tar.gz → 1.1.5__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.2 → steerdev-1.1.5}/PKG-INFO +1 -1
- {steerdev-1.1.2 → steerdev-1.1.5}/pyproject.toml +1 -1
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/cli.py +33 -2
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/prompt/builder.py +1 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/runner.py +36 -20
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/claude_setup.py +45 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/worktree.py +2 -2
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_claude_setup.py +82 -1
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_preparation.py +3 -3
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_runner_merge_modes.py +2 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_runner_worktrees.py +4 -4
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_worktree.py +2 -2
- {steerdev-1.1.2 → steerdev-1.1.5}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/.gitignore +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/AGENTS.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/CLAUDE.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/README.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workspace/preparation.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/__init__.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_api_client.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_config.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_context_search.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_executor.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_prompt.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_retry.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_tasks.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_version.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_workspace.py +0 -0
- {steerdev-1.1.2 → steerdev-1.1.5}/tests/test_workspace_extended.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.5
|
|
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
|
|
@@ -1726,6 +1726,14 @@ def run(
|
|
|
1726
1726
|
help="Default branch for sync/rebase/PR targets (default: from config or 'main')",
|
|
1727
1727
|
),
|
|
1728
1728
|
] = None,
|
|
1729
|
+
task_id: Annotated[
|
|
1730
|
+
str | None,
|
|
1731
|
+
typer.Option(
|
|
1732
|
+
"--task-id",
|
|
1733
|
+
"-t",
|
|
1734
|
+
help="Run a specific task by ID (bypasses queue/wave scheduling)",
|
|
1735
|
+
),
|
|
1736
|
+
] = None,
|
|
1729
1737
|
) -> None:
|
|
1730
1738
|
"""Run the agent for a project!
|
|
1731
1739
|
|
|
@@ -1851,6 +1859,7 @@ def run(
|
|
|
1851
1859
|
dry_run=dry_run,
|
|
1852
1860
|
retry_config=retry_config,
|
|
1853
1861
|
branch_config=resolved_branch_config,
|
|
1862
|
+
task_id=task_id,
|
|
1854
1863
|
)
|
|
1855
1864
|
)
|
|
1856
1865
|
|
|
@@ -2341,8 +2350,15 @@ def _display_dependency_check(deps: list) -> None:
|
|
|
2341
2350
|
|
|
2342
2351
|
for dep in deps:
|
|
2343
2352
|
if dep.found:
|
|
2344
|
-
|
|
2345
|
-
|
|
2353
|
+
if dep.authenticated is False:
|
|
2354
|
+
status = "[red]Not authenticated[/red]"
|
|
2355
|
+
details = dep.auth_hint or "Run the tool's login command"
|
|
2356
|
+
elif dep.authenticated is True:
|
|
2357
|
+
status = "[green]Found & authenticated[/green]"
|
|
2358
|
+
details = dep.auth_details or dep.version or dep.path or ""
|
|
2359
|
+
else:
|
|
2360
|
+
status = "[green]Found[/green]"
|
|
2361
|
+
details = dep.version or dep.path or ""
|
|
2346
2362
|
elif dep.required:
|
|
2347
2363
|
status = "[red]Missing (required)[/red]"
|
|
2348
2364
|
details = f"Install: {dep.install_hint}"
|
|
@@ -2488,6 +2504,21 @@ def setup(
|
|
|
2488
2504
|
)
|
|
2489
2505
|
raise typer.Exit(1)
|
|
2490
2506
|
|
|
2507
|
+
unauthenticated_required = [
|
|
2508
|
+
d for d in deps if d.required and d.found and d.authenticated is False
|
|
2509
|
+
]
|
|
2510
|
+
if unauthenticated_required:
|
|
2511
|
+
names = ", ".join(d.name for d in unauthenticated_required)
|
|
2512
|
+
hints = "\n".join(
|
|
2513
|
+
f" - {d.name}: {d.auth_hint}" for d in unauthenticated_required if d.auth_hint
|
|
2514
|
+
)
|
|
2515
|
+
console.print(
|
|
2516
|
+
f"\n[red]Required tool(s) not authenticated: {names}.[/red]\n"
|
|
2517
|
+
f"Log in before running setup so steerdev can create PRs and "
|
|
2518
|
+
f"manage branches on your behalf.\n{hints}"
|
|
2519
|
+
)
|
|
2520
|
+
raise typer.Exit(1)
|
|
2521
|
+
|
|
2491
2522
|
# Prompt for install target if not provided
|
|
2492
2523
|
if install_target is None:
|
|
2493
2524
|
install_target = _prompt_install_target()
|
|
@@ -24,6 +24,7 @@ class WaveContext(BaseModel):
|
|
|
24
24
|
|
|
25
25
|
wave_number: int = Field(description="Current wave number")
|
|
26
26
|
total_waves: int = Field(description="Total number of waves")
|
|
27
|
+
wave_name: str = Field(default="", description="Unique wave name (e.g., wave-532d5c)")
|
|
27
28
|
wave_description: str = Field(default="", description="Description of this wave")
|
|
28
29
|
wave_tasks_summary: str = Field(default="", description="Summary of tasks in this wave")
|
|
29
30
|
completed_waves_summary: str = Field(default="", description="Summary of completed waves")
|
|
@@ -111,6 +111,7 @@ def build_wave_context(wave_response: dict[str, Any]) -> WaveContext | None:
|
|
|
111
111
|
return WaveContext(
|
|
112
112
|
wave_number=wave_info.get("wave_number", 1),
|
|
113
113
|
total_waves=wave_info.get("total_waves", 1),
|
|
114
|
+
wave_name=wave_info.get("name", ""),
|
|
114
115
|
wave_description=wave_info.get("description", ""),
|
|
115
116
|
wave_tasks_summary=tasks_summary,
|
|
116
117
|
completed_waves_summary=completed_summary,
|
|
@@ -187,13 +188,13 @@ def compute_worktree_name(task_id: str, wave_context: WaveContext | None = None)
|
|
|
187
188
|
"""Compute the git worktree name for a task execution.
|
|
188
189
|
|
|
189
190
|
Naming conventions:
|
|
190
|
-
- Wave-based: "
|
|
191
|
+
- Wave-based: "{wave_name}" (e.g., "wave-532d5c")
|
|
191
192
|
- Task-based: "task-{first_8_chars}" (e.g., "task-abc12345")
|
|
192
193
|
|
|
193
194
|
The worktree lifecycle is managed by Claude CLI via the --worktree flag.
|
|
194
195
|
"""
|
|
195
196
|
if wave_context:
|
|
196
|
-
return f"wave-{wave_context.wave_number}"
|
|
197
|
+
return wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
197
198
|
task_short = task_id[:8] if len(task_id) > 8 else task_id
|
|
198
199
|
return f"task-{task_short}"
|
|
199
200
|
|
|
@@ -286,6 +287,7 @@ class Runner:
|
|
|
286
287
|
branch_config: BranchConfig | None = None,
|
|
287
288
|
shutdown_event: asyncio.Event | None = None,
|
|
288
289
|
agent_id: str | None = None,
|
|
290
|
+
task_id: str | None = None,
|
|
289
291
|
) -> None:
|
|
290
292
|
"""Initialize the runner.
|
|
291
293
|
|
|
@@ -333,6 +335,7 @@ class Runner:
|
|
|
333
335
|
self._enable_waves = enable_waves
|
|
334
336
|
self._enable_canals = enable_canals
|
|
335
337
|
self._agent_id = agent_id
|
|
338
|
+
self._target_task_id = task_id
|
|
336
339
|
|
|
337
340
|
# State
|
|
338
341
|
self._state = RunState.STOPPED
|
|
@@ -643,7 +646,7 @@ class Runner:
|
|
|
643
646
|
|
|
644
647
|
wave_id: str | None = None
|
|
645
648
|
if wave_context:
|
|
646
|
-
wave_id = f"wave-{wave_context.wave_number}"
|
|
649
|
+
wave_id = wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
647
650
|
|
|
648
651
|
try:
|
|
649
652
|
with ReportsClient(api_key=self._api_key) as client:
|
|
@@ -1123,25 +1126,36 @@ class Runner:
|
|
|
1123
1126
|
suggested_workflow_id: str | None = None
|
|
1124
1127
|
|
|
1125
1128
|
with TasksClient(api_key=self._api_key) as client:
|
|
1126
|
-
if self.
|
|
1127
|
-
|
|
1128
|
-
task
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1129
|
+
if self._target_task_id:
|
|
1130
|
+
if self._tasks_executed > 0:
|
|
1131
|
+
# Targeted single-task mode: only run the requested task once.
|
|
1132
|
+
break
|
|
1133
|
+
task = client.get_task(self._target_task_id)
|
|
1134
|
+
if not task:
|
|
1135
|
+
console.print(f"[red]Task {self._target_task_id} not found[/red]")
|
|
1136
|
+
break
|
|
1137
|
+
else:
|
|
1138
|
+
if self._enable_waves:
|
|
1139
|
+
try:
|
|
1140
|
+
task, wave_ctx, suggested_workflow_id = (
|
|
1141
|
+
extract_task_and_wave_context(
|
|
1142
|
+
client.get_next_wave(
|
|
1143
|
+
project_id=self.project_id,
|
|
1144
|
+
agent_id=self._agent_id,
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
)
|
|
1148
|
+
except Exception:
|
|
1149
|
+
logger.debug(
|
|
1150
|
+
"Wave fetch failed, falling back to single-task",
|
|
1151
|
+
exc_info=True,
|
|
1132
1152
|
)
|
|
1133
|
-
)
|
|
1134
|
-
except Exception:
|
|
1135
|
-
logger.debug(
|
|
1136
|
-
"Wave fetch failed, falling back to single-task",
|
|
1137
|
-
exc_info=True,
|
|
1138
|
-
)
|
|
1139
1153
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1154
|
+
if not task:
|
|
1155
|
+
task = client.get_next_task(
|
|
1156
|
+
project_id=self.project_id,
|
|
1157
|
+
agent_id=self._agent_id,
|
|
1158
|
+
)
|
|
1145
1159
|
|
|
1146
1160
|
if not task:
|
|
1147
1161
|
if self._tasks_executed == 0:
|
|
@@ -1394,6 +1408,7 @@ async def run_agent(
|
|
|
1394
1408
|
dry_run: bool = False,
|
|
1395
1409
|
retry_config: RetryConfig | None = None,
|
|
1396
1410
|
branch_config: BranchConfig | None = None,
|
|
1411
|
+
task_id: str | None = None,
|
|
1397
1412
|
) -> dict[str, Any]:
|
|
1398
1413
|
"""Run the steerdev agent.
|
|
1399
1414
|
|
|
@@ -1441,6 +1456,7 @@ async def run_agent(
|
|
|
1441
1456
|
dry_run=dry_run,
|
|
1442
1457
|
retry_config=retry_config,
|
|
1443
1458
|
branch_config=branch_config,
|
|
1459
|
+
task_id=task_id,
|
|
1444
1460
|
)
|
|
1445
1461
|
|
|
1446
1462
|
# Install signal handler that kills the subprocess immediately on Ctrl+C
|
|
@@ -30,6 +30,11 @@ class DependencyStatus:
|
|
|
30
30
|
version: str | None = None
|
|
31
31
|
required: bool = True
|
|
32
32
|
install_hint: str = ""
|
|
33
|
+
# Auth status: True if authenticated, False if not, None if not applicable
|
|
34
|
+
# (or the binary was not found, or the check could not be performed).
|
|
35
|
+
authenticated: bool | None = None
|
|
36
|
+
auth_hint: str = ""
|
|
37
|
+
auth_details: str | None = None
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
def check_cli_dependencies() -> list[DependencyStatus]:
|
|
@@ -75,6 +80,15 @@ def check_cli_dependencies() -> list[DependencyStatus]:
|
|
|
75
80
|
install_hint="https://nodejs.org/",
|
|
76
81
|
),
|
|
77
82
|
]
|
|
83
|
+
|
|
84
|
+
for dep in deps:
|
|
85
|
+
if dep.command == "gh" and dep.found and dep.path is not None:
|
|
86
|
+
authenticated, details = _check_gh_auth(dep.path)
|
|
87
|
+
dep.authenticated = authenticated
|
|
88
|
+
dep.auth_details = details
|
|
89
|
+
if authenticated is False:
|
|
90
|
+
dep.auth_hint = "Run: gh auth login"
|
|
91
|
+
|
|
78
92
|
return deps
|
|
79
93
|
|
|
80
94
|
|
|
@@ -125,6 +139,37 @@ def _check_dep(
|
|
|
125
139
|
)
|
|
126
140
|
|
|
127
141
|
|
|
142
|
+
def _check_gh_auth(gh_path: str) -> tuple[bool | None, str | None]:
|
|
143
|
+
"""Check whether the GitHub CLI is logged in.
|
|
144
|
+
|
|
145
|
+
Returns a tuple of (authenticated, details). `authenticated` is True when
|
|
146
|
+
`gh auth status` exits 0, False when it exits non-zero (meaning gh is
|
|
147
|
+
installed but the user isn't logged in), and None when the check could
|
|
148
|
+
not be executed (timeout / OS error).
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
[gh_path, "auth", "status"],
|
|
153
|
+
capture_output=True,
|
|
154
|
+
text=True,
|
|
155
|
+
timeout=5,
|
|
156
|
+
)
|
|
157
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
158
|
+
logger.debug("Failed to run `gh auth status`")
|
|
159
|
+
return None, None
|
|
160
|
+
|
|
161
|
+
# `gh auth status` writes its human-readable output to stderr.
|
|
162
|
+
combined = (result.stderr or "") + (result.stdout or "")
|
|
163
|
+
details: str | None = None
|
|
164
|
+
for line in combined.splitlines():
|
|
165
|
+
stripped = line.strip()
|
|
166
|
+
if "Logged in to" in stripped or "not logged in" in stripped.lower():
|
|
167
|
+
details = stripped
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
return result.returncode == 0, details
|
|
171
|
+
|
|
172
|
+
|
|
128
173
|
# Word lists for generating agent names
|
|
129
174
|
ADJECTIVES = [
|
|
130
175
|
"bright",
|
|
@@ -213,7 +213,7 @@ def compute_branch_name(
|
|
|
213
213
|
"""Compute the branch name for a task or wave.
|
|
214
214
|
|
|
215
215
|
Naming conventions aligned with merge skill branch patterns:
|
|
216
|
-
- Wave: "
|
|
216
|
+
- Wave: "{wave_name}" (e.g., "wave-532d5c")
|
|
217
217
|
- Task with external ID: "task/{TICKET-ID}-{slug}" (e.g., "task/PROJ-123-add-auth")
|
|
218
218
|
- Task without external ID: "task/{short-id}-{slug}" (e.g., "task/abc12345-add-auth")
|
|
219
219
|
|
|
@@ -225,7 +225,7 @@ def compute_branch_name(
|
|
|
225
225
|
Branch name string.
|
|
226
226
|
"""
|
|
227
227
|
if wave_context:
|
|
228
|
-
return f"wave
|
|
228
|
+
return wave_context.wave_name or f"wave-{wave_context.wave_number}"
|
|
229
229
|
|
|
230
230
|
# Try external ticket ID first (e.g., Linear "PROJ-123")
|
|
231
231
|
external_id = task.get("linear_identifier") or task.get("external_id")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tests for Claude setup templates and installed skills."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import subprocess
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
4
5
|
|
|
5
6
|
from steerdev_agent.setup.claude_setup import ClaudeSetup, check_cli_dependencies
|
|
6
7
|
|
|
@@ -86,3 +87,83 @@ class TestCheckCliDependencies:
|
|
|
86
87
|
if git_dep.found:
|
|
87
88
|
assert git_dep.path is not None
|
|
88
89
|
assert git_dep.version is not None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestGhAuthStatus:
|
|
93
|
+
"""Tests for gh auth status checking within check_cli_dependencies()."""
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _fake_run(*, auth_returncode: int, auth_stderr: str = ""):
|
|
97
|
+
"""Build a fake subprocess.run that returns a version then an auth result."""
|
|
98
|
+
|
|
99
|
+
def run(cmd, *args, **kwargs):
|
|
100
|
+
# First subprocess call is `--version`; second is `auth status`.
|
|
101
|
+
if "auth" in cmd:
|
|
102
|
+
return MagicMock(returncode=auth_returncode, stdout="", stderr=auth_stderr)
|
|
103
|
+
return MagicMock(returncode=0, stdout="gh version 2.0.0\n", stderr="")
|
|
104
|
+
|
|
105
|
+
return run
|
|
106
|
+
|
|
107
|
+
def test_gh_authenticated_sets_flag_true(self):
|
|
108
|
+
with (
|
|
109
|
+
patch("shutil.which", return_value="/usr/bin/gh"),
|
|
110
|
+
patch(
|
|
111
|
+
"subprocess.run",
|
|
112
|
+
side_effect=self._fake_run(
|
|
113
|
+
auth_returncode=0,
|
|
114
|
+
auth_stderr="✓ Logged in to github.com as octocat",
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
):
|
|
118
|
+
deps = check_cli_dependencies()
|
|
119
|
+
|
|
120
|
+
gh_dep = next(d for d in deps if d.command == "gh")
|
|
121
|
+
assert gh_dep.found is True
|
|
122
|
+
assert gh_dep.authenticated is True
|
|
123
|
+
assert gh_dep.auth_hint == ""
|
|
124
|
+
assert gh_dep.auth_details is not None
|
|
125
|
+
assert "Logged in" in gh_dep.auth_details
|
|
126
|
+
|
|
127
|
+
def test_gh_not_authenticated_sets_flag_false_with_hint(self):
|
|
128
|
+
with (
|
|
129
|
+
patch("shutil.which", return_value="/usr/bin/gh"),
|
|
130
|
+
patch(
|
|
131
|
+
"subprocess.run",
|
|
132
|
+
side_effect=self._fake_run(
|
|
133
|
+
auth_returncode=1,
|
|
134
|
+
auth_stderr="You are not logged into any GitHub hosts.",
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
):
|
|
138
|
+
deps = check_cli_dependencies()
|
|
139
|
+
|
|
140
|
+
gh_dep = next(d for d in deps if d.command == "gh")
|
|
141
|
+
assert gh_dep.found is True
|
|
142
|
+
assert gh_dep.authenticated is False
|
|
143
|
+
assert "gh auth login" in gh_dep.auth_hint
|
|
144
|
+
|
|
145
|
+
def test_gh_auth_check_timeout_leaves_status_none(self):
|
|
146
|
+
def run(cmd, *args, **kwargs):
|
|
147
|
+
if "auth" in cmd:
|
|
148
|
+
raise subprocess.TimeoutExpired(cmd=cmd, timeout=5)
|
|
149
|
+
return MagicMock(returncode=0, stdout="gh version 2.0.0\n", stderr="")
|
|
150
|
+
|
|
151
|
+
with (
|
|
152
|
+
patch("shutil.which", return_value="/usr/bin/gh"),
|
|
153
|
+
patch("subprocess.run", side_effect=run),
|
|
154
|
+
):
|
|
155
|
+
deps = check_cli_dependencies()
|
|
156
|
+
|
|
157
|
+
gh_dep = next(d for d in deps if d.command == "gh")
|
|
158
|
+
assert gh_dep.found is True
|
|
159
|
+
assert gh_dep.authenticated is None
|
|
160
|
+
assert gh_dep.auth_hint == ""
|
|
161
|
+
|
|
162
|
+
def test_missing_gh_does_not_run_auth_check(self):
|
|
163
|
+
# When gh isn't installed, authenticated should stay None (not False).
|
|
164
|
+
with patch("shutil.which", return_value=None):
|
|
165
|
+
deps = check_cli_dependencies()
|
|
166
|
+
|
|
167
|
+
gh_dep = next(d for d in deps if d.command == "gh")
|
|
168
|
+
assert gh_dep.found is False
|
|
169
|
+
assert gh_dep.authenticated is None
|
|
@@ -211,7 +211,7 @@ class TestWorkspacePreparationInPlace:
|
|
|
211
211
|
"""Continuing a wave stays on the wave branch, only fetches."""
|
|
212
212
|
from steerdev_agent.prompt.builder import WaveContext
|
|
213
213
|
|
|
214
|
-
wave_ctx = WaveContext(wave_number=3, total_waves=5)
|
|
214
|
+
wave_ctx = WaveContext(wave_number=3, total_waves=5, wave_name="wave-a1b2c3")
|
|
215
215
|
|
|
216
216
|
prep = WorkspacePreparation(
|
|
217
217
|
working_directory=Path("/tmp/repo"),
|
|
@@ -227,7 +227,7 @@ class TestWorkspacePreparationInPlace:
|
|
|
227
227
|
if "rev-parse --is-inside-work-tree" in cmd:
|
|
228
228
|
return (0, "true", "")
|
|
229
229
|
if "branch --show-current" in cmd:
|
|
230
|
-
return (0, "wave
|
|
230
|
+
return (0, "wave-a1b2c3", "") # Already on wave branch
|
|
231
231
|
if "fetch origin" in cmd:
|
|
232
232
|
return (0, "", "")
|
|
233
233
|
return (0, "", "")
|
|
@@ -236,7 +236,7 @@ class TestWorkspacePreparationInPlace:
|
|
|
236
236
|
result = await prep.prepare(sample_task, wave_ctx)
|
|
237
237
|
|
|
238
238
|
assert result.success is True
|
|
239
|
-
assert result.branch_name == "wave
|
|
239
|
+
assert result.branch_name == "wave-a1b2c3"
|
|
240
240
|
# Should NOT have checkout commands (stayed on branch)
|
|
241
241
|
checkout_cmds = [c for c in calls if c[0] == "checkout"]
|
|
242
242
|
assert len(checkout_cmds) == 0
|
|
@@ -12,6 +12,7 @@ def _make_wave_context() -> WaveContext:
|
|
|
12
12
|
return WaveContext(
|
|
13
13
|
wave_number=3,
|
|
14
14
|
total_waves=5,
|
|
15
|
+
wave_name="wave-a1b2c3",
|
|
15
16
|
wave_description="User management improvements",
|
|
16
17
|
wave_tasks_summary=" [todo] Add user endpoint",
|
|
17
18
|
completed_waves_summary=" - Wave 2: Auth cleanup",
|
|
@@ -23,6 +24,7 @@ def _make_wave_response(task_id: str = "task-123") -> dict:
|
|
|
23
24
|
"wave": {
|
|
24
25
|
"wave_number": 3,
|
|
25
26
|
"total_waves": 5,
|
|
27
|
+
"name": "wave-a1b2c3",
|
|
26
28
|
"description": "User management improvements",
|
|
27
29
|
},
|
|
28
30
|
"context": {
|
|
@@ -25,13 +25,13 @@ class TestComputeBranchName:
|
|
|
25
25
|
|
|
26
26
|
def test_wave_based(self):
|
|
27
27
|
task = {"id": "abc123", "title": "Something"}
|
|
28
|
-
wave = WaveContext(wave_number=3, total_waves=5)
|
|
29
|
-
assert compute_branch_name(task, wave) == "wave
|
|
28
|
+
wave = WaveContext(wave_number=3, total_waves=5, wave_name="wave-a1b2c3")
|
|
29
|
+
assert compute_branch_name(task, wave) == "wave-a1b2c3"
|
|
30
30
|
|
|
31
31
|
def test_wave_takes_precedence(self):
|
|
32
32
|
task = {"id": "abc12345xyz", "title": "Add auth", "linear_identifier": "PROJ-123"}
|
|
33
|
-
wave = WaveContext(wave_number=2, total_waves=4)
|
|
34
|
-
assert compute_branch_name(task, wave) == "wave
|
|
33
|
+
wave = WaveContext(wave_number=2, total_waves=4, wave_name="wave-f1e2d3")
|
|
34
|
+
assert compute_branch_name(task, wave) == "wave-f1e2d3"
|
|
35
35
|
|
|
36
36
|
def test_title_slugified(self):
|
|
37
37
|
task = {"id": "abc12345xyz", "title": "Add User Authentication & OAuth 2.0"}
|
|
@@ -37,8 +37,8 @@ class TestComputeBranchName:
|
|
|
37
37
|
from steerdev_agent.prompt.builder import WaveContext
|
|
38
38
|
|
|
39
39
|
task = {"id": "abc", "title": "test"}
|
|
40
|
-
wave = WaveContext(wave_number=5, total_waves=10)
|
|
41
|
-
assert compute_branch_name(task, wave) == "wave
|
|
40
|
+
wave = WaveContext(wave_number=5, total_waves=10, wave_name="wave-d4e5f6")
|
|
41
|
+
assert compute_branch_name(task, wave) == "wave-d4e5f6"
|
|
42
42
|
|
|
43
43
|
def test_task_with_linear_id(self):
|
|
44
44
|
task = {"id": "abc12345xyz", "title": "Add auth", "linear_identifier": "PROJ-42"}
|
|
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.2 → steerdev-1.1.5}/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
|
|
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
|