agent_harness -> rein_aharness (package + all imports), CLI command agent-harness -> rein-aharness, Docker image tag, k8s namespace/labels/ names, Makefile targets, deploy script env var/paths. In-repo identity strings (hub event source, metrics harness field, default assignee, argparse prog name, commit author identity) updated to match. Historical documents left untouched on purpose: docs/adr/ADR-001-agent-harness-architecture.md, docs/architecture.md (dated v0.1 snapshot), workplans/HARNESS-WP-0001 (completed under the old name), and the SSH host alias "forgejo-agent-harness" (external ~/.ssh/config entry, not owned here). Verified: 47/47 tests pass, CLI runs correctly from a fresh venv, `make image` builds and the resulting container runs correctly. deploy/README.md gained an explicit rename cutover checklist for what this session cannot safely do unattended -- moving the host-side secrets dir and checkout on railiance01, and not deleting the old k8s namespace until the new one is confirmed working. The actual live cutover (running that checklist against the real Railiance deployment) is not attempted here -- real production surgery on binky-control's live automation, needs the operator present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
222 lines
7 KiB
Python
222 lines
7 KiB
Python
"""One-task run orchestration.
|
|
|
|
Flow: lock target repo → resolve tool profile / budget from instance
|
|
manifest → snapshot HEAD → persona bundle → prompt → agentic session →
|
|
verify a new commit exists → kaizen metrics + hub progress event
|
|
(+ task close). The run *fails* if the session pushed anywhere or left
|
|
the repo dirty in a way it should not — the worker never pushes;
|
|
publishing is a separate, explicitly-granted lane.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from rein_aharness import hub, metrics
|
|
from rein_aharness.manifest import resolve_run_policy
|
|
from rein_aharness.persona import load_persona_bundle
|
|
from rein_aharness.profiles import UnknownToolProfileError, get_profile
|
|
from rein_aharness.taskspec import TaskSpec
|
|
|
|
PROMPT_TEMPLATE = """\
|
|
You are an unattended executor session (agent persona below, if any).
|
|
Operating rules, non-negotiable:
|
|
- Work ONLY inside the current repository working directory.
|
|
- Green/Blue lane: file edits and local git add/commit only. Never push,
|
|
never touch the network, never run destructive commands.
|
|
- Tool profile for this run: {tool_profile} (lane={lane}).
|
|
- Bounded effort: complete the single task below, commit with a clear
|
|
message, then stop. If the task cannot be completed, commit nothing and
|
|
say why in your final output.
|
|
|
|
{persona}
|
|
|
|
## Task: {title}
|
|
|
|
{description}
|
|
"""
|
|
|
|
|
|
@dataclass
|
|
class RunResult:
|
|
ok: bool
|
|
committed: bool
|
|
head_before: str
|
|
head_after: str
|
|
persona_source: str
|
|
session_output: str
|
|
reason: str = ""
|
|
tool_profile: str = ""
|
|
budget_tokens: int | None = None
|
|
tokens_spent: int | None = None
|
|
execution_time_s: float = 0.0
|
|
|
|
|
|
def _git(repo: Path, *args: str) -> str:
|
|
result = subprocess.run(
|
|
["git", "-C", str(repo), *args],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"git {' '.join(args)} failed: {result.stderr.strip()}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def run_task(
|
|
spec: TaskSpec,
|
|
adapter=None,
|
|
report_to_hub: bool = True,
|
|
write_metrics: bool = True,
|
|
) -> RunResult:
|
|
try:
|
|
profile_name, budget_tokens, lane, blueprint = resolve_run_policy(
|
|
spec.target_repo, spec.agent
|
|
)
|
|
profile = get_profile(profile_name)
|
|
except UnknownToolProfileError as exc:
|
|
return RunResult(
|
|
ok=False,
|
|
committed=False,
|
|
head_before="",
|
|
head_after="",
|
|
persona_source="none",
|
|
session_output="",
|
|
reason=f"refused: {exc}",
|
|
tool_profile="",
|
|
budget_tokens=None,
|
|
)
|
|
except Exception as exc:
|
|
return RunResult(
|
|
ok=False,
|
|
committed=False,
|
|
head_before="",
|
|
head_after="",
|
|
persona_source="none",
|
|
session_output="",
|
|
reason=f"manifest resolution failed: {exc}",
|
|
tool_profile="",
|
|
budget_tokens=None,
|
|
)
|
|
|
|
if adapter is None:
|
|
from rein_aharness.adapter import AgenticClaudeCodeAdapter
|
|
|
|
adapter = AgenticClaudeCodeAdapter(
|
|
workdir=spec.target_repo,
|
|
tool_profile=profile,
|
|
)
|
|
|
|
head_before = _git(spec.target_repo, "rev-parse", "HEAD")
|
|
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,
|
|
description=spec.description,
|
|
tool_profile=profile.name,
|
|
lane=lane or profile.lane,
|
|
)
|
|
|
|
from llm_connect.models import BudgetTracker, RunConfig
|
|
|
|
budget_tracker = BudgetTracker(total=budget_tokens) if budget_tokens else None
|
|
config = RunConfig(
|
|
timeout_seconds=spec.timeout_seconds,
|
|
skip_if_exists=False,
|
|
budget_tracker=budget_tracker,
|
|
)
|
|
|
|
started = time.monotonic()
|
|
try:
|
|
response = adapter.execute_prompt(prompt, config)
|
|
session_output = response.content
|
|
session_ok = True
|
|
reason = ""
|
|
except Exception as exc: # adapter / budget failures must still be reported
|
|
session_output = ""
|
|
session_ok = False
|
|
reason = f"session failed: {exc}"
|
|
execution_time_s = time.monotonic() - started
|
|
|
|
head_after = _git(spec.target_repo, "rev-parse", "HEAD")
|
|
committed = head_after != head_before
|
|
ok = session_ok and committed
|
|
if session_ok and not committed:
|
|
reason = "session completed without committing"
|
|
|
|
tokens_spent = budget_tracker.spent if budget_tracker is not None else None
|
|
|
|
result = RunResult(
|
|
ok=ok,
|
|
committed=committed,
|
|
head_before=head_before,
|
|
head_after=head_after,
|
|
persona_source=persona_source,
|
|
session_output=session_output,
|
|
reason=reason,
|
|
tool_profile=profile.name,
|
|
budget_tokens=budget_tokens,
|
|
tokens_spent=tokens_spent,
|
|
execution_time_s=execution_time_s,
|
|
)
|
|
|
|
if write_metrics:
|
|
try:
|
|
metrics.record_execution(
|
|
spec.target_repo,
|
|
spec.agent,
|
|
success=ok,
|
|
execution_time_s=execution_time_s,
|
|
tokens=tokens_spent,
|
|
committed=committed,
|
|
head_after=head_after,
|
|
reason=reason or None,
|
|
metadata={
|
|
"task_title": spec.title,
|
|
"tool_profile": profile.name,
|
|
"labels": list(spec.labels),
|
|
"completion_event_type": spec.completion_event_type,
|
|
},
|
|
)
|
|
except OSError:
|
|
pass # metrics must not block run completion reporting
|
|
|
|
if report_to_hub:
|
|
detail = {
|
|
"repo": spec.target_repo.name,
|
|
"task_title": spec.title,
|
|
"agent": spec.agent,
|
|
"labels": spec.labels,
|
|
"persona_source": persona_source,
|
|
"committed": committed,
|
|
"head_after": head_after,
|
|
"ok": ok,
|
|
"reason": reason,
|
|
"tool_profile": profile.name,
|
|
"budget_tokens": budget_tokens,
|
|
"tokens_spent": tokens_spent,
|
|
"execution_time_s": round(execution_time_s, 3),
|
|
}
|
|
hub.post_progress_event(
|
|
summary=f"executor run: {spec.title} ({'ok' if ok else 'failed'})",
|
|
event_type=spec.completion_event_type,
|
|
detail=detail,
|
|
task_id=spec.hub_task_id,
|
|
)
|
|
if tokens_spent is not None or budget_tokens is not None:
|
|
hub.post_token_event(
|
|
repo=spec.target_repo.name,
|
|
tokens=tokens_spent or 0,
|
|
budget=budget_tokens,
|
|
agent=spec.agent,
|
|
ok=ok,
|
|
detail={"task_title": spec.title, "tool_profile": profile.name},
|
|
)
|
|
if ok and spec.hub_task_id:
|
|
hub.close_task(spec.hub_task_id)
|
|
|
|
return result
|