"""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())