feat: instance manifest, tool profiles, metrics, and budget enforcement

Land HARNESS-WP-0001 T01/T02/T04/T05: extend ADR-005 schedule.yml with
harness fields, named tool-profile registry, ADR-004 metrics writes, and
BudgetTracker wiring. CLI gains validate/profiles; task-file path kept.
This commit is contained in:
tegwick 2026-07-17 23:49:03 +02:00
parent 16f5c29c08
commit 4144eba160
17 changed files with 1387 additions and 60 deletions

215
tests/test_manifest.py Normal file
View file

@ -0,0 +1,215 @@
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from agent_harness.cli import main
from agent_harness.manifest import (
HARNESS_MAJOR,
ManifestError,
load_manifest,
parse_manifest,
resolve_run_policy,
validate_manifest,
)
from agent_harness.profiles import UnknownToolProfileError, get_profile, list_profiles
def _write_manifest(tmp_path: Path, data: dict) -> Path:
kaizen = tmp_path / ".kaizen"
kaizen.mkdir()
path = kaizen / "schedule.yml"
path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
return path
def test_parse_and_validate_adr005_only() -> None:
manifest = parse_manifest(
{
"version": "1",
"timezone": "Europe/Berlin",
"agents": {
"coach": {"cadence": "weekly", "enabled": True},
},
}
)
assert validate_manifest(manifest) == []
assert validate_manifest(manifest, require_harness_fields=True)
def test_validate_harness_extensions() -> None:
manifest = parse_manifest(
{
"version": "1",
"harness": HARNESS_MAJOR,
"agents": {
"coach": {
"cadence": "daily",
"enabled": True,
"lane": "green",
"tool_profile": "green-commit-only",
"budget": 50000,
},
"mail": {
"cadence": "weekly",
"lane": "blue",
"tool_profile": "blue-mail-triage",
"budget": 10000,
},
},
}
)
assert validate_manifest(manifest) == []
assert validate_manifest(manifest, require_harness_fields=True) == []
def test_unknown_tool_profile_errors() -> None:
manifest = parse_manifest(
{
"version": "1",
"agents": {
"coach": {
"cadence": "daily",
"tool_profile": "does-not-exist",
}
},
}
)
errors = validate_manifest(manifest)
assert any("unknown tool_profile" in e for e in errors)
def test_lane_profile_mismatch() -> None:
manifest = parse_manifest(
{
"version": "1",
"agents": {
"coach": {
"cadence": "daily",
"lane": "blue",
"tool_profile": "green-commit-only",
}
},
}
)
errors = validate_manifest(manifest)
assert any("does not match tool_profile" in e for e in errors)
def test_budget_must_be_positive() -> None:
with pytest.raises(ManifestError, match="budget"):
parse_manifest(
{
"version": "1",
"agents": {"coach": {"cadence": "daily", "budget": 0}},
}
)
def test_harness_major_mismatch() -> None:
manifest = parse_manifest(
{
"version": "1",
"harness": HARNESS_MAJOR + 1,
"agents": {"coach": {"cadence": "daily"}},
}
)
errors = validate_manifest(manifest)
assert any("does not match this runtime" in e for e in errors)
def test_resolve_run_policy_defaults(tmp_path: Path) -> None:
profile, budget, lane = resolve_run_policy(tmp_path, "coach")
assert profile == "green-commit-only"
assert budget is None
assert lane is None
def test_resolve_run_policy_from_manifest(tmp_path: Path) -> None:
_write_manifest(
tmp_path,
{
"version": "1",
"harness": HARNESS_MAJOR,
"agents": {
"coach": {
"cadence": "daily",
"tool_profile": "blue-mail-triage",
"lane": "blue",
"budget": 12000,
}
},
},
)
profile, budget, lane = resolve_run_policy(tmp_path, "coach")
assert profile == "blue-mail-triage"
assert budget == 12000
assert lane == "blue"
def test_resolve_unknown_profile_refuses(tmp_path: Path) -> None:
_write_manifest(
tmp_path,
{
"version": "1",
"agents": {
"coach": {
"cadence": "daily",
"tool_profile": "nope",
}
},
},
)
with pytest.raises(UnknownToolProfileError):
resolve_run_policy(tmp_path, "coach")
def test_load_manifest_missing(tmp_path: Path) -> None:
with pytest.raises(ManifestError, match="not found"):
load_manifest(tmp_path / ".kaizen" / "schedule.yml")
def test_profiles_registry() -> None:
names = {p.name for p in list_profiles()}
assert names == {"green-commit-only", "blue-mail-triage"}
green = get_profile("green-commit-only")
assert "git commit" in green.allowed_tools
assert "git push" not in green.allowed_tools
def test_cli_validate_ok(tmp_path: Path) -> None:
_write_manifest(
tmp_path,
{
"version": "1",
"harness": HARNESS_MAJOR,
"agents": {
"coach": {
"cadence": "daily",
"lane": "green",
"tool_profile": "green-commit-only",
"budget": 1000,
}
},
},
)
assert main(["validate", "--target", str(tmp_path), "--strict"]) == 0
def test_cli_validate_fails_unknown_profile(tmp_path: Path) -> None:
_write_manifest(
tmp_path,
{
"version": "1",
"agents": {
"coach": {"cadence": "daily", "tool_profile": "missing"}
},
},
)
assert main(["validate", "--target", str(tmp_path)]) == 1
def test_cli_profiles() -> None:
assert main(["profiles"]) == 0

51
tests/test_metrics.py Normal file
View file

@ -0,0 +1,51 @@
from __future__ import annotations
import json
from pathlib import Path
from agent_harness.metrics import record_execution, regenerate_summary
def test_record_execution_writes_jsonl_and_summary(tmp_path: Path) -> None:
path = record_execution(
tmp_path,
"coach",
success=True,
execution_time_s=12.5,
tokens=100,
committed=True,
head_after="abc123",
reason=None,
metadata={"task_title": "hello"},
)
assert path.is_file()
lines = path.read_text(encoding="utf-8").strip().splitlines()
assert len(lines) == 1
rec = json.loads(lines[0])
assert rec["agent"] == "coach"
assert rec["success"] is True
assert rec["tokens"] == 100
assert rec["harness"] == "agent-harness"
assert rec["metadata"]["task_title"] == "hello"
summary = json.loads(
(tmp_path / ".kaizen" / "metrics" / "coach" / "summary.json").read_text()
)
assert summary["execution_count"] == 1
assert summary["success_rate"] == 1.0
assert summary["avg_execution_time_s"] == 12.5
def test_summary_aggregates_multiple(tmp_path: Path) -> None:
record_execution(tmp_path, "coach", success=True, execution_time_s=10)
record_execution(tmp_path, "coach", success=False, execution_time_s=20)
summary = json.loads(
(tmp_path / ".kaizen" / "metrics" / "coach" / "summary.json").read_text()
)
assert summary["execution_count"] == 2
assert summary["success_rate"] == 0.5
assert summary["avg_execution_time_s"] == 15.0
def test_regenerate_summary_empty() -> None:
assert regenerate_summary("x", [])["execution_count"] == 0

View file

@ -1,10 +1,13 @@
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
import yaml
from agent_harness.manifest import HARNESS_MAJOR
from agent_harness.runner import RunResult, run_task
from agent_harness.taskspec import TaskSpec, TaskSpecError
@ -23,15 +26,26 @@ def _make_repo(tmp_path: Path) -> Path:
return repo
def _write_manifest(repo: Path, agents: dict) -> None:
kaizen = repo / ".kaizen"
kaizen.mkdir(exist_ok=True)
data = {"version": "1", "harness": HARNESS_MAJOR, "agents": agents}
(kaizen / "schedule.yml").write_text(
yaml.safe_dump(data, sort_keys=False), encoding="utf-8"
)
class CommittingAdapter:
"""Fake adapter that simulates a session which commits."""
def __init__(self, repo: Path):
self.repo = repo
self.prompts: list[str] = []
self.configs: list = []
def execute_prompt(self, prompt, config):
self.prompts.append(prompt)
self.configs.append(config)
(self.repo / "HELLO.md").write_text("hello\n")
subprocess.run(["git", "add", "."], cwd=self.repo, check=True)
subprocess.run(
@ -51,34 +65,140 @@ class IdleAdapter:
return LLMResponse(content="nothing to do", model="fake", usage={}, finish_reason="stop")
def _spec(repo: Path) -> TaskSpec:
return TaskSpec(title="write hello", description="create HELLO.md", target_repo=repo)
class BudgetBlowingAdapter:
def execute_prompt(self, prompt, config):
from llm_connect.exceptions import LLMBudgetExceededError
if config.budget_tracker is not None:
# Simulate preflight/exhaustion the way adapters do.
config.budget_tracker.consume(config.budget_tracker.total)
config.budget_tracker.consume(1)
raise LLMBudgetExceededError(
"Token budget exceeded",
total=1,
spent=1,
requested=1,
)
def _spec(repo: Path, agent: str = "coach") -> TaskSpec:
return TaskSpec(
title="write hello",
description="create HELLO.md",
target_repo=repo,
agent=agent,
)
def test_run_task_success_when_session_commits(tmp_path) -> None:
repo = _make_repo(tmp_path)
adapter = CommittingAdapter(repo)
result = run_task(_spec(repo), adapter=adapter, report_to_hub=False)
result = run_task(
_spec(repo), adapter=adapter, report_to_hub=False, write_metrics=True
)
assert isinstance(result, RunResult)
assert result.ok is True
assert result.committed is True
assert result.head_before != result.head_after
assert result.tool_profile == "green-commit-only"
assert "write hello" in adapter.prompts[0]
assert "Never push" in adapter.prompts[0]
assert "Tool profile for this run: green-commit-only" in adapter.prompts[0]
metrics_path = repo / ".kaizen" / "metrics" / "coach" / "executions.jsonl"
assert metrics_path.is_file()
rec = json.loads(metrics_path.read_text().strip().splitlines()[-1])
assert rec["success"] is True
assert rec["committed"] is True
def test_run_task_fails_without_commit(tmp_path) -> None:
repo = _make_repo(tmp_path)
result = run_task(_spec(repo), adapter=IdleAdapter(), report_to_hub=False)
result = run_task(
_spec(repo), adapter=IdleAdapter(), report_to_hub=False, write_metrics=False
)
assert result.ok is False
assert result.committed is False
assert result.reason == "session completed without committing"
def test_run_task_refuses_unknown_tool_profile(tmp_path) -> None:
repo = _make_repo(tmp_path)
_write_manifest(
repo,
{
"coach": {
"cadence": "daily",
"tool_profile": "not-a-real-profile",
}
},
)
result = run_task(
_spec(repo), adapter=IdleAdapter(), report_to_hub=False, write_metrics=False
)
assert result.ok is False
assert result.reason.startswith("refused:")
assert "not-a-real-profile" in result.reason
def test_run_task_resolves_manifest_profile_and_budget(tmp_path) -> None:
repo = _make_repo(tmp_path)
_write_manifest(
repo,
{
"coach": {
"cadence": "daily",
"lane": "blue",
"tool_profile": "blue-mail-triage",
"budget": 25000,
}
},
)
adapter = CommittingAdapter(repo)
result = run_task(
_spec(repo), adapter=adapter, report_to_hub=False, write_metrics=False
)
assert result.ok is True
assert result.tool_profile == "blue-mail-triage"
assert result.budget_tokens == 25000
assert adapter.configs[0].budget_tracker is not None
assert adapter.configs[0].budget_tracker.total == 25000
assert "blue-mail-triage" in adapter.prompts[0]
def test_run_task_budget_exhaustion_fails(tmp_path) -> None:
repo = _make_repo(tmp_path)
_write_manifest(
repo,
{
"coach": {
"cadence": "daily",
"tool_profile": "green-commit-only",
"budget": 10,
}
},
)
result = run_task(
_spec(repo),
adapter=BudgetBlowingAdapter(),
report_to_hub=False,
write_metrics=False,
)
assert result.ok is False
assert "session failed" in result.reason
assert result.budget_tokens == 10
def test_taskspec_rejects_non_repo(tmp_path) -> None:
spec_file = tmp_path / "task.json"
spec_file.write_text(