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.
- {steerdev-1.0.56 → steerdev-1.0.58}/PKG-INFO +1 -1
- {steerdev-1.0.56 → steerdev-1.0.58}/pyproject.toml +1 -1
- steerdev-1.0.58/snapshots/steerdev-agent-v1/Dockerfile +56 -0
- steerdev-1.0.58/snapshots/steerdev-agent-v1/start-agent.sh +42 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/reports.py +4 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/cli.py +15 -3
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/models.py +8 -1
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/evidence.py +35 -4
- steerdev-1.0.58/src/steerdev_agent/evidence_assets.py +177 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/runner.py +60 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/steerdev.yaml +3 -1
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_reports_client.py +6 -1
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_runner_worktrees.py +8 -2
- steerdev-1.0.58/tests/test_workflow_runs_api.py +358 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/.github/workflows/publish.yml +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/.gitignore +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/.pre-commit-config.yaml +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/AGENTS.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/CLAUDE.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/README.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/agent_loop.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/commands.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/__init__.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_agent_loop.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_agents_api.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_api_client.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_claude_executor.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_claude_setup.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_client_methods.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_commands_api.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_config.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_config_extended.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_context_search.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_executor.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_platform_config.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_prompt.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_retry.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_stream_parser.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_tasks.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_version.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workflow_context.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workspace.py +0 -0
- {steerdev-1.0.56 → steerdev-1.0.58}/tests/test_workspace_extended.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
|
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=
|
|
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 `
|
|
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
|
|
83
|
-
|
|
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:
|
|
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
|
|
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
|
|
85
|
+
def test_enabled_by_default(self):
|
|
86
86
|
r = Runner(project_id="p", api_key="k")
|
|
87
|
-
assert r._evidence_config.enabled is
|
|
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
|