steerdev 1.1.45__tar.gz → 1.1.48__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.45 → steerdev-1.1.48}/PKG-INFO +1 -1
- {steerdev-1.1.45 → steerdev-1.1.48}/pyproject.toml +1 -1
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/agent_loop.py +99 -1
- steerdev-1.1.48/tests/test_agent_loop_wave_pr.py +180 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/.github/workflows/publish.yml +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/.gitignore +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/.pre-commit-config.yaml +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/AGENTS.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/CLAUDE.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/README.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/docs/superpowers/plans/2026-06-05-wave-id-implies-unlimited-max-tasks.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/docs/superpowers/specs/2026-06-05-wave-id-implies-unlimited-max-tasks-design.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/merger.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/cli.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/git/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/git/commit.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/git/pr.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/git_ops.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/runner.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workspace/preparation.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/__init__.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_agent_loop.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_agents_api.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_api_client.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_claude_executor.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_claude_setup.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_client_methods.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_commands_api.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_config.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_config_extended.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_context_search.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_executor.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_git_commit.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_git_ops.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_git_pr.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_hooks_post_tool_use.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_platform_config.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_preparation.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_preparation_provenance.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_prompt.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_reports_client.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_resolve_executor_config.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_resolve_max_tasks.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_retry.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_runner_boundary_commit.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_runner_wave_pr.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_runner_worktrees.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_stream_parser.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_tasks.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_tasks_update_branch.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_version.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_wave_scoped_fetch.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_workflow_context.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_workflow_runs_api.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_workspace.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.1.45 → steerdev-1.1.48}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerdev
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.48
|
|
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
|
|
@@ -18,8 +18,9 @@ import asyncio
|
|
|
18
18
|
import contextlib
|
|
19
19
|
import signal
|
|
20
20
|
import socket
|
|
21
|
+
from dataclasses import dataclass, field
|
|
21
22
|
from pathlib import Path
|
|
22
|
-
from typing import TYPE_CHECKING
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
23
24
|
|
|
24
25
|
import httpx
|
|
25
26
|
from loguru import logger
|
|
@@ -28,6 +29,7 @@ from rich.panel import Panel
|
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
30
31
|
from steerdev_agent.api.agents import AgentsClient, ProjectAssignment
|
|
32
|
+
from steerdev_agent.prompt.builder import WaveContext
|
|
31
33
|
from steerdev_agent.workspace.project_manager import ProjectManager
|
|
32
34
|
|
|
33
35
|
from steerdev_agent.api.commands import CommandResponse, CommandsClient
|
|
@@ -54,6 +56,15 @@ class AgentLoopError(Exception):
|
|
|
54
56
|
"""Error during agent loop execution."""
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
@dataclass
|
|
60
|
+
class _WavePrTracking:
|
|
61
|
+
"""Per-project wave PR safety-net state (mirrors Runner.run's tracking)."""
|
|
62
|
+
|
|
63
|
+
wave_ctx: WaveContext
|
|
64
|
+
completed_tasks: list[dict[str, Any]] = field(default_factory=list)
|
|
65
|
+
working_dir: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
57
68
|
# ---------------------------------------------------------------------------
|
|
58
69
|
# Shared execution logic
|
|
59
70
|
# ---------------------------------------------------------------------------
|
|
@@ -92,6 +103,7 @@ class CommandExecutor:
|
|
|
92
103
|
_commands_succeeded: int
|
|
93
104
|
_commands_failed: int
|
|
94
105
|
_is_busy: bool
|
|
106
|
+
_wave_pr_state: dict[str, _WavePrTracking]
|
|
95
107
|
|
|
96
108
|
async def _execute_command(
|
|
97
109
|
self,
|
|
@@ -273,6 +285,90 @@ class CommandExecutor:
|
|
|
273
285
|
finally:
|
|
274
286
|
await runs_client.close()
|
|
275
287
|
|
|
288
|
+
# Wave PR safety net (mirrors Runner.run): once the wave has no more
|
|
289
|
+
# pending tasks, make sure its PR exists.
|
|
290
|
+
await self._maybe_ensure_wave_pr(
|
|
291
|
+
project_id,
|
|
292
|
+
task,
|
|
293
|
+
wave_ctx,
|
|
294
|
+
result,
|
|
295
|
+
fallback_working_dir=str(command.working_directory or working_directory),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
async def _maybe_ensure_wave_pr(
|
|
299
|
+
self,
|
|
300
|
+
project_id: str,
|
|
301
|
+
task: dict[str, Any],
|
|
302
|
+
wave_ctx: WaveContext | None,
|
|
303
|
+
result: dict[str, Any],
|
|
304
|
+
fallback_working_dir: str,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Track completed wave tasks across commands and create the wave PR.
|
|
307
|
+
|
|
308
|
+
Command-driven counterpart of Runner.run's wave PR safety net: each
|
|
309
|
+
command executes a single task, so wave completion is detected by
|
|
310
|
+
peeking at the wave queue after every successful wave task — no task
|
|
311
|
+
left (or scheduling moved to a different wave) means the wave is done
|
|
312
|
+
and its PR must exist. Best-effort: never raises.
|
|
313
|
+
"""
|
|
314
|
+
if wave_ctx is None or not result.get("success"):
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
from steerdev_agent.runner import extract_task_and_wave_context, wave_identity
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
state = self._wave_pr_state.get(project_id)
|
|
321
|
+
if state is not None and wave_identity(state.wave_ctx) != wave_identity(wave_ctx):
|
|
322
|
+
# Scheduling moved on to a new wave: the tracked one completed.
|
|
323
|
+
self._flush_wave_pr(project_id, fallback_working_dir)
|
|
324
|
+
state = None
|
|
325
|
+
if state is None:
|
|
326
|
+
state = _WavePrTracking(wave_ctx=wave_ctx)
|
|
327
|
+
self._wave_pr_state[project_id] = state
|
|
328
|
+
state.completed_tasks.append({"id": task.get("id"), "title": task.get("title")})
|
|
329
|
+
if result.get("working_dir"):
|
|
330
|
+
state.working_dir = result["working_dir"]
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
with TasksClient(api_key=self._api_key) as tasks_client:
|
|
334
|
+
wave_response = tasks_client.get_next_wave(
|
|
335
|
+
project_id=project_id, agent_id=self._agent_id
|
|
336
|
+
)
|
|
337
|
+
next_task, next_wave_ctx, _ = extract_task_and_wave_context(wave_response)
|
|
338
|
+
except Exception:
|
|
339
|
+
logger.debug("Wave PR safety net: wave peek failed, skipping", exc_info=True)
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
next_task is None
|
|
344
|
+
or next_wave_ctx is None
|
|
345
|
+
or wave_identity(next_wave_ctx) != wave_identity(wave_ctx)
|
|
346
|
+
):
|
|
347
|
+
self._flush_wave_pr(project_id, fallback_working_dir)
|
|
348
|
+
except Exception: # broad catch intentional: safety net must never break the loop
|
|
349
|
+
logger.warning("Wave PR safety net failed (non-fatal)", exc_info=True)
|
|
350
|
+
|
|
351
|
+
def _flush_wave_pr(self, project_id: str, fallback_working_dir: str) -> None:
|
|
352
|
+
"""Create the tracked wave's PR if the agent didn't. Never raises."""
|
|
353
|
+
state = self._wave_pr_state.pop(project_id, None)
|
|
354
|
+
if state is None:
|
|
355
|
+
return
|
|
356
|
+
try:
|
|
357
|
+
from steerdev_agent.git.pr import ensure_wave_pr
|
|
358
|
+
|
|
359
|
+
status = ensure_wave_pr(
|
|
360
|
+
state.working_dir or fallback_working_dir,
|
|
361
|
+
state.wave_ctx,
|
|
362
|
+
state.completed_tasks,
|
|
363
|
+
api_key=self._api_key,
|
|
364
|
+
)
|
|
365
|
+
if status == "created":
|
|
366
|
+
console.print("[green]Wave PR created (safety net)[/green]")
|
|
367
|
+
else:
|
|
368
|
+
logger.info(f"Wave PR safety net: {status}")
|
|
369
|
+
except Exception: # broad catch intentional: safety net must never break the loop
|
|
370
|
+
logger.warning("Wave PR safety net failed (non-fatal)", exc_info=True)
|
|
371
|
+
|
|
276
372
|
async def _execute_prompt_command(
|
|
277
373
|
self,
|
|
278
374
|
command: CommandResponse,
|
|
@@ -530,6 +626,7 @@ class AgentLoop(CommandExecutor):
|
|
|
530
626
|
self._commands_executed = 0
|
|
531
627
|
self._commands_succeeded = 0
|
|
532
628
|
self._commands_failed = 0
|
|
629
|
+
self._wave_pr_state: dict[str, _WavePrTracking] = {}
|
|
533
630
|
|
|
534
631
|
# Components
|
|
535
632
|
self._commands_client: CommandsClient | None = None
|
|
@@ -921,6 +1018,7 @@ class WorkspaceAgentLoop(CommandExecutor):
|
|
|
921
1018
|
self._commands_executed = 0
|
|
922
1019
|
self._commands_succeeded = 0
|
|
923
1020
|
self._commands_failed = 0
|
|
1021
|
+
self._wave_pr_state: dict[str, _WavePrTracking] = {}
|
|
924
1022
|
|
|
925
1023
|
# Components
|
|
926
1024
|
self._commands_client: CommandsClient | None = None
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for the agent loop's wave PR safety net (command-driven counterpart
|
|
2
|
+
of the runner's Runner.run safety net)."""
|
|
3
|
+
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from steerdev_agent.agent_loop import AgentLoop, _WavePrTracking
|
|
9
|
+
from tests.test_runner_merge_modes import _make_wave_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_loop() -> AgentLoop:
|
|
13
|
+
return AgentLoop(project_id="p", agent_name="a", api_key="k")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_wave_response(wave_name: str = "wave-a1b2c3", next_task: dict | None = None) -> dict:
|
|
17
|
+
return {
|
|
18
|
+
"wave": {"wave_number": 3, "total_waves": 5, "name": wave_name},
|
|
19
|
+
"context": {"next_task": next_task},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _patch_wave_peek(wave_response):
|
|
24
|
+
"""Patch TasksClient so get_next_wave returns the given response."""
|
|
25
|
+
client = MagicMock()
|
|
26
|
+
client.__enter__.return_value = client
|
|
27
|
+
client.get_next_wave.return_value = wave_response
|
|
28
|
+
return patch("steerdev_agent.agent_loop.TasksClient", return_value=client)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestMaybeEnsureWavePr:
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_noop_without_wave_context(self):
|
|
34
|
+
loop = _make_loop()
|
|
35
|
+
with patch("steerdev_agent.git.pr.ensure_wave_pr") as ensure:
|
|
36
|
+
await loop._maybe_ensure_wave_pr("p", {"id": "t1"}, None, {"success": True}, "/repo")
|
|
37
|
+
ensure.assert_not_called()
|
|
38
|
+
assert loop._wave_pr_state == {}
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_noop_on_failed_task(self):
|
|
42
|
+
loop = _make_loop()
|
|
43
|
+
with patch("steerdev_agent.git.pr.ensure_wave_pr") as ensure:
|
|
44
|
+
await loop._maybe_ensure_wave_pr(
|
|
45
|
+
"p", {"id": "t1"}, _make_wave_context(), {"success": False}, "/repo"
|
|
46
|
+
)
|
|
47
|
+
ensure.assert_not_called()
|
|
48
|
+
assert loop._wave_pr_state == {}
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_accumulates_while_wave_has_pending_tasks(self):
|
|
52
|
+
loop = _make_loop()
|
|
53
|
+
ctx = _make_wave_context()
|
|
54
|
+
# Peek returns another task in the same wave → wave not done yet.
|
|
55
|
+
peek = _make_wave_response(next_task={"id": "t2", "title": "next"})
|
|
56
|
+
with (
|
|
57
|
+
_patch_wave_peek(peek),
|
|
58
|
+
patch("steerdev_agent.git.pr.ensure_wave_pr") as ensure,
|
|
59
|
+
):
|
|
60
|
+
await loop._maybe_ensure_wave_pr(
|
|
61
|
+
"p", {"id": "t1", "title": "do work"}, ctx, {"success": True}, "/repo"
|
|
62
|
+
)
|
|
63
|
+
ensure.assert_not_called()
|
|
64
|
+
assert loop._wave_pr_state["p"].completed_tasks == [{"id": "t1", "title": "do work"}]
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_creates_pr_when_wave_exhausted(self):
|
|
68
|
+
loop = _make_loop()
|
|
69
|
+
ctx = _make_wave_context()
|
|
70
|
+
with (
|
|
71
|
+
_patch_wave_peek(None),
|
|
72
|
+
patch("steerdev_agent.git.pr.ensure_wave_pr", return_value="created") as ensure,
|
|
73
|
+
):
|
|
74
|
+
await loop._maybe_ensure_wave_pr(
|
|
75
|
+
"p",
|
|
76
|
+
{"id": "t1", "title": "do work"},
|
|
77
|
+
ctx,
|
|
78
|
+
{"success": True, "working_dir": "/repo/.worktrees/wave-a1b2c3"},
|
|
79
|
+
"/repo",
|
|
80
|
+
)
|
|
81
|
+
ensure.assert_called_once_with(
|
|
82
|
+
"/repo/.worktrees/wave-a1b2c3",
|
|
83
|
+
ctx,
|
|
84
|
+
[{"id": "t1", "title": "do work"}],
|
|
85
|
+
api_key="k",
|
|
86
|
+
)
|
|
87
|
+
# State is reset so the next wave starts clean.
|
|
88
|
+
assert loop._wave_pr_state == {}
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_creates_pr_when_scheduling_moves_to_next_wave(self):
|
|
92
|
+
loop = _make_loop()
|
|
93
|
+
ctx = _make_wave_context()
|
|
94
|
+
peek = _make_wave_response(wave_name="wave-next", next_task={"id": "t9"})
|
|
95
|
+
with (
|
|
96
|
+
_patch_wave_peek(peek),
|
|
97
|
+
patch("steerdev_agent.git.pr.ensure_wave_pr", return_value="created") as ensure,
|
|
98
|
+
):
|
|
99
|
+
await loop._maybe_ensure_wave_pr(
|
|
100
|
+
"p", {"id": "t1", "title": "do work"}, ctx, {"success": True}, "/repo"
|
|
101
|
+
)
|
|
102
|
+
ensure.assert_called_once()
|
|
103
|
+
assert loop._wave_pr_state == {}
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_flushes_previous_wave_when_new_wave_task_completes(self):
|
|
107
|
+
loop = _make_loop()
|
|
108
|
+
old_ctx = _make_wave_context()
|
|
109
|
+
new_ctx = _make_wave_context()
|
|
110
|
+
new_ctx.wave_name = "wave-next"
|
|
111
|
+
loop._wave_pr_state["p"] = _WavePrTracking(
|
|
112
|
+
wave_ctx=old_ctx, completed_tasks=[{"id": "t0", "title": "old"}]
|
|
113
|
+
)
|
|
114
|
+
# Peek says the new wave still has tasks → only the old wave flushes.
|
|
115
|
+
peek = _make_wave_response(wave_name="wave-next", next_task={"id": "t9"})
|
|
116
|
+
with (
|
|
117
|
+
_patch_wave_peek(peek),
|
|
118
|
+
patch("steerdev_agent.git.pr.ensure_wave_pr", return_value="created") as ensure,
|
|
119
|
+
):
|
|
120
|
+
await loop._maybe_ensure_wave_pr(
|
|
121
|
+
"p", {"id": "t1", "title": "new work"}, new_ctx, {"success": True}, "/repo"
|
|
122
|
+
)
|
|
123
|
+
ensure.assert_called_once_with(
|
|
124
|
+
"/repo", old_ctx, [{"id": "t0", "title": "old"}], api_key="k"
|
|
125
|
+
)
|
|
126
|
+
assert loop._wave_pr_state["p"].wave_ctx is new_ctx
|
|
127
|
+
assert loop._wave_pr_state["p"].completed_tasks == [{"id": "t1", "title": "new work"}]
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_peek_failure_keeps_state_and_skips(self):
|
|
131
|
+
loop = _make_loop()
|
|
132
|
+
ctx = _make_wave_context()
|
|
133
|
+
with (
|
|
134
|
+
patch("steerdev_agent.agent_loop.TasksClient", side_effect=RuntimeError("api down")),
|
|
135
|
+
patch("steerdev_agent.git.pr.ensure_wave_pr") as ensure,
|
|
136
|
+
):
|
|
137
|
+
await loop._maybe_ensure_wave_pr(
|
|
138
|
+
"p", {"id": "t1", "title": "do work"}, ctx, {"success": True}, "/repo"
|
|
139
|
+
)
|
|
140
|
+
ensure.assert_not_called()
|
|
141
|
+
# Task is still tracked, so a later flush includes it.
|
|
142
|
+
assert loop._wave_pr_state["p"].completed_tasks == [{"id": "t1", "title": "do work"}]
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio
|
|
145
|
+
async def test_never_raises(self):
|
|
146
|
+
loop = _make_loop()
|
|
147
|
+
ctx = _make_wave_context()
|
|
148
|
+
with (
|
|
149
|
+
_patch_wave_peek(None),
|
|
150
|
+
patch("steerdev_agent.git.pr.ensure_wave_pr", side_effect=RuntimeError("boom")),
|
|
151
|
+
):
|
|
152
|
+
await loop._maybe_ensure_wave_pr(
|
|
153
|
+
"p", {"id": "t1", "title": "do work"}, ctx, {"success": True}, "/repo"
|
|
154
|
+
) # must not raise
|
|
155
|
+
assert loop._wave_pr_state == {}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestFlushWavePr:
|
|
159
|
+
def test_noop_without_tracked_state(self):
|
|
160
|
+
loop = _make_loop()
|
|
161
|
+
with patch("steerdev_agent.git.pr.ensure_wave_pr") as ensure:
|
|
162
|
+
loop._flush_wave_pr("p", "/repo")
|
|
163
|
+
ensure.assert_not_called()
|
|
164
|
+
|
|
165
|
+
def test_falls_back_to_command_working_dir(self):
|
|
166
|
+
loop = _make_loop()
|
|
167
|
+
loop._wave_pr_state["p"] = _WavePrTracking(wave_ctx=_make_wave_context())
|
|
168
|
+
with patch("steerdev_agent.git.pr.ensure_wave_pr", return_value="created") as ensure:
|
|
169
|
+
loop._flush_wave_pr("p", "/repo")
|
|
170
|
+
assert ensure.call_args.args[0] == "/repo"
|
|
171
|
+
assert loop._wave_pr_state == {}
|
|
172
|
+
|
|
173
|
+
def test_state_is_per_project(self):
|
|
174
|
+
loop = _make_loop()
|
|
175
|
+
loop._wave_pr_state["p1"] = _WavePrTracking(wave_ctx=_make_wave_context())
|
|
176
|
+
loop._wave_pr_state["p2"] = _WavePrTracking(wave_ctx=_make_wave_context())
|
|
177
|
+
with patch("steerdev_agent.git.pr.ensure_wave_pr", return_value="created"):
|
|
178
|
+
loop._flush_wave_pr("p1", "/repo")
|
|
179
|
+
assert "p1" not in loop._wave_pr_state
|
|
180
|
+
assert "p2" in loop._wave_pr_state
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.1.45 → steerdev-1.1.48}/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
|
{steerdev-1.1.45 → steerdev-1.1.48}/src/steerdev_agent/setup/templates/worktrunk.config.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|