diff --git a/agent_harness/manifest.py b/agent_harness/manifest.py index ebc7bee..1d532b6 100644 --- a/agent_harness/manifest.py +++ b/agent_harness/manifest.py @@ -273,28 +273,28 @@ def resolve_run_policy( agent_name: str, *, default_profile: str = "green-commit-only", -) -> tuple[str, int | None, str | None]: - """Resolve (tool_profile_name, budget_tokens, lane) for a run. +) -> tuple[str, int | None, str | None, str]: + """Resolve (tool_profile_name, budget_tokens, lane, blueprint) for a run. If the repo has no manifest or no entry for *agent_name*, returns the - default profile with no budget. If the entry names an unknown profile, - raises UnknownToolProfileError (refuse to run). + default profile with no budget and blueprint=agent_name. If the entry + names an unknown profile, raises UnknownToolProfileError (refuse to run). """ root = Path(project_root) path = manifest_path(root) if not path.exists(): get_profile(default_profile) # validate default exists - return default_profile, None, None + return default_profile, None, None, agent_name manifest = load_manifest(path) instance = manifest.agent_for(agent_name) if instance is None or not instance.enabled: get_profile(default_profile) - return default_profile, None, None + return default_profile, None, None, agent_name profile_name = instance.tool_profile or default_profile get_profile(profile_name) # raises if unknown - return profile_name, instance.budget, instance.lane + return profile_name, instance.budget, instance.lane, instance.blueprint_name def known_profile_names() -> list[str]: diff --git a/agent_harness/runner.py b/agent_harness/runner.py index 19766a2..7248f01 100644 --- a/agent_harness/runner.py +++ b/agent_harness/runner.py @@ -74,7 +74,7 @@ def run_task( write_metrics: bool = True, ) -> RunResult: try: - profile_name, budget_tokens, lane = resolve_run_policy( + profile_name, budget_tokens, lane, blueprint = resolve_run_policy( spec.target_repo, spec.agent ) profile = get_profile(profile_name) @@ -112,7 +112,7 @@ def run_task( ) head_before = _git(spec.target_repo, "rev-parse", "HEAD") - persona, persona_source = load_persona_bundle(spec.agent, spec.target_repo) + persona, persona_source = load_persona_bundle(blueprint, spec.target_repo) prompt = PROMPT_TEMPLATE.format( persona=persona or "(no persona bundle available for this run)", title=spec.title, diff --git a/scripts/tenant_onboard_runs.py b/scripts/tenant_onboard_runs.py new file mode 100644 index 0000000..1f129d8 --- /dev/null +++ b/scripts/tenant_onboard_runs.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Run tenant onboarding task specs through the harness (deterministic adapter). + +Used for HARNESS-WP-0001-T07 when proving instance entries + hub completion +events without a full Claude Code session. Each run commits a small evidence +file under history/harness-onboarding/ and posts the task's completion_event_type. +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +# Allow running from repo root without install. +_ROOT = Path(__file__).resolve().parents[1] +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +from agent_harness.runner import run_task +from agent_harness.taskspec import TaskSpec + + +class EvidenceAdapter: + def __init__(self, repo: Path, slug: str, event_type: str): + self.repo = repo + self.slug = slug + self.event_type = event_type + self.prompts: list[str] = [] + + def execute_prompt(self, prompt, config): + self.prompts.append(prompt) + out_dir = self.repo / "history" / "harness-onboarding" + out_dir.mkdir(parents=True, exist_ok=True) + path = out_dir / f"{self.slug}.md" + path.write_text( + f"# Harness onboarding run — {self.slug}\n\n" + f"- completion_event_type: `{self.event_type}`\n" + f"- tool profile resolved from instance manifest\n" + f"- deterministic adapter (T07 proof; not a full agentic session)\n", + encoding="utf-8", + ) + subprocess.run(["git", "add", str(path.relative_to(self.repo))], cwd=self.repo, check=True) + subprocess.run( + [ + "git", + "-c", + "user.email=agent-harness@local", + "-c", + "user.name=agent-harness", + "commit", + "-qm", + f"harness onboard: {self.slug}", + ], + cwd=self.repo, + check=True, + ) + from llm_connect.models import LLMResponse + + return LLMResponse( + content=f"onboard {self.slug} ok", + model="onboard-adapter", + usage={}, + finish_reason="stop", + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--task-file", + action="append", + required=True, + help="Task JSON path (repeatable)", + ) + parser.add_argument("--no-hub", action="store_true") + args = parser.parse_args(argv) + + results = [] + for task_path in args.task_file: + spec = TaskSpec.from_file(task_path) + slug = Path(task_path).stem + adapter = EvidenceAdapter(spec.target_repo, slug, spec.completion_event_type) + result = run_task( + spec, + adapter=adapter, + report_to_hub=not args.no_hub, + write_metrics=True, + ) + results.append( + { + "task_file": task_path, + "ok": result.ok, + "committed": result.committed, + "tool_profile": result.tool_profile, + "event": spec.completion_event_type, + "reason": result.reason, + "head_after": result.head_after, + } + ) + print(json.dumps(results[-1], indent=2)) + if not result.ok: + return 1 + print(json.dumps({"all_ok": True, "runs": len(results)}, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 7bae098..11b56a5 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -121,10 +121,11 @@ def test_harness_major_mismatch() -> None: def test_resolve_run_policy_defaults(tmp_path: Path) -> None: - profile, budget, lane = resolve_run_policy(tmp_path, "coach") + profile, budget, lane, blueprint = resolve_run_policy(tmp_path, "coach") assert profile == "green-commit-only" assert budget is None assert lane is None + assert blueprint == "coach" def test_resolve_run_policy_from_manifest(tmp_path: Path) -> None: @@ -139,14 +140,24 @@ def test_resolve_run_policy_from_manifest(tmp_path: Path) -> None: "tool_profile": "blue-mail-triage", "lane": "blue", "budget": 12000, - } + }, + "mail-triage": { + "cadence": "weekly", + "blueprint": "coach", + "tool_profile": "blue-mail-triage", + "lane": "blue", + "budget": 1000, + }, }, }, ) - profile, budget, lane = resolve_run_policy(tmp_path, "coach") + profile, budget, lane, blueprint = resolve_run_policy(tmp_path, "coach") assert profile == "blue-mail-triage" assert budget == 12000 assert lane == "blue" + assert blueprint == "coach" + _, _, _, bp2 = resolve_run_policy(tmp_path, "mail-triage") + assert bp2 == "coach" def test_resolve_unknown_profile_refuses(tmp_path: Path) -> None: diff --git a/workplans/HARNESS-WP-0001-harness-foundation.md b/workplans/HARNESS-WP-0001-harness-foundation.md index cd9ca5c..4af7795 100644 --- a/workplans/HARNESS-WP-0001-harness-foundation.md +++ b/workplans/HARNESS-WP-0001-harness-foundation.md @@ -105,7 +105,7 @@ cron-bridge cutover. ```task id: HARNESS-WP-0001-T07 -status: todo +status: done priority: medium state_hub_task_id: "3b724966-3481-405f-a483-80276f39cf1b" ```