feat: add profiled agent challenge harness
This commit is contained in:
@@ -0,0 +1,748 @@
|
||||
# Agent Challenge Harness V2 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a manifest-driven generic challenge runner with layered prompts, `none|skills|all` instruction profiles, one-hour trial ceilings, normalized OpenCode tool/token evidence, policy findings, and generated audit reports.
|
||||
|
||||
**Architecture:** Challenge manifests and templates are data; executable code lives in `examples/agent_challenges`. The runner creates one isolated trial workspace, composes base/profile/challenge prompts, executes OpenCode from that workspace, preserves raw JSONL, and delegates bounded normalization to focused metrics/policy/report modules. Automatic task classification and validity are provisional; manual audit remains authoritative.
|
||||
|
||||
**Tech Stack:** Python 3.14, dataclasses/Pydantic, PyYAML, subprocess, hashlib, pytest, existing OpenCode JSONL format.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create `examples/agent_challenges/models.py`: manifest/profile/result DTOs.
|
||||
- Create `examples/agent_challenges/manifests.py`: YAML loading and path resolution.
|
||||
- Create `examples/agent_challenges/prompts.py`: layered prompt rendering/provenance.
|
||||
- Create `examples/agent_challenges/metrics.py`: JSONL event/tool/token normalization.
|
||||
- Create `examples/agent_challenges/policy.py`: observed path categorization and validity.
|
||||
- Modify `examples/agent_challenges/workspace.py`: profile-aware workspace creation and
|
||||
instruction bundle copying.
|
||||
- Modify `examples/agent_challenges/opencode_io.py`: command uses persisted rendered prompt.
|
||||
- Modify `examples/agent_challenges/runner.py`: manifest/profile orchestration, trial cwd,
|
||||
3,600-second default, normalized evidence.
|
||||
- Modify `examples/agent_challenges/reports.py`: generated report with evidence sections.
|
||||
- Modify `examples/agent_challenges/audit.py`: consume v2 task/validity fields.
|
||||
- Create `examples/agent_challenges/run_trials.py`: central CLI.
|
||||
- Create `examples/agent_challenges/save_trial_report.py`: central report CLI.
|
||||
- Create `examples/agent_challenges/save_manual_audit.py`: central audit CLI.
|
||||
- Create `examples/agent_challenges/base-prompt.md`: invariant benchmark rules.
|
||||
- Create `examples/agent_challenges/profile-prompts/{none,skills,all}.md`: policy fragments.
|
||||
- Create `tests/examples/test_agent_challenge_harness_v2.py`: generic behavior tests.
|
||||
|
||||
### Task 1: Define Manifest And Profile Models
|
||||
|
||||
**Files:**
|
||||
- Create: `examples/agent_challenges/models.py`
|
||||
- Create: `examples/agent_challenges/manifests.py`
|
||||
- Create: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||
|
||||
- [ ] **Step 1: Write failing manifest/profile tests**
|
||||
|
||||
Create `tests/examples/test_agent_challenge_harness_v2.py` with imports and:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from examples.agent_challenges.manifests import load_challenge_manifest
|
||||
from examples.agent_challenges.models import InstructionProfile
|
||||
|
||||
|
||||
def _write_manifest(root: Path) -> Path:
|
||||
(root / "workspace_template").mkdir(parents=True)
|
||||
(root / "challenge-prompt.md").write_text("Build it.\n", encoding="utf-8")
|
||||
path = root / "challenge.yaml"
|
||||
path.write_text(
|
||||
"""\
|
||||
version: 1
|
||||
id: fixture
|
||||
prompt: challenge-prompt.md
|
||||
workspace_template: workspace_template
|
||||
source:
|
||||
id: local.fixture
|
||||
root: source
|
||||
module: ops
|
||||
registry: registry
|
||||
store_root: .wf_fixture_store
|
||||
server:
|
||||
config: wf.config.json
|
||||
default_port: 8779
|
||||
report:
|
||||
required_fields: [value, run_failed]
|
||||
success_assertions:
|
||||
value: expected
|
||||
run_failed: false
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def test_load_challenge_manifest_resolves_paths(tmp_path: Path) -> None:
|
||||
manifest_path = _write_manifest(tmp_path)
|
||||
|
||||
loaded = load_challenge_manifest(manifest_path)
|
||||
|
||||
assert loaded.manifest.id == "fixture"
|
||||
assert loaded.root == tmp_path.resolve()
|
||||
assert loaded.prompt_path == (tmp_path / "challenge-prompt.md").resolve()
|
||||
assert loaded.workspace_template == (tmp_path / "workspace_template").resolve()
|
||||
assert loaded.manifest.report.success_assertions == {
|
||||
"value": "expected",
|
||||
"run_failed": False,
|
||||
}
|
||||
|
||||
|
||||
def test_instruction_profiles_are_exactly_the_supported_conditions() -> None:
|
||||
assert [profile.value for profile in InstructionProfile] == [
|
||||
"none",
|
||||
"skills",
|
||||
"all",
|
||||
]
|
||||
|
||||
|
||||
def test_invalid_manifest_rejects_parent_traversal(tmp_path: Path) -> None:
|
||||
path = _write_manifest(tmp_path)
|
||||
text = path.read_text(encoding="utf-8").replace(
|
||||
"workspace_template: workspace_template",
|
||||
"workspace_template: ../outside",
|
||||
)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="workspace_template"):
|
||||
load_challenge_manifest(path)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests and verify imports fail**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -q
|
||||
```
|
||||
|
||||
Expected: import failures for `models`/`manifests`.
|
||||
|
||||
- [ ] **Step 3: Implement Pydantic manifest DTOs**
|
||||
|
||||
Create `examples/agent_challenges/models.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class InstructionProfile(StrEnum):
|
||||
NONE = "none"
|
||||
SKILLS = "skills"
|
||||
ALL = "all"
|
||||
|
||||
|
||||
class SourceManifest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
id: str = Field(min_length=1)
|
||||
root: str = Field(min_length=1)
|
||||
module: str = Field(min_length=1)
|
||||
registry: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ServerManifest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
config: str = Field(min_length=1)
|
||||
default_port: int = Field(ge=1, le=65535)
|
||||
|
||||
|
||||
class ReportManifest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
required_fields: list[str] = Field(default_factory=list)
|
||||
success_assertions: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ChallengeManifest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
version: int = Field(ge=1)
|
||||
id: str = Field(pattern=r"^[a-z][a-z0-9_-]*$")
|
||||
prompt: str
|
||||
workspace_template: str
|
||||
source: SourceManifest
|
||||
store_root: str
|
||||
server: ServerManifest
|
||||
report: ReportManifest
|
||||
|
||||
|
||||
class LoadedChallenge(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
manifest_path: Path
|
||||
root: Path
|
||||
prompt_path: Path
|
||||
workspace_template: Path
|
||||
source_root: Path
|
||||
server_config: Path
|
||||
manifest: ChallengeManifest
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Implement safe manifest loading**
|
||||
|
||||
Create `examples/agent_challenges/manifests.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from .models import ChallengeManifest, LoadedChallenge
|
||||
|
||||
|
||||
def _inside(root: Path, relative: str, *, field: str) -> Path:
|
||||
candidate = (root / relative).resolve()
|
||||
if not candidate.is_relative_to(root):
|
||||
raise ValueError(f"challenge {field} must stay inside challenge directory")
|
||||
return candidate
|
||||
|
||||
|
||||
def load_challenge_manifest(path: Path) -> LoadedChallenge:
|
||||
manifest_path = path.resolve()
|
||||
root = manifest_path.parent
|
||||
loaded = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest = ChallengeManifest.model_validate(loaded)
|
||||
prompt_path = _inside(root, manifest.prompt, field="prompt")
|
||||
workspace_template = _inside(
|
||||
root, manifest.workspace_template, field="workspace_template"
|
||||
)
|
||||
source_root = (root / manifest.source.root).resolve()
|
||||
server_config = (root / manifest.server.config).resolve()
|
||||
if not prompt_path.is_file():
|
||||
raise ValueError(f"challenge prompt does not exist: {prompt_path}")
|
||||
if not workspace_template.is_dir():
|
||||
raise ValueError(f"challenge workspace_template does not exist: {workspace_template}")
|
||||
return LoadedChallenge(
|
||||
manifest_path=manifest_path,
|
||||
root=root,
|
||||
prompt_path=prompt_path,
|
||||
workspace_template=workspace_template,
|
||||
source_root=source_root,
|
||||
server_config=server_config,
|
||||
manifest=manifest,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run manifest tests and commit**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -q
|
||||
git add examples/agent_challenges/models.py examples/agent_challenges/manifests.py tests/examples/test_agent_challenge_harness_v2.py
|
||||
git commit -m "feat: add agent challenge manifests"
|
||||
```
|
||||
|
||||
### Task 2: Add Layered Prompts And Profile-Aware Workspaces
|
||||
|
||||
**Files:**
|
||||
- Create: `examples/agent_challenges/base-prompt.md`
|
||||
- Create: `examples/agent_challenges/profile-prompts/none.md`
|
||||
- Create: `examples/agent_challenges/profile-prompts/skills.md`
|
||||
- Create: `examples/agent_challenges/profile-prompts/all.md`
|
||||
- Create: `examples/agent_challenges/prompts.py`
|
||||
- Modify: `examples/agent_challenges/workspace.py`
|
||||
- Test: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||
|
||||
- [ ] **Step 1: Add failing prompt/profile tests**
|
||||
|
||||
Append tests that call `compose_trial_prompt` and `prepare_v2_trial_workspace`:
|
||||
|
||||
```python
|
||||
from examples.agent_challenges.prompts import compose_trial_prompt
|
||||
from examples.agent_challenges.workspace import prepare_v2_trial_workspace
|
||||
|
||||
|
||||
def test_challenge_prompt_is_identical_across_profiles(tmp_path: Path) -> None:
|
||||
challenge = load_challenge_manifest(_write_manifest(tmp_path / "challenge"))
|
||||
rendered = {
|
||||
profile: compose_trial_prompt(
|
||||
challenge,
|
||||
profile=profile,
|
||||
wf_command_prefix="uv run wf --config wf.config.json --local",
|
||||
server_context="Local mode.",
|
||||
workspace_path=tmp_path / profile.value,
|
||||
)
|
||||
for profile in InstructionProfile
|
||||
}
|
||||
|
||||
assert {value.challenge_sha256 for value in rendered.values()} == {
|
||||
rendered[InstructionProfile.NONE].challenge_sha256
|
||||
}
|
||||
assert len({value.rendered_sha256 for value in rendered.values()}) == 3
|
||||
assert "report the exact blocker" in rendered[InstructionProfile.NONE].text
|
||||
assert ".agent/skills" in rendered[InstructionProfile.SKILLS].text
|
||||
assert "inspect broader repository" in rendered[InstructionProfile.ALL].text
|
||||
|
||||
|
||||
def test_skills_profile_copies_bundle_but_none_does_not(tmp_path: Path) -> None:
|
||||
challenge = load_challenge_manifest(_write_manifest(tmp_path / "challenge"))
|
||||
bundle = ROOT / "examples/agent_challenges/instruction_bundles/workflow_cli.yaml"
|
||||
|
||||
none_workspace = prepare_v2_trial_workspace(
|
||||
challenge,
|
||||
profile=InstructionProfile.NONE,
|
||||
model="model",
|
||||
index=1,
|
||||
workspaces_dir=tmp_path / "workspaces",
|
||||
instruction_bundle=bundle,
|
||||
)
|
||||
skills_workspace = prepare_v2_trial_workspace(
|
||||
challenge,
|
||||
profile=InstructionProfile.SKILLS,
|
||||
model="model",
|
||||
index=2,
|
||||
workspaces_dir=tmp_path / "workspaces",
|
||||
instruction_bundle=bundle,
|
||||
)
|
||||
|
||||
assert not (none_workspace.root / ".agent/skills").exists()
|
||||
assert (skills_workspace.root / ".agent/skills/wf-cli/SKILL.md").is_file()
|
||||
assert skills_workspace.instruction_files
|
||||
```
|
||||
|
||||
Add `ROOT = Path(__file__).resolve().parents[2]` if it is not already present.
|
||||
|
||||
- [ ] **Step 2: Create invariant and profile prompts**
|
||||
|
||||
Create `base-prompt.md` with placeholders:
|
||||
|
||||
```markdown
|
||||
# Workflow Agent Challenge
|
||||
|
||||
Use the repository's public `wf` product path to complete the challenge below.
|
||||
Do not replace the workflow lifecycle with a helper script that imports internal
|
||||
workflow APIs. Preserve exact commands, failures, run ids, and evidence in your
|
||||
final answer.
|
||||
|
||||
Use this command prefix:
|
||||
|
||||
{{wf_command_prefix}}
|
||||
|
||||
{{server_context}}
|
||||
|
||||
Your writable trial workspace is `{{workspace_path}}`. Write attempt files only
|
||||
inside it. End with the challenge's requested YAML self-report. The self-report
|
||||
will be checked against observed tool calls and manually audited.
|
||||
```
|
||||
|
||||
Create profile fragments:
|
||||
|
||||
`none.md`:
|
||||
|
||||
```markdown
|
||||
## Instruction Profile: none
|
||||
|
||||
Use challenge files, `wf --help`, `wf schema`, validation, inspect, and bounded
|
||||
trace commands. Do not read repository skills, docs, examples, tests, source,
|
||||
prior trials, or prior stores. If public surfaces are insufficient, report the
|
||||
exact blocker and finish the task as failed rather than reverse-engineering the
|
||||
implementation.
|
||||
```
|
||||
|
||||
`skills.md`:
|
||||
|
||||
```markdown
|
||||
## Instruction Profile: skills
|
||||
|
||||
Use the supplied skills under `.agent/skills/` plus public `wf` commands. Do not
|
||||
read repository examples, tests, source, prior trials, or prior stores. If the
|
||||
skills and public surfaces are insufficient, report the exact blocker rather
|
||||
than reverse-engineering implementation code.
|
||||
```
|
||||
|
||||
`all.md`:
|
||||
|
||||
```markdown
|
||||
## Instruction Profile: all
|
||||
|
||||
Start with the supplied skills and public docs. If genuinely blocked, you may
|
||||
inspect broader repository docs, examples, tests, and source. Report what you
|
||||
read; observed tool calls will also be retained for audit.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Implement prompt composition and hashes**
|
||||
|
||||
Create `prompts.py` with a `RenderedPrompt` frozen dataclass, SHA-256 helper,
|
||||
placeholder replacement, and concatenation in this order:
|
||||
|
||||
```text
|
||||
base prompt
|
||||
profile fragment
|
||||
challenge prompt
|
||||
```
|
||||
|
||||
The dataclass must expose `text`, `base_sha256`, `profile_sha256`,
|
||||
`challenge_sha256`, and `rendered_sha256`. Reject unresolved `{{...}}`
|
||||
placeholders with `ValueError`.
|
||||
|
||||
- [ ] **Step 4: Extend workspace preparation**
|
||||
|
||||
Add a `V2TrialWorkspace` dataclass with:
|
||||
|
||||
```python
|
||||
root: Path
|
||||
config_path: Path
|
||||
rendered_prompt_path: Path
|
||||
instruction_files: tuple[Path, ...]
|
||||
```
|
||||
|
||||
Implement `prepare_v2_trial_workspace` to:
|
||||
|
||||
- create the unique workspace;
|
||||
- copy the challenge template;
|
||||
- write the local config;
|
||||
- for `skills` and `all`, load the explicit bundle manifest and `copy2` each
|
||||
file under `.agent/skills/<destination>`;
|
||||
- for `none`, copy no instruction files;
|
||||
- never use symbolic links or junctions.
|
||||
|
||||
- [ ] **Step 5: Run profile tests and commit**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k "prompt or profile" -q
|
||||
git add examples/agent_challenges/base-prompt.md examples/agent_challenges/profile-prompts examples/agent_challenges/prompts.py examples/agent_challenges/workspace.py tests/examples/test_agent_challenge_harness_v2.py
|
||||
git commit -m "feat: add challenge instruction profiles"
|
||||
```
|
||||
|
||||
### Task 3: Normalize OpenCode Tool And Token Events
|
||||
|
||||
**Files:**
|
||||
- Create: `examples/agent_challenges/metrics.py`
|
||||
- Test: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||
|
||||
- [ ] **Step 1: Add a realistic JSONL fixture test**
|
||||
|
||||
Append a test using four JSON lines: `step_start`, successful `tool_use` for
|
||||
`read`, failed `tool_use` for `bash`, and `step_finish` with token/cost data.
|
||||
Assert:
|
||||
|
||||
```python
|
||||
metrics.step_count == 1
|
||||
metrics.tool_call_count == 2
|
||||
metrics.failed_tool_call_count == 1
|
||||
metrics.tool_counts == {"bash": 1, "read": 1}
|
||||
metrics.tokens.total == 120
|
||||
metrics.tokens.input == 20
|
||||
metrics.tokens.output == 30
|
||||
metrics.tokens.reasoning == 10
|
||||
metrics.tokens.cache_read == 60
|
||||
metrics.cost == 0.01
|
||||
metrics.tool_calls[0].tool == "read"
|
||||
metrics.tool_calls[0].output_chars == 4000
|
||||
len(metrics.tool_calls[0].output_preview) <= 500
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run and verify the metrics import fails**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k metrics -q
|
||||
```
|
||||
|
||||
Expected: import failure for `metrics`.
|
||||
|
||||
- [ ] **Step 3: Implement event parsing DTOs**
|
||||
|
||||
In `metrics.py`, define frozen dataclasses:
|
||||
|
||||
```python
|
||||
TokenMetrics(total, input, output, reasoning, cache_read, cache_write)
|
||||
ToolCallEvidence(ordinal, call_id, tool, status, title, input, metadata,
|
||||
output_chars, output_preview, output_sha256, failed)
|
||||
TrialMetrics(step_count, tool_call_count, failed_tool_call_count, tool_counts,
|
||||
tokens, cost, unknown_event_count, tool_calls)
|
||||
```
|
||||
|
||||
Implement `extract_trial_metrics(stdout: str, *, preview_chars: int = 500)`:
|
||||
|
||||
- parse each nonblank line independently as JSON;
|
||||
- preserve malformed line count as unknown evidence instead of aborting;
|
||||
- count `step_finish` events and sum numeric token fields;
|
||||
- read nested cache `read`/`write` fields;
|
||||
- sum numeric `cost` values;
|
||||
- normalize `tool_use.part` and `part.state` fields;
|
||||
- hash full string output with SHA-256 and store only the bounded preview in
|
||||
normalized metrics;
|
||||
- retain unknown event count.
|
||||
|
||||
- [ ] **Step 4: Add JSON serialization helper**
|
||||
|
||||
Implement `metrics_payload(metrics: TrialMetrics) -> dict[str, Any]` using
|
||||
`dataclasses.asdict`, with tool-count keys sorted for deterministic output.
|
||||
|
||||
- [ ] **Step 5: Run metrics tests and commit**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k metrics -q
|
||||
git add examples/agent_challenges/metrics.py tests/examples/test_agent_challenge_harness_v2.py
|
||||
git commit -m "feat: extract opencode trial metrics"
|
||||
```
|
||||
|
||||
### Task 4: Derive Policy Evidence From Tool Calls
|
||||
|
||||
**Files:**
|
||||
- Create: `examples/agent_challenges/policy.py`
|
||||
- Test: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||
|
||||
- [ ] **Step 1: Add path-category and validity tests**
|
||||
|
||||
Create tool evidence fixtures for reads of:
|
||||
|
||||
- current workspace file;
|
||||
- `.agent/skills/wf-cli/SKILL.md`;
|
||||
- repository `src/wf_cli/app.py`;
|
||||
- repository `tests/wf_cli/test_app.py`;
|
||||
- another trial workspace;
|
||||
- prior `.wf_*` store;
|
||||
- opaque `bash` command.
|
||||
|
||||
Assert:
|
||||
|
||||
- `none` with source read is `contaminated` and records the path;
|
||||
- `skills` permits copied skill reads but source/test reads contaminate;
|
||||
- `all` permits source/test reads and sets `escalated_to_product_code=True`
|
||||
without contamination;
|
||||
- opaque shell commands produce `unauditable` only when no stronger
|
||||
contamination result exists.
|
||||
|
||||
- [ ] **Step 2: Implement policy DTO and categorization**
|
||||
|
||||
Create:
|
||||
|
||||
```python
|
||||
class EvaluationValidity(StrEnum):
|
||||
CLEAN = "clean"
|
||||
CONTAMINATED = "contaminated"
|
||||
UNAUDITABLE = "unauditable"
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PolicyEvidence:
|
||||
validity: EvaluationValidity
|
||||
disallowed_reads: tuple[str, ...]
|
||||
escalated_to_product_code: bool
|
||||
opaque_shell_commands: tuple[str, ...]
|
||||
reads_by_category: dict[str, tuple[str, ...]]
|
||||
```
|
||||
|
||||
Implement `evaluate_policy(profile, tool_calls, *, workspace_root,
|
||||
repository_root, workspaces_root)` using normalized tool names/inputs.
|
||||
|
||||
Recognize structured read/search inputs for tools named `read`, `glob`,
|
||||
`grep`, `list`, and equivalent names already observed in saved OpenCode output.
|
||||
Classify resolved paths into workspace, supplied skills, docs, examples, tests,
|
||||
source, adjacent attempts, prior stores, or outside/unknown.
|
||||
|
||||
For shell tools, store the command text. Recognize only clear literal path
|
||||
arguments; do not claim complete shell parsing. Commands that cannot be
|
||||
classified remain in `opaque_shell_commands`.
|
||||
|
||||
- [ ] **Step 3: Run policy tests and commit**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k policy -q
|
||||
git add examples/agent_challenges/policy.py tests/examples/test_agent_challenge_harness_v2.py
|
||||
git commit -m "feat: derive challenge policy evidence"
|
||||
```
|
||||
|
||||
### Task 5: Integrate Manifest Runner, Workspace CWD, And One-Hour Ceiling
|
||||
|
||||
**Files:**
|
||||
- Modify: `examples/agent_challenges/opencode_io.py`
|
||||
- Modify: `examples/agent_challenges/runner.py`
|
||||
- Create: `examples/agent_challenges/run_trials.py`
|
||||
- Test: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||
|
||||
- [ ] **Step 1: Add runner integration tests with injected subprocess**
|
||||
|
||||
Add tests that inject a fake run function and assert:
|
||||
|
||||
- default timeout is `3600`;
|
||||
- `cwd` is the generated trial workspace;
|
||||
- exactly one profile is present in config;
|
||||
- rendered prompt is persisted;
|
||||
- result JSON contains `instruction_profile`, prompt hashes, metrics, policy,
|
||||
repository commit/dirty marker, and raw stdout/stderr;
|
||||
- timeout preserves partial stdout and extracted partial metrics.
|
||||
|
||||
- [ ] **Step 2: Replace command construction input**
|
||||
|
||||
Change `TrialConfig`/`build_opencode_command` so command construction receives
|
||||
the already-rendered prompt text or reads `rendered_prompt_path`. Prompt
|
||||
composition must not happen inside `opencode_io.py`.
|
||||
|
||||
Keep:
|
||||
|
||||
```text
|
||||
opencode run [--attach URL] <rendered prompt> --format json --model ... --variant ...
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Refactor runner entrypoint**
|
||||
|
||||
The central `run_trials.py` CLI accepts:
|
||||
|
||||
```text
|
||||
--challenge PATH
|
||||
--instruction-profile none|skills|all
|
||||
--model MODEL
|
||||
--variant VARIANT
|
||||
--trials N
|
||||
--timeout-seconds 3600
|
||||
--attach URL
|
||||
--results-dir PATH
|
||||
--workspaces-dir PATH
|
||||
--instruction-bundle PATH
|
||||
--server-url URL
|
||||
--start-server
|
||||
--server-port PORT
|
||||
```
|
||||
|
||||
Require one explicit or default profile, never a profile list. Use one generated
|
||||
workspace per trial and pass that workspace as `cwd` to `subprocess.run`.
|
||||
|
||||
- [ ] **Step 4: Write normalized evidence after every terminal path**
|
||||
|
||||
For success, nonzero process exit, timeout, and parse error:
|
||||
|
||||
- write raw result JSON first;
|
||||
- extract metrics from available stdout;
|
||||
- evaluate policy from available tool calls;
|
||||
- write `metrics.json` in the workspace;
|
||||
- include prompt/bundle hashes and repository provenance;
|
||||
- call report generation;
|
||||
- never discard raw evidence if later stages fail.
|
||||
|
||||
- [ ] **Step 5: Run runner tests and direct help smoke**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k "runner or timeout" -q
|
||||
uv run python examples/agent_challenges/run_trials.py --help
|
||||
```
|
||||
|
||||
Expected: tests pass and help lists `--instruction-profile` and default timeout.
|
||||
|
||||
- [ ] **Step 6: Commit runner integration**
|
||||
|
||||
```powershell
|
||||
git add examples/agent_challenges/opencode_io.py examples/agent_challenges/runner.py examples/agent_challenges/run_trials.py tests/examples/test_agent_challenge_harness_v2.py
|
||||
git commit -m "feat: run profiled agent challenge trials"
|
||||
```
|
||||
|
||||
### Task 6: Generate Evidence-Rich Reports And Central Audit CLIs
|
||||
|
||||
**Files:**
|
||||
- Modify: `examples/agent_challenges/reports.py`
|
||||
- Modify: `examples/agent_challenges/audit.py`
|
||||
- Create: `examples/agent_challenges/save_trial_report.py`
|
||||
- Create: `examples/agent_challenges/save_manual_audit.py`
|
||||
- Test: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||
|
||||
- [ ] **Step 1: Add report snapshot assertions**
|
||||
|
||||
Build a small result payload and assert generated Markdown contains:
|
||||
|
||||
```text
|
||||
Instruction profile: skills
|
||||
Task outcome: success
|
||||
Evaluation validity: contaminated
|
||||
Duration
|
||||
Observed token metrics
|
||||
Tool calls by tool
|
||||
Disallowed reads
|
||||
Agent self-report discrepancies
|
||||
Final agent answer
|
||||
Manual audit: pending
|
||||
```
|
||||
|
||||
Assert full tool outputs are absent and previews are bounded.
|
||||
|
||||
- [ ] **Step 2: Separate task outcome from validity in reports**
|
||||
|
||||
Update `reports.py` to render these sections in order:
|
||||
|
||||
1. Trial identity/provenance.
|
||||
2. Task outcome and evaluation validity.
|
||||
3. Duration/token/cost metrics.
|
||||
4. Tool/command summary.
|
||||
5. Observed reads and policy findings.
|
||||
6. Agent self-report and observed discrepancies.
|
||||
7. Final agent answer.
|
||||
8. Manual-audit status.
|
||||
|
||||
Keep `report_from_result` and `save_report_from_result_payload` as generic
|
||||
helpers, but remove browser-click assumptions.
|
||||
|
||||
- [ ] **Step 3: Update manual audit payload**
|
||||
|
||||
`manual-audit.yaml` must include:
|
||||
|
||||
```yaml
|
||||
task_outcome: success
|
||||
evaluation_validity: clean
|
||||
official_outcome: pass
|
||||
auditor_notes: "..."
|
||||
```
|
||||
|
||||
The manual command can override automatic task/validity fields but must retain
|
||||
the automatic values under `automatic_evidence` for comparison.
|
||||
|
||||
- [ ] **Step 4: Add central wrapper CLIs**
|
||||
|
||||
`save_trial_report.py` delegates to `reports.main`; `save_manual_audit.py`
|
||||
delegates to `audit.main`. Both must support direct execution from repo root and
|
||||
display useful `--help` text.
|
||||
|
||||
- [ ] **Step 5: Run report/audit tests and commit**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k "report or audit" -q
|
||||
git add examples/agent_challenges/reports.py examples/agent_challenges/audit.py examples/agent_challenges/save_trial_report.py examples/agent_challenges/save_manual_audit.py tests/examples/test_agent_challenge_harness_v2.py
|
||||
git commit -m "feat: report agent challenge evidence"
|
||||
```
|
||||
|
||||
### Task 7: Final Generic Harness Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
- Move after completion: `docs/superpowers/plans/2026-06-22-agent-challenge-harness-v2.md` to `docs/historical/superpowers/plans/2026-06-22-agent-challenge-harness-v2.md`
|
||||
|
||||
- [ ] **Step 1: Run focused tests and static checks**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/examples/test_agent_challenge_harness_v2.py tests/examples/test_agent_challenge_skill_bundle.py -q
|
||||
uv run ruff check examples/agent_challenges tests/examples/test_agent_challenge_harness_v2.py tests/examples/test_agent_challenge_skill_bundle.py
|
||||
uv run ruff format --check examples/agent_challenges tests/examples/test_agent_challenge_harness_v2.py tests/examples/test_agent_challenge_skill_bundle.py
|
||||
uv run basedpyright --level error examples/agent_challenges tests/examples/test_agent_challenge_harness_v2.py tests/examples/test_agent_challenge_skill_bundle.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all pass; only accepted Windows CRLF warnings may appear.
|
||||
|
||||
- [ ] **Step 2: Add roadmap completion note**
|
||||
|
||||
```markdown
|
||||
- Completed: the generic agent challenge harness now supports data-driven
|
||||
manifests, layered prompts, explicit `none|skills|all` profiles, one-hour hard
|
||||
ceilings, normalized OpenCode tool/token evidence, policy findings, and
|
||||
manual-audited reports.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Archive and commit the plan**
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force docs/historical/superpowers/plans | Out-Null
|
||||
Move-Item docs/superpowers/plans/2026-06-22-agent-challenge-harness-v2.md docs/historical/superpowers/plans/2026-06-22-agent-challenge-harness-v2.md
|
||||
git add docs/current_roadmap.md docs/superpowers/plans/2026-06-22-agent-challenge-harness-v2.md docs/historical/superpowers/plans/2026-06-22-agent-challenge-harness-v2.md
|
||||
git commit -m "docs: record agent challenge harness v2"
|
||||
```
|
||||
Reference in New Issue
Block a user