diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e5dff..7e91ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- Launch any installed coding tool from `dyro start` / `open`, not only a + Codex Profile adapter; `agent add --preset` now covers the catalogued + launchable tools. +- Treat a healthy workspace as ready with no mutation: `next --format json` + no longer hands Agents a `start --agent codex` command. +- Install the control-plane Skill avatar for Grok when `~/.grok` is present. +- Teach the packaged Skill to read `objective explain --format json`. + ## 0.6.5 - 2026-08-13 - Expand the packaged `dyro-control-plane` Skill into a host-neutral, diff --git a/src/dyro/cli.py b/src/dyro/cli.py index 8b9240d..f3a6b1d 100644 --- a/src/dyro/cli.py +++ b/src/dyro/cli.py @@ -8,7 +8,6 @@ import os from pathlib import Path import shlex -import shutil import subprocess import sys import time @@ -92,14 +91,19 @@ from .errors import DyroError, ValidationError from .home import ( HomeTool, + _record_for_root, + existing_line_workspace, home_tools, + launch_start_tool, launcher_tools, open_line, open_task, print_agent_discovery, print_all_status, print_status, + ready_home_tools, resolve_home_config, + resolve_start_tool, run_home, sort_home_tools, ) @@ -142,6 +146,8 @@ append_adapter, command_adapter, config_value, + installed_launchable_presets, + launchable_preset_ids, preset_adapter, set_config_value, test_adapter, @@ -914,38 +920,34 @@ def _apply_setup_personal_preferences(preferences: SetupPersonalPreferences) -> return "success" -def _setup_provider_preset() -> str | None: - """Offer only a Provider that Core can configure and execute safely today.""" - - discovered = [ - command - for command in ( - "agy", - "codex", - "claude", - "cursor-agent", - "grok", - "opencode", - "hermes", - "kimi", - "qodercli", - ) - if shutil.which(command) - ] +def _normalize_provider_presets(value: object) -> tuple[str, ...]: + if not value: + return () + if isinstance(value, str): + return (value,) + return tuple(value) + + +def _setup_provider_preset() -> tuple[str, ...]: + """Register every installed launchable Agent the user confirms.""" + + discovered = installed_launchable_presets() if not discovered: - print("未发现本机 Agent;可稍后运行 dyro agent add --command '…'。") - return None - if "codex" in discovered: - if _ask_yes_no("检测到 Codex。将它加入 Dyro Profile 吗", default=True): - return "codex" - unsupported = [command for command in discovered if command != "codex"] - if unsupported: print( - "已发现 " - + "、".join(unsupported) - + ";它们尚无 Core 的受审计适配器,因此不会写入配置。" + "未发现本机 Agent;可稍后运行 dyro agent add --command '…'," + "或直接 dyro start 使用已安装工具。" ) - return None + return () + labels = "、".join(discovered) + print("检测到本机编码工具:" + labels) + question = ( + "将它加入 Dyro Profile 吗" + if len(discovered) == 1 + else "将它们加入 Dyro Profile 吗" + ) + if _ask_yes_no(question, default=True): + return discovered + return () def _setup_registration_plan( @@ -1078,7 +1080,9 @@ def _apply_setup_plan( if config_file.exists(): raise DyroError(f"配置已存在:{config_file}") plan.root.mkdir(parents=True, exist_ok=True) - adapter_presets = (plan.provider_preset,) if plan.provider_preset else () + adapter_presets = plan.provider_presets or ( + (plan.provider_preset,) if plan.provider_preset else () + ) atomic_write_text( config_file, render_config( @@ -1212,7 +1216,7 @@ def _interactive_setup(args: argparse.Namespace) -> None: line_id = _ask_value("首条开发线 ID(留空则仅创建 Profile)", default="dev") if line_id: validate_id(line_id, "开发线 ID") - provider = _setup_provider_preset() + presets = _normalize_provider_presets(_setup_provider_preset()) plan = SetupPlan( root=root, name=name, @@ -1220,7 +1224,8 @@ def _interactive_setup(args: argparse.Namespace) -> None: default_base=base, line_id=line_id or None, branch=f"feat/{line_id}" if line_id else None, - provider_preset=provider, + provider_preset=presets[0] if presets else None, + provider_presets=presets, ) registration = _setup_registration_plan(plan.root, plan.name, args) preferences = _setup_personal_preferences( @@ -2053,20 +2058,29 @@ def cmd_start(args: argparse.Namespace) -> None: raise DyroError( "工作区尚未就绪;先修复 doctor 失败项,或运行 dyro bootstrap --yes" ) - if not config.adapters: - raise DyroError("尚未配置可启动的 Agent;先运行 dyro next 查看安全的下一步") line_id = args.line or _choose("开发线", [line.id for line in list_lines(config)]) - line = get_line(config, line_id, args.kind) - agent = args.agent or _choose("Agent", sorted(config.adapters)) - open_args = argparse.Namespace( - root=str(config.root), + line, workspace = existing_line_workspace(config, line_id, args.kind) + record = _record_for_root(config.root) + last_tool = record.last_agent if record else "" + requested = args.agent or None + tool = resolve_start_tool( + config, + requested=requested, + workspace=workspace, + last_tool=last_tool, + ) + if tool is None: + ready = ready_home_tools(config, workspace=workspace) + chosen = _choose("Agent", [item.id for item in ready]) + tool = next(item for item in ready if item.id == chosen) + launch_start_tool( + config, + workspace=workspace, + tool=tool, line=line.id, - kind=line.kind, - agent=agent, prompt=args.prompt or "", dry_run=args.dry_run, ) - cmd_open(open_args) def _bootstrap_destination_safe(config: Config, relative: str) -> bool: @@ -2182,62 +2196,21 @@ def cmd_next(args: argparse.Namespace) -> None: return print(f"Profile 已就绪,但还没有开发线。下一步:{command}") return - if not config.adapters: - if shutil.which("codex"): - command = _scoped_command( - args, config, "agent", "add", "codex", "--preset", "codex" - ) - if args.format == "json": - _print_control_plane_json( - "next_step", - state="needs_agent", - summary="工作区已就绪,检测到 Codex 尚未加入 Profile。", - commands=[command], - mutation_available=True, - ) - return - print( - f"工作区已就绪,检测到 Codex 尚未加入 Profile。下一步:{command}" - ) - else: - command = _scoped_command( - args, config, "agent", "add", "", "--command", "…" - ) - if args.format == "json": - _print_control_plane_json( - "next_step", - state="needs_agent", - summary="工作区已就绪,但尚未配置可启动的 Agent。", - commands=[], - mutation_available=False, - required_inputs=["agent_id", "agent_command"], - ) - return - print( - f"工作区已就绪,但尚未配置可启动的 Agent。下一步:{command}" - ) - return - if len(lines) == 1 and len(config.adapters) == 1: - command = _scoped_command( - args, - config, - "start", - "--line", - lines[0].id, - "--agent", - next(iter(config.adapters)), - ) + if not config.adapters and not installed_launchable_presets(): if args.format == "json": _print_control_plane_json( "next_step", - state="ready", - summary="工作区已就绪。", - commands=[command], - mutation_available=True, + state="needs_agent", + summary="工作区已就绪,但尚未发现可启动的编码工具。", + commands=[], + mutation_available=False, + required_inputs=["agent_id", "agent_command"], ) return print( - f"工作区已就绪。下一步:{command}" + "工作区已就绪,但尚未发现可启动的编码工具。" + "安装本机 Agent 后运行 dyro start,或 " + + _scoped_command(args, config, "agent", "add", "", "--command", "…") ) return if args.format == "json": @@ -2245,11 +2218,11 @@ def cmd_next(args: argparse.Namespace) -> None: "next_step", state="ready", summary="工作区已就绪。", - commands=[_scoped_command(args, config, "start")], - mutation_available=True, + commands=[], + mutation_available=False, ) return - print(f"工作区已就绪。下一步:{_scoped_command(args, config, 'start')}") + print("工作区已就绪。可用 dyro start 打开本机已安装的编码工具。") def cmd_line_list(args: argparse.Namespace) -> None: @@ -3717,7 +3690,7 @@ def build_parser() -> argparse.ArgumentParser: ) agent_add.add_argument("id") agent_source = agent_add.add_mutually_exclusive_group(required=True) - agent_source.add_argument("--preset", choices=("codex", "noop")) + agent_source.add_argument("--preset", choices=launchable_preset_ids()) agent_source.add_argument( "--command", help="作为 launch/read/write 的 argv 命令行;不会经 shell 执行" ) @@ -3957,7 +3930,7 @@ def build_parser() -> argparse.ArgumentParser: open_cmd = sub.add_parser("open", help="在指定开发线启动 Agent") open_cmd.add_argument("line") open_cmd.add_argument("--kind", choices=("line", "hotfix")) - open_cmd.add_argument("--agent", default="codex") + open_cmd.add_argument("--agent") open_cmd.add_argument("--prompt", default="") open_cmd.set_defaults(func=cmd_open) start = sub.add_parser("start", help="新人入口:检查工作区、选择开发线和 Agent") @@ -4237,7 +4210,7 @@ def build_parser() -> argparse.ArgumentParser: "open", help="进入已存在的任务工作树,不改变任务状态" ) task_open.add_argument("id") - task_open.add_argument("--agent", default="codex") + task_open.add_argument("--agent") task_open.add_argument("--prompt", default="") task_open.set_defaults(func=cmd_task_open) task_claim = task_sub.add_parser("claim", help="由隔离执行器一次性领取任务") diff --git a/src/dyro/home.py b/src/dyro/home.py index 96606dd..8acaba7 100644 --- a/src/dyro/home.py +++ b/src/dyro/home.py @@ -51,7 +51,7 @@ DISCOVERABLE_AGENTS = tuple( - (definition.command, definition.id == "codex") + (definition.command, bool(definition.launch)) for definition in TOOL_DEFINITIONS if definition.interface != "desktop" ) @@ -491,8 +491,9 @@ def state_cell(state: str, *, installed: bool, configured: bool) -> str: state = f"已配置但不可用:{configured_as}" note = "命令不可用;请检查安装或 adapter 配置" elif installed and integrated: + preset = definition.id if definition is not None else command state = "尚未配置" - note = f"运行 dyro agent add {command} --preset {command}" + note = f"运行 dyro agent add {preset} --preset {preset}" elif installed: state = "尚未集成" note = "首页可仅打开工作区;不获得执行、门禁或复核权限" @@ -545,6 +546,106 @@ def state_cell(state: str, *, installed: bool, configured: bool) -> str: ) +def ready_home_tools(config: Config, *, workspace: Path) -> list[HomeTool]: + return [ + tool + for tool in home_tools(config, workspace=workspace) + if tool.available and tool.id != "shell" + ] + + +def resolve_start_tool( + config: Config, + *, + requested: str | None, + workspace: Path, + last_tool: str = "", +) -> HomeTool | None: + """Pick a ready adapter or installed launcher. + + Returns None when the user must choose among multiple tools. + """ + + ready = ready_home_tools(config, workspace=workspace) + if not ready: + raise DyroError( + "未发现可启动的编码工具;安装本机 Agent 后运行 dyro agent discover" + ) + + def match(value: str) -> HomeTool | None: + for tool in ready: + if tool.id == value: + return tool + definition = tool_definition(value) or tool_definition_for_command(value) + if definition is None: + return None + for tool in ready: + if tool.id == definition.id or tool.id == definition.command: + return tool + return None + + if requested: + tool = match(requested) + if tool is None: + available = "、".join(tool.id for tool in ready) + raise DyroError( + f"未找到可启动的 Agent:{requested}。" + f"本机可用:{available}。运行 dyro agent discover 查看详情" + ) + return tool + + preferences = load_tool_preferences() + for candidate in (preferences.default_tool, last_tool): + if candidate: + tool = match(candidate) + if tool is not None: + return tool + adapters = [tool for tool in ready if tool.kind == "adapter"] + if len(adapters) == 1: + return adapters[0] + if len(ready) == 1: + return ready[0] + return None + + +def launch_start_tool( + config: Config, + *, + workspace: Path, + tool: HomeTool, + line: str = "", + task: str = "", + prompt: str = "", + dry_run: bool, +) -> None: + if tool.kind == "adapter": + launch_adapter( + config, + workspace=workspace, + adapter_id=tool.id, + line=line, + task=task, + prompt=prompt, + dry_run=dry_run, + ) + return + if not tool.argv: + rebuilt = next( + ( + item + for item in home_tools(config, workspace=workspace) + if item.id == tool.id and item.argv + ), + None, + ) + if rebuilt is None: + raise DyroError( + f"{tool.label} 没有可执行的启动命令;运行 dyro agent discover 查看详情" + ) + tool = rebuilt + launch_home_tool(workspace=workspace, tool=tool, dry_run=dry_run) + + def launch_adapter( config: Config, *, @@ -636,15 +737,29 @@ def open_line( line_id: str, *, kind: str | None, - agent: str, + agent: str | None, prompt: str, dry_run: bool, + last_tool: str = "", ) -> None: line, workspace = existing_line_workspace(config, line_id, kind) - launch_adapter( + tool = resolve_start_tool( + config, + requested=agent or None, + workspace=workspace, + last_tool=last_tool, + ) + if tool is None: + ready = "、".join( + item.id for item in ready_home_tools(config, workspace=workspace) + ) + raise DyroError( + f"有多个可启动的编码工具;请用 --agent 指定。本机可用:{ready}" + ) + launch_start_tool( config, workspace=workspace, - adapter_id=agent, + tool=tool, line=line.id, prompt=prompt, dry_run=dry_run, @@ -652,13 +767,33 @@ def open_line( def open_task( - config: Config, task_id: str, *, agent: str, prompt: str, dry_run: bool + config: Config, + task_id: str, + *, + agent: str | None, + prompt: str, + dry_run: bool, + last_tool: str = "", ) -> None: task = load_task(config, task_id) - launch_adapter( + workspace = existing_task_workspace(config, task) + tool = resolve_start_tool( + config, + requested=agent or None, + workspace=workspace, + last_tool=last_tool, + ) + if tool is None: + ready = "、".join( + item.id for item in ready_home_tools(config, workspace=workspace) + ) + raise DyroError( + f"有多个可启动的编码工具;请用 --agent 指定。本机可用:{ready}" + ) + launch_start_tool( config, - workspace=existing_task_workspace(config, task), - adapter_id=agent, + workspace=workspace, + tool=tool, line=task.line, task=task.id, prompt=prompt, diff --git a/src/dyro/integrations/assets/dyro-control-plane/SKILL.md b/src/dyro/integrations/assets/dyro-control-plane/SKILL.md index 8e01ae3..34f1690 100644 --- a/src/dyro/integrations/assets/dyro-control-plane/SKILL.md +++ b/src/dyro/integrations/assets/dyro-control-plane/SKILL.md @@ -18,6 +18,7 @@ When the request already supplies a workspace alias, skip global discovery and u - Change Sets: `dyro --workspace changeset list --format json` or `dyro --workspace changeset verify --format json` - Installed Skill health: `dyro integration status skill --format json` - Objective inventory or facts: `dyro --workspace objective list --format json` or `dyro --workspace objective status --format json` +- Objective explanation: `dyro --workspace objective explain --format json` - Objective blockers or human attention: `dyro --workspace objective attention --format json` - Objective dependency graph: `dyro --workspace objective graph --format json` - Objective next-wave preview: `dyro --workspace objective tick --format json` diff --git a/src/dyro/integrations/manager.py b/src/dyro/integrations/manager.py index f9fecab..9d4bee1 100644 --- a/src/dyro/integrations/manager.py +++ b/src/dyro/integrations/manager.py @@ -55,6 +55,7 @@ class HostSpec: HostSpec("claude", "CLAUDE_HOME", ".claude"), HostSpec("agents", "AGENTS_HOME", ".agents"), HostSpec("cursor", "CURSOR_HOME", ".cursor"), + HostSpec("grok", "GROK_HOME", ".grok"), ) diff --git a/src/dyro/onboarding.py b/src/dyro/onboarding.py index d37392c..d4cb7c0 100644 --- a/src/dyro/onboarding.py +++ b/src/dyro/onboarding.py @@ -12,6 +12,7 @@ from .config import CONFIG_NAME, Config, load, validate_id from .errors import DyroError, ValidationError +from .profile import preset_adapter from .process import run from .read_limits import open_safe_directory_chain from .state import atomic_write_text, exclusive_lock @@ -37,6 +38,7 @@ class SetupPlan: line_id: str | None branch: str | None provider_preset: str | None = None + provider_presets: tuple[str, ...] = () @property def needs_bootstrap(self) -> bool: @@ -285,15 +287,14 @@ def render_config( ] ) for preset in adapter_presets: - if preset != "codex": - raise ValidationError(f"首次设置不支持的 Agent preset:{preset}") + adapter = preset_adapter(preset, preset) chunks.extend( ( "", - "[adapters.codex]", - 'launch = ["codex", "-C", "{workspace}"]', - 'read = ["codex", "exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "{prompt}"]', - 'write = ["codex", "exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "{prompt}"]', + f"[adapters.{_toml_table_key(adapter.id)}]", + f"launch = {json.dumps(list(adapter.launch), ensure_ascii=False)}", + f"read = {json.dumps(list(adapter.read), ensure_ascii=False)}", + f"write = {json.dumps(list(adapter.write), ensure_ascii=False)}", ) ) for repo in repositories: @@ -320,8 +321,11 @@ def render_setup_plan(plan: SetupPlan) -> tuple[str, ...]: lines.append(f"开发线:{plan.line_id}({plan.branch})") else: lines.append("开发线:暂不创建") - if plan.provider_preset: - lines.append(f"Agent:{plan.provider_preset}") + presets = plan.provider_presets or ( + (plan.provider_preset,) if plan.provider_preset else () + ) + if presets: + lines.append("Agent:" + "、".join(presets)) else: lines.append("Agent:暂不配置") return tuple(lines) diff --git a/src/dyro/profile.py b/src/dyro/profile.py index c4c0c4a..47fb6be 100644 --- a/src/dyro/profile.py +++ b/src/dyro/profile.py @@ -4,11 +4,12 @@ from pathlib import Path import re import shutil -from typing import Iterable +from typing import Callable, Iterable from .config import Adapter, CONFIG_NAME, Config, expand_argv, load, validate_id from .errors import DyroError, ValidationError from .state import atomic_write_text, exclusive_lock +from .tooling import TOOL_DEFINITIONS, tool_definition _BARE_TOML_KEY = re.compile(r"^[A-Za-z0-9_-]+$") @@ -40,6 +41,26 @@ def _validate_argv(argv: Iterable[str], label: str) -> tuple[str, ...]: return value +def launchable_preset_ids() -> tuple[str, ...]: + """Presets that can be written into a Profile and used by `dyro start`.""" + + tool_ids = tuple( + definition.id for definition in TOOL_DEFINITIONS if definition.launch + ) + return ("noop", *tool_ids) + + +def installed_launchable_presets( + *, which: Callable[[str], str | None] | None = None +) -> tuple[str, ...]: + lookup = which or shutil.which + found: list[str] = [] + for definition in TOOL_DEFINITIONS: + if definition.launch and lookup(definition.command): + found.append(definition.id) + return tuple(found) + + def preset_adapter(adapter_id: str, preset: str) -> Adapter: validate_id(adapter_id, "adapter id") if preset == "codex": @@ -51,7 +72,11 @@ def preset_adapter(adapter_id: str, preset: str) -> Adapter: ) if preset == "noop": return Adapter(adapter_id, ("/usr/bin/true",), ("/usr/bin/true",), ("/usr/bin/true",)) - raise ValidationError(f"未知 Agent preset:{preset}") + definition = tool_definition(preset) + if definition is None or not definition.launch: + raise ValidationError(f"未知 Agent preset:{preset}") + launch = _validate_argv(definition.launch, "Agent launch") + return Adapter(adapter_id, launch, launch, launch) def command_adapter(adapter_id: str, argv: Iterable[str]) -> Adapter: diff --git a/tests/test_cli.py b/tests/test_cli.py index 8fad288..ff3e01d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -340,9 +340,7 @@ def test_setup_rejects_conflicting_interaction_modes(self) -> None: self.assertEqual(raised.exception.code, 2) self.assertIn("not allowed with argument", stderr.getvalue()) - def test_setup_reports_detected_but_unintegrated_providers_without_registering_them( - self, - ) -> None: + def test_setup_offers_every_detected_launchable_provider(self) -> None: discovered = { "agy", "claude", @@ -356,19 +354,32 @@ def test_setup_reports_detected_but_unintegrated_providers_without_registering_t output = StringIO() with ( patch( - "dyro.cli.shutil.which", + "dyro.profile.shutil.which", side_effect=lambda command: ( f"/fake/{command}" if command in discovered else None ), ), + patch("dyro.cli._ask_yes_no", return_value=True), redirect_stdout(output), ): - self.assertIsNone(_setup_provider_preset()) + presets = _setup_provider_preset() + self.assertEqual( + presets, + ( + "antigravity", + "claude", + "cursor-agent", + "grok", + "opencode", + "hermes", + "kimi", + "qoder", + ), + ) rendered = output.getvalue() - for command in discovered: - self.assertIn(command, rendered) - self.assertIn("不会写入配置", rendered) + self.assertIn("antigravity", rendered) + self.assertIn("grok", rendered) def test_interactive_setup_can_be_cancelled_without_writing(self) -> None: with tempfile.TemporaryDirectory(prefix="dyro-cli-") as tmp: @@ -810,6 +821,44 @@ def test_config_and_agent_management_do_not_require_manual_toml_edits(self) -> N self.assertIn("isolated", load(self.root).adapters) main(["--root", str(self.root), "agent", "test", "isolated"]) + main(["--root", str(self.root), "agent", "add", "grok", "--preset", "grok"]) + self.assertEqual( + load(self.root).adapters["grok"].launch, + ("grok", "--cwd", "{workspace}"), + ) + + def test_start_can_launch_an_installed_tool_without_a_profile_adapter(self) -> None: + create_line(load(self.root), line_id="alpha", branch="feat/alpha", base="main") + launched: list[object] = [] + + def fake_available(executable: str, cwd=None) -> bool: + return Path(executable).name == "grok" + + with ( + patch("dyro.home.shutil.which", side_effect=lambda name: "/fake/grok" if name == "grok" else None), + patch("dyro.home.executable_available", side_effect=fake_available), + patch( + "dyro.cli.launch_start_tool", + side_effect=lambda *args, **kwargs: launched.append( + kwargs["tool"].id + ), + ), + ): + main( + [ + "--root", + str(self.root), + "--dry-run", + "start", + "--line", + "alpha", + "--agent", + "grok", + ] + ) + + self.assertEqual(launched, ["grok"]) + class ExternalClaimCommandsTests(WorkspaceCase): def test_claim_output_preflight_preserves_task_and_existing_file( @@ -939,10 +988,8 @@ def test_control_plane_workspace_views_have_stable_json_shapes(self) -> None: next_payload = self._read_json("next") self.assertEqual(next_payload["kind"], "next_step") self.assertEqual(next_payload["state"], "ready") - self.assertEqual( - next_payload["commands"], - [f"dyro --root {self.root.resolve()} start --line alpha --agent noop"], - ) + self.assertEqual(next_payload["commands"], []) + self.assertFalse(next_payload["mutation_available"]) lines_payload = self._read_json("line", "list") self.assertEqual(lines_payload["kind"], "line_list") @@ -978,10 +1025,9 @@ def test_control_plane_next_preserves_an_explicit_workspace_selector(self) -> No ) payload = json.loads(output.getvalue()) - self.assertEqual( - payload["commands"], - ["dyro --workspace selected start --line alpha --agent noop"], - ) + self.assertEqual(payload["state"], "ready") + self.assertEqual(payload["commands"], []) + self.assertFalse(payload["mutation_available"]) def test_control_plane_json_runtime_errors_use_one_stable_envelope(self) -> None: with tempfile.TemporaryDirectory(prefix="dyro-registry-") as registry_home: diff --git a/tests/test_hub.py b/tests/test_hub.py index 4423554..774a17d 100644 --- a/tests/test_hub.py +++ b/tests/test_hub.py @@ -1340,9 +1340,11 @@ def test_agent_discovery_separates_configured_and_unintegrated_commands( self.assertIn("codex", rendered) self.assertIn("claude", rendered) self.assertIn("尚未配置", rendered) - self.assertIn("尚未集成", rendered) - self.assertIn("首页可仅打开工作区", rendered) - self.assertIn("不获得执行、门禁或复核权限", rendered) + self.assertIn("dyro agent add claude --preset claude", rendered) + self.assertIn("dyro agent add cursor-agent --preset cursor-agent", rendered) + self.assertIn("dyro agent add codex --preset codex", rendered) + self.assertIn("未安装", rendered) + self.assertNotIn("尚无 Core", rendered) def test_agent_discovery_reports_configured_missing_command(self) -> None: with self.root.joinpath("dyro.toml").open("a", encoding="utf-8") as handle: diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 0eae602..bf0bbb9 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -105,6 +105,7 @@ def test_packaged_skill_is_concise_and_has_required_metadata(self) -> None: "status --format json", "doctor --format json", "objective attention --format json", + "objective explain --format json", "objective plan --format json", ): self.assertIn(command, content) diff --git a/tests/test_onboarding.py b/tests/test_onboarding.py index 9d56c80..8df07bf 100644 --- a/tests/test_onboarding.py +++ b/tests/test_onboarding.py @@ -88,6 +88,18 @@ def test_render_config_requires_an_explicit_provider_choice(self) -> None: self.assertNotIn("[adapters.codex]", content) + def test_render_config_writes_launchable_non_codex_presets(self) -> None: + content = render_config( + "workspace", + [RepositoryInput("api", "repositories/api", "api")], + adapter_presets=("grok", "claude"), + ) + + self.assertIn("[adapters.grok]", content) + self.assertIn('launch = ["grok", "--cwd", "{workspace}"]', content) + self.assertIn("[adapters.claude]", content) + self.assertIn('launch = ["claude"]', content) + def test_remote_repository_and_setup_plan_are_safe_to_preview(self) -> None: repository = repository_from_remote("git@github.com:acme/payments.git") plan = SetupPlan(