#!/usr/bin/env python3 """Resolve an Orca project or terminal into tester's structured agent config.""" from __future__ import annotations import argparse import importlib.util import json import subprocess import sys from pathlib import Path from typing import Any try: import yaml except ImportError: # pragma: no cover print("需要 PyYAML: python3 -m pip install pyyaml", file=sys.stderr) raise SystemExit(2) SUPPORTED_CLI = {"codex", "cursor-agent", "grok", "omp", "pi"} def run_orca(args: list[str], environment: str | None = None) -> dict[str, Any]: command = ["orca", *args] if environment and environment != "local": command.extend(["--environment", environment]) command.append("--json") proc = subprocess.run(command, capture_output=True, text=True, check=False) raw = (proc.stdout or "").strip() try: data = json.loads(raw) except json.JSONDecodeError as exc: detail = (proc.stderr or raw or str(exc)).strip() raise RuntimeError(f"orca 命令没有返回 JSON: {' '.join(command)}\n{detail}") from exc if not data.get("ok"): error = data.get("error") or {} message = error.get("message") or error.get("code") or "unknown error" raise RuntimeError(f"{' '.join(command)}: {message}") result = data.get("result") return result if isinstance(result, dict) else {} def environments() -> list[str]: result = run_orca(["environment", "list"]) names = ["local"] for item in result.get("environments") or []: if isinstance(item, dict) and item.get("name"): names.append(str(item["name"])) return names def worktree_from_terminal(terminal: dict[str, Any], environment: str) -> dict[str, Any]: worktree_id = str(terminal.get("worktreeId") or "") if not worktree_id: raise RuntimeError("terminal 没有 worktreeId") result = run_orca(["worktree", "show", "--worktree", worktree_id], environment) worktree = result.get("worktree") if not isinstance(worktree, dict): raise RuntimeError("找不到 terminal 对应的 worktree") return worktree def config_from_terminal(handle: str, requested_environment: str | None) -> dict[str, Any]: candidates = [requested_environment] if requested_environment else environments() errors: list[str] = [] for environment in candidates: if not environment: continue try: result = run_orca(["terminal", "show", "--terminal", handle], environment) terminal = result.get("terminal") if not isinstance(terminal, dict): raise RuntimeError("返回缺少 terminal") worktree = worktree_from_terminal(terminal, environment) identity = str(terminal.get("agentIdentity") or "") cli = identity if identity in SUPPORTED_CLI else "codex" return { "environment": "" if environment == "local" else environment, "host": str(terminal.get("executionHostId") or worktree.get("hostId") or "local"), "project": str(worktree.get("projectId") or worktree.get("repoId") or ""), "path": str(terminal.get("worktreePath") or worktree.get("path") or ""), "role": "test", "cli": cli, "tier": "standard", "model": None, "reasoningEffort": "medium", "permissionMode": "workspace-write", "_source": f"terminal:{handle}", "_project_name": str(worktree.get("displayName") or ""), "_terminal_agent_identity": identity, } except RuntimeError as exc: errors.append(f"{environment}: {exc}") raise RuntimeError("找不到可用 terminal: " + ";".join(errors)) def worktrees(environment: str) -> list[dict[str, Any]]: result = run_orca(["worktree", "list", "--limit", "200"], environment) return [item for item in result.get("worktrees") or [] if isinstance(item, dict)] def projects(environment: str) -> list[dict[str, Any]]: result = run_orca(["project", "list"], environment) return [item for item in result.get("projects") or [] if isinstance(item, dict)] def config_from_project( selector: str, requested_environment: str | None, requested_path: str | None = None, ) -> dict[str, Any]: environments_to_check = [requested_environment] if requested_environment else environments() matches: list[tuple[str, dict[str, Any]]] = [] for environment in environments_to_check: if not environment: continue project_ids: set[str] = set() for project in projects(environment): project_values = { str(project.get("id") or ""), str(project.get("displayName") or ""), } if selector in project_values: project_ids.add(str(project.get("id") or "")) project_ids.update(str(repo_id) for repo_id in project.get("sourceRepoIds") or []) for item in worktrees(environment): values = { str(item.get("id") or ""), str(item.get("projectId") or ""), str(item.get("repoId") or ""), str(item.get("displayName") or ""), str(item.get("path") or ""), Path(str(item.get("path") or "")).name, } direct_match = selector in values project_match = bool( project_ids and project_ids.intersection( { str(item.get("projectId") or ""), str(item.get("repoId") or ""), } ) ) path_match = not requested_path or str(item.get("path") or "") == requested_path if path_match and (direct_match or project_match): matches.append((environment, item)) if not matches: raise RuntimeError(f"找不到 project id/名称: {selector}") if len(matches) > 1: lines = [ f"{env}: {item.get('projectId') or item.get('repoId')} " f"{item.get('displayName') or ''} {item.get('path') or ''}" for env, item in matches ] raise RuntimeError("project 匹配多个 worktree,请补充 environment 或 path:\n" + "\n".join(lines)) environment, item = matches[0] return { "environment": "" if environment == "local" else environment, "host": str(item.get("hostId") or "local"), "project": str(item.get("projectId") or item.get("repoId") or item.get("id") or ""), "path": str(item.get("path") or ""), "role": "test", "cli": "codex", "tier": "standard", "model": None, "reasoningEffort": "medium", "permissionMode": "workspace-write", "_source": f"project:{selector}", "_project_name": str(item.get("displayName") or ""), } def clean_config(config: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in config.items() if not key.startswith("_")} def config_from_binding(root: Path, selector: str) -> dict[str, Any]: """Materialize a common agent-runtime tester binding for legacy local.yaml.""" runtime_path = Path(__file__).resolve().parents[2] / "skills" / "agent-runtime" / "scripts" / "agent.py" if not runtime_path.is_file(): raise RuntimeError(f"找不到共享 agent-runtime: {runtime_path}") spec = importlib.util.spec_from_file_location("pouch_agent_runtime", runtime_path) if spec is None or spec.loader is None: raise RuntimeError(f"无法加载共享 agent-runtime: {runtime_path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) try: common = module.load_config(root) value = module.binding(common, selector) except (ValueError, OSError) as exc: raise RuntimeError(str(exc)) from exc if value.get("adapter") != "tester": raise RuntimeError(f"binding {selector} 的 adapter 不是 tester") location = value["location"] profile = value["profile"] return { "environment": "" if location.get("environment") == "local" else str(location.get("environment") or ""), "host": str(location.get("host") or "local"), "project": str(location.get("project") or ""), "path": str(location.get("path") or ""), "role": "test", "cli": str(profile.get("cli") or "codex"), "tier": str(profile.get("tier") or "standard"), "model": profile.get("model"), "reasoningEffort": profile.get("reasoningEffort"), "permissionMode": str(profile.get("permissionMode") or "workspace-write"), "_source": f"binding:{selector}", } def write_local(root: Path, config: dict[str, Any]) -> tuple[Path, dict[str, Any]]: path = root / ".pouch" / "tester" / "local.yaml" path.parent.mkdir(parents=True, exist_ok=True) data: dict[str, Any] = {} if path.is_file(): loaded = yaml.safe_load(path.read_text(encoding="utf-8")) or {} if not isinstance(loaded, dict): raise RuntimeError(f"{path} 必须是 mapping") data = loaded current_agent = data.get("agent") if current_agent is None: current_agent = {} if not isinstance(current_agent, dict): raise RuntimeError(f"{path} 的 agent 必须是 mapping") resolved = clean_config(config) # Target location always comes from the user's selector. Keep explicit local # choices for model/policy fields, so resolving a terminal cannot erase them. location_fields = {"environment", "host", "project", "path"} terminal_cli = bool(config.get("_terminal_agent_identity")) merged = dict(current_agent) for key, value in resolved.items(): if key in location_fields or key not in merged or (key == "cli" and terminal_cli): merged[key] = value data["agent"] = merged path.write_text(yaml.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8") return path, merged def print_confirmation(config: dict[str, Any], written: bool) -> None: model = config.get("model") or "CLI 默认模型" print("## tester Agent 配置更新结果" if written else "## tester Agent 配置解析预览") print() print(f"来源: {config.get('_source', '配置文件')}") print(f"Agent: {config.get('cli')}") print(f"角色 / 档位: {config.get('role')} / {config.get('tier')}") print(f"模型: {model}") print(f"推理强度: {config.get('reasoningEffort') or 'n/a'}") print(f"权限: {config.get('permissionMode') or 'n/a'}") print(f"Orca 环境: {config.get('environment') or 'local'}") print(f"执行节点: {config.get('host') or 'local'}") print(f"项目: {config.get('project') or 'n/a'}({config.get('_project_name') or 'n/a'})") print(f"目录: {config.get('path') or 'n/a'}") print("Terminal: 每轮新建,结束后关闭,不复用输入的 terminal") print() if written: print("以上是已写入 local.yaml.agent 的生效配置;如需调整,直接修改配置后重新运行 tester。") else: print("以上是解析结果预览;加上 --write 后会写入 local.yaml.agent。") def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="解析 Orca project/terminal 为 tester Agent 配置") parser.add_argument("--root", default=".", help="项目根") source = parser.add_mutually_exclusive_group(required=True) source.add_argument("--terminal", help="Orca terminal handle") source.add_argument("--project", help="Orca project id、名称或路径") source.add_argument("--binding", help="common agent.yaml 中的 tester. binding") parser.add_argument("--path", help="项目有多个 worktree 时按目标机器上的绝对路径消歧") parser.add_argument("--environment", help="限定 Orca environment") parser.add_argument("--write", action="store_true", help="写入 .pouch/tester/local.yaml") args = parser.parse_args(argv) try: root = Path(args.root).resolve() if args.binding: config = config_from_binding(root, args.binding) else: config = ( config_from_terminal(args.terminal, args.environment) if args.terminal else config_from_project(args.project, args.environment, args.path) ) if args.write: path, saved_agent = write_local(Path(args.root).resolve(), config) config = {**config, **saved_agent} print(f"已写入: {path}") print_confirmation(config, written=args.write) return 0 except RuntimeError as exc: print(f"environment-fail: {exc}", file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())