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.
171 lines
5.3 KiB
Python
171 lines
5.3 KiB
Python
"""CLI: validate instance manifests and execute exactly one task."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from agent_harness.manifest import (
|
|
ManifestError,
|
|
load_manifest_for_repo,
|
|
manifest_path,
|
|
validate_manifest,
|
|
)
|
|
from agent_harness.profiles import list_profiles
|
|
from agent_harness.runner import run_task
|
|
from agent_harness.taskspec import TaskSpec, TaskSpecError
|
|
|
|
|
|
def _cmd_validate(args: argparse.Namespace) -> int:
|
|
target = Path(args.target).expanduser().resolve()
|
|
path = manifest_path(target)
|
|
if not path.is_file():
|
|
print(f"error: no instance manifest at {path}", file=sys.stderr)
|
|
return 2
|
|
try:
|
|
manifest = load_manifest_for_repo(target)
|
|
except ManifestError as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 2
|
|
|
|
errors = validate_manifest(
|
|
manifest, require_harness_fields=bool(args.strict)
|
|
)
|
|
if errors:
|
|
print(f"invalid: {path}", file=sys.stderr)
|
|
for err in errors:
|
|
print(f" - {err}", file=sys.stderr)
|
|
return 1
|
|
|
|
enabled = manifest.enabled_agents()
|
|
print(f"ok: {path}")
|
|
print(f" version: {manifest.version}")
|
|
if manifest.timezone:
|
|
print(f" timezone: {manifest.timezone}")
|
|
if manifest.harness is not None:
|
|
print(f" harness major: {manifest.harness}")
|
|
print(f" agents: {len(manifest.agents)} ({len(enabled)} enabled)")
|
|
for agent in manifest.agents:
|
|
flags = []
|
|
if not agent.enabled:
|
|
flags.append("disabled")
|
|
if agent.tool_profile:
|
|
flags.append(f"profile={agent.tool_profile}")
|
|
if agent.lane:
|
|
flags.append(f"lane={agent.lane}")
|
|
if agent.budget is not None:
|
|
flags.append(f"budget={agent.budget}")
|
|
suffix = f" [{', '.join(flags)}]" if flags else ""
|
|
print(f" - {agent.name}: {agent.cadence}{suffix}")
|
|
return 0
|
|
|
|
|
|
def _cmd_profiles(_args: argparse.Namespace) -> int:
|
|
for profile in list_profiles():
|
|
print(f"{profile.name}\tlane={profile.lane}\t{profile.description}")
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(prog="agent-harness")
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
run = sub.add_parser("run", help="Execute one task from a JSON spec file")
|
|
run.add_argument("--task-file", required=True)
|
|
run.add_argument("--no-hub", action="store_true", help="Skip hub reporting")
|
|
run.add_argument(
|
|
"--no-metrics",
|
|
action="store_true",
|
|
help="Skip writing .kaizen/metrics in the target repo",
|
|
)
|
|
|
|
scan = sub.add_parser(
|
|
"mail-scan", help="Deterministic company-mailbox scan (no LLM session)"
|
|
)
|
|
scan.add_argument("--target-repo", required=True)
|
|
scan.add_argument("--config", default="integrations/mailbox-binky-company.yml")
|
|
scan.add_argument("--out", default="mailmeta/reports")
|
|
scan.add_argument("--no-hub", action="store_true", help="Skip hub reporting")
|
|
|
|
validate = sub.add_parser(
|
|
"validate",
|
|
help="Validate instance manifest (.kaizen/schedule.yml + harness fields)",
|
|
)
|
|
validate.add_argument(
|
|
"--target",
|
|
default=".",
|
|
help="Consuming repo root (default: cwd)",
|
|
)
|
|
validate.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
help="Require lane, tool_profile, and harness pin on every enabled agent",
|
|
)
|
|
|
|
sub.add_parser("profiles", help="List named tool profiles")
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.command == "validate":
|
|
return _cmd_validate(args)
|
|
|
|
if args.command == "profiles":
|
|
return _cmd_profiles(args)
|
|
|
|
if args.command == "mail-scan":
|
|
from agent_harness.mailscan import run_mail_scan
|
|
|
|
result = run_mail_scan(
|
|
target_repo=Path(args.target_repo).expanduser(),
|
|
config=args.config,
|
|
out_dir=args.out,
|
|
report_to_hub=not args.no_hub,
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"ok": result.ok,
|
|
"report": result.report_path,
|
|
"new_messages": result.new_messages,
|
|
"auth_lane": result.auth_lane,
|
|
"reason": result.reason,
|
|
},
|
|
indent=2,
|
|
)
|
|
)
|
|
return 0 if result.ok else 1
|
|
|
|
try:
|
|
spec = TaskSpec.from_file(args.task_file)
|
|
except (TaskSpecError, OSError, json.JSONDecodeError) as exc:
|
|
print(f"invalid task spec: {exc}", file=sys.stderr)
|
|
return 2
|
|
|
|
result = run_task(
|
|
spec,
|
|
report_to_hub=not args.no_hub,
|
|
write_metrics=not args.no_metrics,
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"ok": result.ok,
|
|
"committed": result.committed,
|
|
"head_after": result.head_after,
|
|
"persona_source": result.persona_source,
|
|
"tool_profile": result.tool_profile,
|
|
"budget_tokens": result.budget_tokens,
|
|
"tokens_spent": result.tokens_spent,
|
|
"execution_time_s": round(result.execution_time_s, 3),
|
|
"reason": result.reason,
|
|
},
|
|
indent=2,
|
|
)
|
|
)
|
|
return 0 if result.ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|