feat: blueprint-aware policy resolve for multi-instance tenants (T07)
resolve_run_policy returns blueprint for persona prepare; add tenant onboard helper script. Marks HARNESS-WP-0001-T07 done after binky onboarding handoff.
This commit is contained in:
parent
67f1791491
commit
154e535695
5 changed files with 135 additions and 13 deletions
|
|
@ -273,28 +273,28 @@ def resolve_run_policy(
|
||||||
agent_name: str,
|
agent_name: str,
|
||||||
*,
|
*,
|
||||||
default_profile: str = "green-commit-only",
|
default_profile: str = "green-commit-only",
|
||||||
) -> tuple[str, int | None, str | None]:
|
) -> tuple[str, int | None, str | None, str]:
|
||||||
"""Resolve (tool_profile_name, budget_tokens, lane) for a run.
|
"""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
|
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,
|
default profile with no budget and blueprint=agent_name. If the entry
|
||||||
raises UnknownToolProfileError (refuse to run).
|
names an unknown profile, raises UnknownToolProfileError (refuse to run).
|
||||||
"""
|
"""
|
||||||
root = Path(project_root)
|
root = Path(project_root)
|
||||||
path = manifest_path(root)
|
path = manifest_path(root)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
get_profile(default_profile) # validate default exists
|
get_profile(default_profile) # validate default exists
|
||||||
return default_profile, None, None
|
return default_profile, None, None, agent_name
|
||||||
|
|
||||||
manifest = load_manifest(path)
|
manifest = load_manifest(path)
|
||||||
instance = manifest.agent_for(agent_name)
|
instance = manifest.agent_for(agent_name)
|
||||||
if instance is None or not instance.enabled:
|
if instance is None or not instance.enabled:
|
||||||
get_profile(default_profile)
|
get_profile(default_profile)
|
||||||
return default_profile, None, None
|
return default_profile, None, None, agent_name
|
||||||
|
|
||||||
profile_name = instance.tool_profile or default_profile
|
profile_name = instance.tool_profile or default_profile
|
||||||
get_profile(profile_name) # raises if unknown
|
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]:
|
def known_profile_names() -> list[str]:
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ def run_task(
|
||||||
write_metrics: bool = True,
|
write_metrics: bool = True,
|
||||||
) -> RunResult:
|
) -> RunResult:
|
||||||
try:
|
try:
|
||||||
profile_name, budget_tokens, lane = resolve_run_policy(
|
profile_name, budget_tokens, lane, blueprint = resolve_run_policy(
|
||||||
spec.target_repo, spec.agent
|
spec.target_repo, spec.agent
|
||||||
)
|
)
|
||||||
profile = get_profile(profile_name)
|
profile = get_profile(profile_name)
|
||||||
|
|
@ -112,7 +112,7 @@ def run_task(
|
||||||
)
|
)
|
||||||
|
|
||||||
head_before = _git(spec.target_repo, "rev-parse", "HEAD")
|
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(
|
prompt = PROMPT_TEMPLATE.format(
|
||||||
persona=persona or "(no persona bundle available for this run)",
|
persona=persona or "(no persona bundle available for this run)",
|
||||||
title=spec.title,
|
title=spec.title,
|
||||||
|
|
|
||||||
111
scripts/tenant_onboard_runs.py
Normal file
111
scripts/tenant_onboard_runs.py
Normal file
|
|
@ -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())
|
||||||
|
|
@ -121,10 +121,11 @@ def test_harness_major_mismatch() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_run_policy_defaults(tmp_path: Path) -> 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 profile == "green-commit-only"
|
||||||
assert budget is None
|
assert budget is None
|
||||||
assert lane is None
|
assert lane is None
|
||||||
|
assert blueprint == "coach"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_run_policy_from_manifest(tmp_path: Path) -> None:
|
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",
|
"tool_profile": "blue-mail-triage",
|
||||||
"lane": "blue",
|
"lane": "blue",
|
||||||
"budget": 12000,
|
"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 profile == "blue-mail-triage"
|
||||||
assert budget == 12000
|
assert budget == 12000
|
||||||
assert lane == "blue"
|
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:
|
def test_resolve_unknown_profile_refuses(tmp_path: Path) -> None:
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ cron-bridge cutover.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: HARNESS-WP-0001-T07
|
id: HARNESS-WP-0001-T07
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "3b724966-3481-405f-a483-80276f39cf1b"
|
state_hub_task_id: "3b724966-3481-405f-a483-80276f39cf1b"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue