"""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 _cmd_poll(args: argparse.Namespace) -> int: from agent_harness.intake import IntakeError, IssueCoreClient, poll_next try: client = IssueCoreClient() result = poll_next(client, claim=not args.no_claim) except IntakeError as exc: print(f"intake error: {exc}", file=sys.stderr) return 2 if result is None: print(json.dumps({"queue": "empty"}, indent=2)) return 0 issue, spec = result print( json.dumps( { "issue_id": issue.issue_id, "state": issue.state, "title": issue.title, "agent": spec.agent, "target_repo": str(spec.target_repo), "completion_event_type": spec.completion_event_type, "labels": spec.labels, "claimed": not args.no_claim, }, indent=2, ) ) return 0 def _cmd_run(args: argparse.Namespace) -> int: from agent_harness.intake import IntakeError, IssueCoreClient, poll_next issue_id: str | None = None client: IssueCoreClient | None = None if args.from_issue_core: try: client = IssueCoreClient() polled = poll_next(client, claim=True) except IntakeError as exc: print(f"intake error: {exc}", file=sys.stderr) return 2 if polled is None: print(json.dumps({"ok": True, "queue": "empty"}, indent=2)) return 0 issue, spec = polled issue_id = issue.issue_id else: if not args.task_file: print( "error: provide --task-file or --from-issue-core", file=sys.stderr, ) return 2 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, ) closed = False close_error = "" if client is not None and issue_id: try: if result.ok: client.close(issue_id) closed = True else: client.reopen(issue_id) except IntakeError as exc: close_error = str(exc) 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, "issue_id": issue_id, "issue_closed": closed, "issue_close_error": close_error, }, indent=2, ) ) return 0 if result.ok else 1 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 file or next issue-core emission", ) run_src = run.add_mutually_exclusive_group(required=True) run_src.add_argument("--task-file", help="Local JSON task-spec (dev path)") run_src.add_argument( "--from-issue-core", action="store_true", help="Poll issue-core for one open harness-labeled task, claim, run, close", ) 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") triage = sub.add_parser( "mail-triage", help="Triage newest mail CSV via llm-connect (JSON + deterministic apply; no Claude)", ) triage.add_argument("--target-repo", required=True) triage.add_argument("--reports-dir", default="mailmeta/reports") triage.add_argument("--mail-log", default="mailmeta/mail-log.md") triage.add_argument( "--max-rows", type=int, default=None, help="Max CSV rows to send to the model (default env MAIL_TRIAGE_MAX_ROWS or 40)", ) triage.add_argument("--no-hub", action="store_true", help="Skip hub reporting") triage.add_argument( "--no-commit", action="store_true", help="Apply log updates but do not git commit", ) brief = sub.add_parser( "brief-daily", help="Write daily brief via llm-connect (JSON + markdown apply; no Claude)", ) brief.add_argument("--target-repo", required=True) brief.add_argument( "--date", default=None, help="Brief date YYYY-MM-DD (default: today Europe/Berlin)", ) brief.add_argument( "--force", action="store_true", help="Overwrite if today's brief already exists", ) brief.add_argument("--no-hub", action="store_true", help="Skip hub reporting") brief.add_argument( "--no-commit", action="store_true", help="Write brief file but do not git commit", ) 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") smoke = sub.add_parser( "smoke", help="Deterministic Railiance smoke: commit SMOKE.md, optional push, hub event", ) smoke.add_argument( "--target-repo", help="Existing git checkout (default: clone executor-sandbox under --work-dir)", ) smoke.add_argument( "--work-dir", default="~/work/executor-sandbox", help="Clone destination when --target-repo is omitted", ) smoke.add_argument( "--remote", default="ssh://git@forgejo-agent-harness/coulomb/executor-sandbox.git", help="Git remote for sandbox clone", ) smoke.add_argument("--no-hub", action="store_true", help="Skip hub reporting") smoke.add_argument("--no-push", action="store_true", help="Skip git push after commit") poll = sub.add_parser( "poll", help="Peek/claim next open issue-core task labeled for the harness (no execute)", ) poll.add_argument( "--no-claim", action="store_true", help="List/map only; do not set in_progress", ) args = parser.parse_args(argv) if args.command == "validate": return _cmd_validate(args) if args.command == "profiles": return _cmd_profiles(args) if args.command == "poll": return _cmd_poll(args) if args.command == "run": return _cmd_run(args) if args.command == "smoke": from agent_harness.smoke import ensure_sandbox_clone, run_smoke if args.target_repo: repo = Path(args.target_repo).expanduser().resolve() else: try: repo = ensure_sandbox_clone( Path(args.work_dir).expanduser(), remote=args.remote, ) except Exception as exc: print(f"smoke clone failed: {exc}", file=sys.stderr) return 2 try: smoke_result = run_smoke( repo, report_to_hub=not args.no_hub, push=not args.no_push, ) except Exception as exc: print(f"smoke failed: {exc}", file=sys.stderr) return 1 print( json.dumps( { "ok": smoke_result.run.ok and ( smoke_result.pushed or args.no_push ), "committed": smoke_result.run.committed, "pushed": smoke_result.pushed, "head_after": smoke_result.run.head_after, "tool_profile": smoke_result.run.tool_profile, "reason": smoke_result.run.reason, "push_reason": smoke_result.push_reason, "target_repo": str(repo), }, indent=2, ) ) ok = smoke_result.run.ok and (smoke_result.pushed or args.no_push) return 0 if ok else 1 if args.command == "mail-triage": from agent_harness.mail_triage import run_mail_triage triage_result = run_mail_triage( target_repo=Path(args.target_repo).expanduser(), reports_dir=args.reports_dir, mail_log_rel=args.mail_log, max_rows=args.max_rows, report_to_hub=not args.no_hub, commit=not args.no_commit, ) print( json.dumps( { "ok": triage_result.ok, "report": triage_result.report, "entries_applied": triage_result.entries_applied, "committed": triage_result.committed, "head_after": triage_result.head_after, "reason": triage_result.reason, "model_meta": triage_result.model_meta, }, indent=2, ) ) return 0 if triage_result.ok else 1 if args.command == "brief-daily": from datetime import date as date_cls from agent_harness.brief_daily import run_brief_daily day = None if args.date: day = date_cls.fromisoformat(args.date) brief_result = run_brief_daily( target_repo=Path(args.target_repo).expanduser(), day=day, force=bool(args.force), report_to_hub=not args.no_hub, commit=not args.no_commit, ) print( json.dumps( { "ok": brief_result.ok, "date": brief_result.date, "path": brief_result.path, "wrote": brief_result.wrote, "committed": brief_result.committed, "skipped_existing": brief_result.skipped_existing, "head_after": brief_result.head_after, "reason": brief_result.reason, "model_meta": brief_result.model_meta, }, indent=2, ) ) return 0 if brief_result.ok else 1 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 print(f"unknown command: {args.command}", file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())