steerdev 1.0.56__tar.gz → 1.0.58__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 (113) hide show
  1. {steerdev-1.0.56 → steerdev-1.0.58}/PKG-INFO +1 -1
  2. {steerdev-1.0.56 → steerdev-1.0.58}/pyproject.toml +1 -1
  3. steerdev-1.0.58/snapshots/steerdev-agent-v1/Dockerfile +56 -0
  4. steerdev-1.0.58/snapshots/steerdev-agent-v1/start-agent.sh +42 -0
  5. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/reports.py +4 -0
  6. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/cli.py +15 -3
  7. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/models.py +8 -1
  8. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/evidence.py +35 -4
  9. steerdev-1.0.58/src/steerdev_agent/evidence_assets.py +177 -0
  10. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/runner.py +60 -0
  11. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/steerdev.yaml +3 -1
  12. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_reports_client.py +6 -1
  13. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_runner_worktrees.py +8 -2
  14. steerdev-1.0.58/tests/test_workflow_runs_api.py +358 -0
  15. {steerdev-1.0.56 → steerdev-1.0.58}/.github/workflows/pre-commit.yml +0 -0
  16. {steerdev-1.0.56 → steerdev-1.0.58}/.github/workflows/publish.yml +0 -0
  17. {steerdev-1.0.56 → steerdev-1.0.58}/.gitignore +0 -0
  18. {steerdev-1.0.56 → steerdev-1.0.58}/.pre-commit-config.yaml +0 -0
  19. {steerdev-1.0.56 → steerdev-1.0.58}/AGENTS.md +0 -0
  20. {steerdev-1.0.56 → steerdev-1.0.58}/CLAUDE.md +0 -0
  21. {steerdev-1.0.56 → steerdev-1.0.58}/README.md +0 -0
  22. {steerdev-1.0.56 → steerdev-1.0.58}/scripts/pre-commit-version-bump.sh +0 -0
  23. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/__init__.py +0 -0
  24. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/agent_loop.py +0 -0
  25. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/__init__.py +0 -0
  26. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/activity.py +0 -0
  27. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/agents.py +0 -0
  28. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/canals.py +0 -0
  29. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/client.py +0 -0
  30. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/commands.py +0 -0
  31. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/configs.py +0 -0
  32. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/context.py +0 -0
  33. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/events.py +0 -0
  34. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/hooks.py +0 -0
  35. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/implementation_plan.py +0 -0
  36. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/messages.py +0 -0
  37. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/prd.py +0 -0
  38. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/runs.py +0 -0
  39. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/sessions.py +0 -0
  40. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/specs.py +0 -0
  41. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/tasks.py +0 -0
  42. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/workflow_runs.py +0 -0
  43. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/workflows.py +0 -0
  44. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/__init__.py +0 -0
  45. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/platform.py +0 -0
  46. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/settings.py +0 -0
  47. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/__init__.py +0 -0
  48. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/base.py +0 -0
  49. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/claude.py +0 -0
  50. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/stream.py +0 -0
  51. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/handlers/__init__.py +0 -0
  52. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/handlers/prd.py +0 -0
  53. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/integration.py +0 -0
  54. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/__init__.py +0 -0
  55. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/builder.py +0 -0
  56. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/templates.py +0 -0
  57. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  58. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/py.typed +0 -0
  59. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/retry.py +0 -0
  60. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/__init__.py +0 -0
  61. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/claude_setup.py +0 -0
  62. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/repo_setup.py +0 -0
  63. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  64. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  65. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/settings.json +0 -0
  66. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  67. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  68. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  69. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  70. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
  71. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  72. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
  73. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  74. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  75. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
  76. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  77. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/update_check.py +0 -0
  78. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/version.py +0 -0
  79. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/__init__.py +0 -0
  80. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/context.py +0 -0
  81. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/executor.py +0 -0
  82. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/memory.py +0 -0
  83. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workspace/__init__.py +0 -0
  84. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workspace/project_manager.py +0 -0
  85. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  86. {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/worktree.py +0 -0
  87. {steerdev-1.0.56 → steerdev-1.0.58}/tests/__init__.py +0 -0
  88. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_agent_loop.py +0 -0
  89. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_agent_loop_extended.py +0 -0
  90. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_agents_api.py +0 -0
  91. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_api_client.py +0 -0
  92. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_claude_executor.py +0 -0
  93. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_claude_setup.py +0 -0
  94. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_client_methods.py +0 -0
  95. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_commands_api.py +0 -0
  96. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_config.py +0 -0
  97. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_config_extended.py +0 -0
  98. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_conflict_mitigation.py +0 -0
  99. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_context_search.py +0 -0
  100. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_executor.py +0 -0
  101. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_platform_config.py +0 -0
  102. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_prompt.py +0 -0
  103. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_retry.py +0 -0
  104. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_runner_merge_modes.py +0 -0
  105. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_stream_parser.py +0 -0
  106. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_tasks.py +0 -0
  107. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_version.py +0 -0
  108. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workflow_context.py +0 -0
  109. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workflow_memory.py +0 -0
  110. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workflow_prompt_template.py +0 -0
  111. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workspace.py +0 -0
  112. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workspace_extended.py +0 -0
  113. {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerdev
3
- Version: 1.0.56
3
+ Version: 1.0.58
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.56"
3
+ version = "1.0.58"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,56 @@
1
+ FROM ubuntu:22.04
2
+
3
+ # Prevent interactive prompts during package install
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+
6
+ # ── System packages ──────────────────────────────────────────────────────────
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ build-essential \
9
+ ca-certificates \
10
+ curl \
11
+ git \
12
+ wget \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # ── Python 3.12 via deadsnakes PPA ───────────────────────────────────────────
16
+ RUN apt-get update && apt-get install -y --no-install-recommends \
17
+ software-properties-common \
18
+ && add-apt-repository ppa:deadsnakes/ppa \
19
+ && apt-get update && apt-get install -y --no-install-recommends \
20
+ python3.12 \
21
+ python3.12-venv \
22
+ python3-pip \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ # Make python3.12 the default python3
26
+ RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 \
27
+ && update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1
28
+
29
+ # ── uv (fast Python package installer) ───────────────────────────────────────
30
+ RUN curl -Ls https://astral.sh/uv/install.sh | sh
31
+ ENV PATH="/root/.local/bin:${PATH}"
32
+
33
+ # ── Node.js 20 LTS ───────────────────────────────────────────────────────────
34
+ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
35
+ && apt-get install -y --no-install-recommends nodejs \
36
+ && rm -rf /var/lib/apt/lists/*
37
+
38
+ # ── daytona user (uid 1000, matching Daytona convention) ─────────────────────
39
+ RUN useradd -m -u 1000 -s /bin/bash daytona
40
+
41
+ # ── steerdev agent (PyPI) — installed as root so it's on PATH for all users ──
42
+ RUN pip install --no-cache-dir steerdev
43
+
44
+ # ── Claude Code CLI (npm global) ─────────────────────────────────────────────
45
+ RUN npm install -g @anthropic-ai/claude-code
46
+
47
+ # ── Startup script ───────────────────────────────────────────────────────────
48
+ COPY start-agent.sh /home/daytona/start-agent.sh
49
+ RUN chmod 755 /home/daytona/start-agent.sh \
50
+ && chown daytona:daytona /home/daytona/start-agent.sh
51
+
52
+ # ── Runtime defaults ─────────────────────────────────────────────────────────
53
+ USER daytona
54
+ WORKDIR /home/daytona
55
+
56
+ CMD ["/home/daytona/start-agent.sh"]
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # Startup script for steerdev-agent-v1 Daytona sandbox.
3
+ #
4
+ # Environment variables consumed:
5
+ # STEERDEV_PROJECT_ID (required) — project ID passed to the agent
6
+ # STEERDEV_AGENT_NAME (required) — name used to register the agent
7
+ # STEERDEV_API_KEY (required) — API key read natively by the steerdev CLI
8
+ # GIT_REPO_URL (optional) — if set, clone this repo before starting
9
+ # GIT_BRANCH (optional) — branch to checkout (default: main)
10
+ # GIT_WORKSPACE_DIR (optional) — clone destination (default: /home/daytona/workspace)
11
+
12
+ # Disable trace mode so shell expansion never prints secret values to stdout.
13
+ set +x
14
+ set -euo pipefail
15
+
16
+ : "${STEERDEV_PROJECT_ID:?STEERDEV_PROJECT_ID is required}"
17
+ : "${STEERDEV_AGENT_NAME:?STEERDEV_AGENT_NAME is required}"
18
+ : "${STEERDEV_API_KEY:?STEERDEV_API_KEY is required}"
19
+
20
+ GIT_BRANCH="${GIT_BRANCH:-main}"
21
+ GIT_WORKSPACE_DIR="${GIT_WORKSPACE_DIR:-/home/daytona/workspace}"
22
+
23
+ # ── Optional repository clone ─────────────────────────────────────────────────
24
+ if [ -n "${GIT_REPO_URL:-}" ]; then
25
+ # Configure git to authenticate via GITHUB_TOKEN for private repos.
26
+ # URL rewriting keeps the token out of process arguments and clone logs.
27
+ if [ -n "${GITHUB_TOKEN:-}" ]; then
28
+ git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
29
+ fi
30
+
31
+ mkdir -p "${GIT_WORKSPACE_DIR}"
32
+ git clone --branch "${GIT_BRANCH}" -- "${GIT_REPO_URL}" "${GIT_WORKSPACE_DIR}"
33
+ fi
34
+
35
+ cd "${GIT_WORKSPACE_DIR}"
36
+
37
+ # Replace this shell process with the agent so signals (SIGTERM/SIGINT) are
38
+ # delivered directly to the agent. Credentials are passed only via environment
39
+ # variables — never as CLI flags (which would appear in `ps aux`).
40
+ exec steerdev agent \
41
+ --project-id "${STEERDEV_PROJECT_ID}" \
42
+ --agent-name "${STEERDEV_AGENT_NAME}"
@@ -29,6 +29,7 @@ class ReportsClient(SteerDevClient):
29
29
  project_id: str,
30
30
  summary: str,
31
31
  blocks: list[dict[str, Any]],
32
+ artifacts: list[dict[str, Any]] | None = None,
32
33
  task_id: str | None = None,
33
34
  wave_id: str | None = None,
34
35
  agent_id: str | None = None,
@@ -40,6 +41,7 @@ class ReportsClient(SteerDevClient):
40
41
  project_id: SteerDev project ID.
41
42
  summary: Report title/summary (required for reviewability).
42
43
  blocks: List of report blocks (type, content, order).
44
+ artifacts: Optional list of file artifacts (filename, mime_type, size, content_base64/url).
43
45
  task_id: Associated task ID.
44
46
  wave_id: Associated wave ID.
45
47
  agent_id: Associated agent ID.
@@ -53,6 +55,8 @@ class ReportsClient(SteerDevClient):
53
55
  "summary": summary,
54
56
  "blocks": blocks,
55
57
  }
58
+ if artifacts:
59
+ payload["artifacts"] = artifacts
56
60
  if task_id:
57
61
  payload["task_id"] = task_id
58
62
  if wave_id:
@@ -1667,7 +1667,7 @@ def run(
1667
1667
  bool | None,
1668
1668
  typer.Option(
1669
1669
  "--evidence/--no-evidence",
1670
- help="Submit evidence reports after task completion (default: from config or disabled)",
1670
+ help="Submit evidence reports after task completion (default: from config or enabled)",
1671
1671
  ),
1672
1672
  ] = None,
1673
1673
  dry_run: Annotated[
@@ -1983,11 +1983,19 @@ def agent(
1983
1983
  help="Enable canal merge flow selection (default: disabled)",
1984
1984
  ),
1985
1985
  ] = False,
1986
+ worktrees: Annotated[
1987
+ bool | None,
1988
+ typer.Option(
1989
+ "--worktrees/--no-worktrees",
1990
+ "-W",
1991
+ help="Enable git worktree isolation (default: from config or disabled)",
1992
+ ),
1993
+ ] = None,
1986
1994
  evidence: Annotated[
1987
1995
  bool | None,
1988
1996
  typer.Option(
1989
1997
  "--evidence/--no-evidence",
1990
- help="Submit evidence reports after task completion (default: from config or disabled)",
1998
+ help="Submit evidence reports after task completion (default: from config or enabled)",
1991
1999
  ),
1992
2000
  ] = None,
1993
2001
  ) -> None:
@@ -2008,12 +2016,14 @@ def agent(
2008
2016
  Use --workflow-id to force a specific workflow for all tasks.
2009
2017
 
2010
2018
  Use --waves/--no-waves to control wave-aware scheduling. Use --canals to
2011
- expose the canal merge flow to workflow phases.
2019
+ expose the canal merge flow to workflow phases. Use --worktrees to enable
2020
+ git worktree isolation.
2012
2021
 
2013
2022
  Example:
2014
2023
  steerdev agent --project-id abc123 --agent-name my-agent
2015
2024
  steerdev agent --workspace ~/workspace --agent-name my-agent
2016
2025
  steerdev agent --project-id abc123 --agent-name my-agent --no-waves
2026
+ steerdev agent --project-id abc123 --agent-name my-agent --worktrees
2017
2027
  steerdev agent --project-id abc123 --agent-name my-agent --workflow-id wf-abc123 --canals
2018
2028
  steerdev agent --project-id abc123 --agent-name my-agent --workflow-id wf-abc123
2019
2029
  steerdev agent --project-id abc123 --agent-name my-agent --poll-interval 10
@@ -2050,6 +2060,8 @@ def agent(
2050
2060
  agent_loop_config.gap_seconds = gap_seconds
2051
2061
 
2052
2062
  resolved_worktree_config = config.worktrees.model_copy()
2063
+ if worktrees is not None:
2064
+ resolved_worktree_config.enabled = worktrees
2053
2065
  resolved_evidence_config = config.evidence.model_copy()
2054
2066
  if evidence is not None:
2055
2067
  resolved_evidence_config.enabled = evidence
@@ -255,7 +255,14 @@ class EvidenceConfig(BaseModel):
255
255
 
256
256
  enabled: Annotated[
257
257
  bool,
258
- Field(default=False, description="Submit evidence reports after task completion"),
258
+ Field(default=True, description="Submit evidence reports after task completion"),
259
+ ]
260
+ assets_dir: Annotated[
261
+ str,
262
+ Field(
263
+ default=".steerdev/evidence",
264
+ description="Directory for evidence assets (screenshots, logs) relative to working directory",
265
+ ),
259
266
  ]
260
267
 
261
268
 
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  import contextlib
12
+ from pathlib import Path
12
13
  from typing import Any
13
14
 
14
15
  from loguru import logger
@@ -27,6 +28,7 @@ EVIDENCE_MAX_TURNS = 25
27
28
  def build_evidence_prompt(
28
29
  task: dict[str, Any],
29
30
  success: bool,
31
+ assets_dir: str | None = None,
30
32
  phase_summaries: list[dict[str, Any]] | None = None,
31
33
  evidence_messages: list[str] | None = None,
32
34
  ) -> str:
@@ -35,6 +37,7 @@ def build_evidence_prompt(
35
37
  Args:
36
38
  task: The completed task dict.
37
39
  success: Whether the task succeeded.
40
+ assets_dir: Absolute path where the agent should save evidence files.
38
41
  phase_summaries: Per-phase summaries (workflow tasks only).
39
42
  evidence_messages: Assistant messages collected during execution
40
43
  (non-workflow tasks only).
@@ -66,21 +69,43 @@ def build_evidence_prompt(
66
69
  last = evidence_messages[-1][:3000]
67
70
  sections.append(f"## Agent Output (last message)\n{last}\n")
68
71
 
72
+ # Build asset directory instructions
73
+ assets_instruction = ""
74
+ if assets_dir:
75
+ assets_instruction = (
76
+ f"\n**IMPORTANT — Evidence Assets Directory:** `{assets_dir}/`\n"
77
+ "Save ALL screenshots, logs, and evidence files to this directory.\n"
78
+ )
79
+
80
+ screenshot_cmd = "agent-browser screenshot"
81
+ if assets_dir:
82
+ screenshot_cmd = f"agent-browser screenshot {assets_dir}/screenshot-01.png"
83
+
69
84
  sections.append(
70
85
  "## Your Instructions\n\n"
71
86
  "Collect evidence that the work is correct. Follow these steps:\n\n"
87
+ f"{assets_instruction}\n"
72
88
  "### 1. File Changes\n"
73
89
  "Run `git diff --stat` and `git log --oneline -5` to see what changed.\n"
74
90
  "Summarize the key modifications.\n\n"
75
91
  "### 2. Visual Verification\n"
76
92
  "If the task involved **UI or frontend changes**:\n"
77
- "- Use `agent-browser open <url>` to navigate to the relevant page\n"
78
- "- Use `agent-browser screenshot` to capture it\n"
93
+ f"- Use `agent-browser open <url>` to navigate to the relevant page\n"
94
+ f"- Use `{screenshot_cmd}` to capture it\n"
95
+ "- Take multiple screenshots if needed (screenshot-01.png, screenshot-02.png, etc.)\n"
79
96
  "- Describe what you see and whether it matches the acceptance criteria\n\n"
80
97
  "If the task was backend-only, verify via tests or logs instead.\n\n"
81
98
  "### 3. Tests\n"
82
- "If there are relevant tests, run them and report the results.\n\n"
83
- "### 4. Review Summary\n"
99
+ "If there are relevant tests, run them and report the results.\n"
100
+ )
101
+
102
+ if assets_dir:
103
+ sections.append(
104
+ f"If tests produce log output, save it to `{assets_dir}/test-output.log`.\n"
105
+ )
106
+
107
+ sections.append(
108
+ "\n### 4. Review Summary\n"
84
109
  "Write a structured review covering:\n"
85
110
  "- What was implemented or changed\n"
86
111
  "- Key files modified and why\n"
@@ -101,6 +126,7 @@ async def collect_evidence(
101
126
  model: str | None,
102
127
  api_key: str | None,
103
128
  dry_run: bool = False,
129
+ assets_dir: str | None = None,
104
130
  phase_summaries: list[dict[str, Any]] | None = None,
105
131
  evidence_messages: list[str] | None = None,
106
132
  ) -> list[str]:
@@ -109,9 +135,14 @@ async def collect_evidence(
109
135
  Returns a list of assistant messages produced during the evidence session.
110
136
  Best-effort: returns an empty list on any failure.
111
137
  """
138
+ # Ensure assets directory exists so the evidence agent can write to it.
139
+ if assets_dir:
140
+ Path(assets_dir).mkdir(parents=True, exist_ok=True)
141
+
112
142
  prompt = build_evidence_prompt(
113
143
  task,
114
144
  success=success,
145
+ assets_dir=assets_dir,
115
146
  phase_summaries=phase_summaries,
116
147
  evidence_messages=evidence_messages,
117
148
  )
@@ -0,0 +1,177 @@
1
+ """Evidence asset scanning and upload.
2
+
3
+ Scans a directory for evidence files (screenshots, logs) produced during
4
+ evidence collection, then uploads them to the platform via multipart upload
5
+ and returns metadata with CDN URLs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import mimetypes
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ import httpx
15
+ from loguru import logger
16
+
17
+ from steerdev_agent.api.client import get_api_endpoint
18
+
19
+ # File extensions we recognise as evidence assets.
20
+ ASSET_EXTENSIONS: set[str] = {
21
+ ".png",
22
+ ".jpg",
23
+ ".jpeg",
24
+ ".webp",
25
+ ".gif", # images
26
+ ".pdf", # documents
27
+ ".webm",
28
+ ".mp4", # video
29
+ ".log",
30
+ ".txt", # logs / text
31
+ }
32
+
33
+ # Hard limits to avoid uploading too much data.
34
+ MAX_ASSETS = 10
35
+ DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class EvidenceAsset:
40
+ """A file found in the evidence assets directory, optionally uploaded."""
41
+
42
+ filepath: Path
43
+ filename: str
44
+ mime_type: str
45
+ size: int
46
+ # Populated after upload — proxy URL that serves the file through app auth
47
+ url: str | None = None
48
+
49
+
50
+ def scan_evidence_assets(
51
+ assets_dir: str | Path,
52
+ *,
53
+ max_file_size: int = DEFAULT_MAX_FILE_SIZE,
54
+ max_assets: int = MAX_ASSETS,
55
+ ) -> list[EvidenceAsset]:
56
+ """Scan *assets_dir* and return evidence assets found on disk.
57
+
58
+ Best-effort: logs warnings for unreadable files, never raises.
59
+ """
60
+ dir_path = Path(assets_dir)
61
+ if not dir_path.is_dir():
62
+ return []
63
+
64
+ assets: list[EvidenceAsset] = []
65
+
66
+ try:
67
+ # Sort by name for deterministic ordering.
68
+ for entry in sorted(dir_path.iterdir(), key=lambda p: p.name):
69
+ if len(assets) >= max_assets:
70
+ logger.debug(f"Evidence assets capped at {max_assets}")
71
+ break
72
+
73
+ if not entry.is_file():
74
+ continue
75
+
76
+ suffix = entry.suffix.lower()
77
+ if suffix not in ASSET_EXTENSIONS:
78
+ continue
79
+
80
+ try:
81
+ size = entry.stat().st_size
82
+ if size == 0:
83
+ continue
84
+ if size > max_file_size:
85
+ logger.warning(
86
+ f"Evidence asset too large, skipping: {entry.name} ({size} bytes)"
87
+ )
88
+ continue
89
+
90
+ mime_type = mimetypes.guess_type(entry.name)[0] or "application/octet-stream"
91
+
92
+ assets.append(
93
+ EvidenceAsset(
94
+ filepath=entry,
95
+ filename=entry.name,
96
+ mime_type=mime_type,
97
+ size=size,
98
+ )
99
+ )
100
+ except Exception:
101
+ logger.warning(f"Failed to read evidence asset: {entry.name}", exc_info=True)
102
+
103
+ except Exception:
104
+ logger.warning("Failed to scan evidence assets directory", exc_info=True)
105
+
106
+ return assets
107
+
108
+
109
+ def upload_evidence_assets(
110
+ assets: list[EvidenceAsset],
111
+ *,
112
+ api_key: str | None,
113
+ folder: str = "evidence",
114
+ timeout: float = 60.0,
115
+ ) -> list[EvidenceAsset]:
116
+ """Upload evidence assets to the platform via multipart upload.
117
+
118
+ Returns a new list of ``EvidenceAsset`` with ``url`` and ``download_url``
119
+ populated for successfully uploaded files. Assets that fail to upload are
120
+ returned unchanged (url=None).
121
+
122
+ Best-effort: individual upload failures are logged, never raised.
123
+ """
124
+ if not assets or not api_key:
125
+ return assets
126
+
127
+ api_base = get_api_endpoint()
128
+ upload_url = f"{api_base}/upload"
129
+ headers = {"Authorization": f"Bearer {api_key}"}
130
+
131
+ uploaded: list[EvidenceAsset] = []
132
+
133
+ with httpx.Client(timeout=timeout, follow_redirects=True) as client:
134
+ for asset in assets:
135
+ try:
136
+ with asset.filepath.open("rb") as f:
137
+ files = {
138
+ "file": (asset.filename, f, asset.mime_type),
139
+ }
140
+ data = {
141
+ "filename": asset.filename,
142
+ "folder": folder,
143
+ }
144
+ response = client.post(
145
+ upload_url,
146
+ headers=headers,
147
+ files=files,
148
+ data=data,
149
+ )
150
+
151
+ if response.status_code in (200, 201):
152
+ result = response.json()
153
+ uploaded.append(
154
+ EvidenceAsset(
155
+ filepath=asset.filepath,
156
+ filename=asset.filename,
157
+ mime_type=asset.mime_type,
158
+ size=asset.size,
159
+ url=result.get("url"),
160
+ )
161
+ )
162
+ logger.debug(f"Uploaded evidence asset: {asset.filename}")
163
+ else:
164
+ logger.warning(
165
+ f"Evidence asset upload failed ({response.status_code}): "
166
+ f"{asset.filename} - {response.text[:200]}"
167
+ )
168
+ uploaded.append(asset)
169
+
170
+ except Exception:
171
+ logger.warning(
172
+ f"Failed to upload evidence asset: {asset.filename}",
173
+ exc_info=True,
174
+ )
175
+ uploaded.append(asset)
176
+
177
+ return uploaded
@@ -496,9 +496,13 @@ class Runner:
496
496
  Spawns a short agent session that reviews git changes, optionally
497
497
  uses agent-browser for visual verification, and writes a structured
498
498
  review. The review output becomes the evidence report content.
499
+ Screenshots and other assets are gathered from the evidence directory
500
+ and uploaded alongside the text report.
499
501
 
500
502
  Best-effort: errors are logged but never propagate.
501
503
  """
504
+ from pathlib import Path
505
+
502
506
  from steerdev_agent.api.reports import ReportsClient
503
507
  from steerdev_agent.evidence import collect_evidence
504
508
 
@@ -506,6 +510,11 @@ class Runner:
506
510
  task_title = task.get("title", "Unknown Task")
507
511
  status_label = "Completed" if success else "Failed"
508
512
 
513
+ # ── Compute evidence assets directory ──
514
+ assets_dir = str(
515
+ Path(self.working_directory) / self._evidence_config.assets_dir / (task_id or "unknown")
516
+ )
517
+
509
518
  # ── Run evidence-collection session ──
510
519
  collected = await collect_evidence(
511
520
  task=task,
@@ -515,10 +524,47 @@ class Runner:
515
524
  model=self.model,
516
525
  api_key=self._api_key,
517
526
  dry_run=self.dry_run,
527
+ assets_dir=assets_dir,
518
528
  phase_summaries=phase_summaries,
519
529
  evidence_messages=evidence_messages,
520
530
  )
521
531
 
532
+ # ── Scan and upload evidence assets (screenshots, logs) ──
533
+ api_artifacts: list[dict[str, Any]] = []
534
+ uploaded_assets: list[Any] = []
535
+ try:
536
+ from steerdev_agent.evidence_assets import (
537
+ scan_evidence_assets,
538
+ upload_evidence_assets,
539
+ )
540
+
541
+ assets = scan_evidence_assets(assets_dir)
542
+ if assets:
543
+ console.print(f"[dim]Evidence assets found: {len(assets)} files[/dim]")
544
+ upload_folder = f"evidence/{self.project_id}/{task_id or 'unknown'}"
545
+ uploaded_assets = upload_evidence_assets(
546
+ assets,
547
+ api_key=self._api_key,
548
+ folder=upload_folder,
549
+ )
550
+ uploaded_count = sum(1 for a in uploaded_assets if a.url)
551
+ console.print(
552
+ f"[dim]Evidence assets uploaded: {uploaded_count}/{len(assets)}[/dim]"
553
+ )
554
+
555
+ for asset in uploaded_assets:
556
+ if asset.url:
557
+ api_artifacts.append(
558
+ {
559
+ "filename": asset.filename,
560
+ "mime_type": asset.mime_type,
561
+ "size": asset.size,
562
+ "url": asset.url,
563
+ }
564
+ )
565
+ except Exception:
566
+ logger.debug("Evidence asset scanning/upload failed", exc_info=True)
567
+
522
568
  # ── Build report blocks from collected evidence ──
523
569
  summary = f"{task_title} - {status_label}"
524
570
  blocks: list[dict[str, Any]] = []
@@ -556,6 +602,19 @@ class Runner:
556
602
  )
557
603
  order += 1
558
604
 
605
+ # Add image blocks for uploaded screenshot assets (with CDN URLs)
606
+ for asset in uploaded_assets:
607
+ if asset.url and asset.mime_type.startswith("image/"):
608
+ blocks.append(
609
+ {
610
+ "type": "image",
611
+ "content": asset.url,
612
+ "order": order,
613
+ "alt_text": asset.filename,
614
+ }
615
+ )
616
+ order += 1
617
+
559
618
  if not blocks:
560
619
  blocks.append(
561
620
  {
@@ -584,6 +643,7 @@ class Runner:
584
643
  project_id=self.project_id,
585
644
  summary=summary,
586
645
  blocks=blocks,
646
+ artifacts=api_artifacts or None,
587
647
  task_id=task_id,
588
648
  wave_id=wave_id,
589
649
  evaluation_steps=evaluation_steps,
@@ -74,7 +74,9 @@ worktrees:
74
74
 
75
75
  # Evidence reports: submit a review-ready report after each task/workflow
76
76
  evidence:
77
- enabled: false
77
+ enabled: true
78
+ # Directory for evidence assets (screenshots, logs) relative to working dir
79
+ assets_dir: ".steerdev/evidence"
78
80
 
79
81
  # Retry failed tasks automatically
80
82
  retry:
@@ -108,8 +108,13 @@ class TestReportsClient:
108
108
  class TestEvidenceConfig:
109
109
  def test_defaults(self):
110
110
  cfg = EvidenceConfig()
111
- assert cfg.enabled is False
111
+ assert cfg.enabled is True
112
+ assert cfg.assets_dir == ".steerdev/evidence"
112
113
 
113
114
  def test_enabled(self):
114
115
  cfg = EvidenceConfig(enabled=True)
115
116
  assert cfg.enabled is True
117
+
118
+ def test_custom_assets_dir(self):
119
+ cfg = EvidenceConfig(assets_dir=".evidence")
120
+ assert cfg.assets_dir == ".evidence"
@@ -82,11 +82,17 @@ class TestRunnerWorktreeConfig:
82
82
 
83
83
 
84
84
  class TestRunnerEvidenceConfig:
85
- def test_disabled_by_default(self):
85
+ def test_enabled_by_default(self):
86
86
  r = Runner(project_id="p", api_key="k")
87
- assert r._evidence_config.enabled is False
87
+ assert r._evidence_config.enabled is True
88
+ assert r._evidence_config.assets_dir == ".steerdev/evidence"
88
89
 
89
90
  def test_enabled_via_config(self):
90
91
  cfg = EvidenceConfig(enabled=True)
91
92
  r = Runner(project_id="p", api_key="k", evidence_config=cfg)
92
93
  assert r._evidence_config.enabled is True
94
+
95
+ def test_disabled_via_config(self):
96
+ cfg = EvidenceConfig(enabled=False)
97
+ r = Runner(project_id="p", api_key="k", evidence_config=cfg)
98
+ assert r._evidence_config.enabled is False