feat: add official Codex skill and harden task execution
Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
This commit is contained in:
1 parent
2328a4df0c
commit
e1ea099592
128 files changed
+4944
-700
No files matched your search
@@ -104,122 +104,44 @@ def _codex_toml_block(server_config: dict) -> str:
|
||||
|
||||
|
||||
def _merge_codex_toml(file_path: Path, server_config: dict) -> bool:
|
||||
"""Adds or updates Artemis in Codex config.toml while preserving unrelated settings."""
|
||||
from artemis.integrations.codex_mcp import refresh_codex
|
||||
|
||||
try:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
existing = file_path.read_text(encoding="utf-8") if file_path.exists() else ""
|
||||
managed_block = _codex_toml_block(server_config)
|
||||
begin_marker = "# BEGIN ARTEMIS MCP CONFIG"
|
||||
end_marker = "# END ARTEMIS MCP CONFIG"
|
||||
|
||||
if begin_marker in existing and end_marker in existing:
|
||||
managed_pattern = re.compile(
|
||||
rf"(?ms)^{re.escape(begin_marker)}\n.*?^{re.escape(end_marker)}\n?"
|
||||
)
|
||||
# Windows paths contain backslashes; a replacement string would
|
||||
# interpret them as regex escapes on a second installation.
|
||||
updated = managed_pattern.sub(lambda _: managed_block, existing, count=1)
|
||||
else:
|
||||
# Remove an older, unmanaged Artemis table before adding the managed block.
|
||||
kept_lines: list[str] = []
|
||||
skipping_artemis = False
|
||||
header_pattern = re.compile(r"^\s*\[{1,2}\s*(.+?)\s*\]{1,2}\s*(?:#.*)?$")
|
||||
for line in existing.splitlines(keepends=True):
|
||||
header_match = header_pattern.match(line.rstrip("\r\n"))
|
||||
if header_match:
|
||||
normalized = re.sub(r"[\s\"']", "", header_match.group(1)).lower()
|
||||
skipping_artemis = normalized == "mcp_servers.artemis" or normalized.startswith(
|
||||
"mcp_servers.artemis."
|
||||
)
|
||||
if not skipping_artemis:
|
||||
kept_lines.append(line)
|
||||
preserved = "".join(kept_lines).rstrip()
|
||||
updated = f"{preserved}\n\n{managed_block}" if preserved else managed_block
|
||||
|
||||
# Never replace a user's config with invalid TOML.
|
||||
tomllib.loads(updated)
|
||||
if updated != existing:
|
||||
file_path.write_text(updated, encoding="utf-8")
|
||||
result = refresh_codex(
|
||||
file_path,
|
||||
Path(server_config["cwd"]),
|
||||
server_config["command"],
|
||||
ARTEMIS_MCP_TOOLS,
|
||||
template=server_config,
|
||||
install_missing=True,
|
||||
)
|
||||
if result is None:
|
||||
logger.warning("保留自定义 Codex MCP 连接;请手动核对 ARTEMIS 配置。")
|
||||
return True
|
||||
except Exception as e:
|
||||
if file_path.exists():
|
||||
try:
|
||||
backup_path = file_path.with_suffix(file_path.suffix + ".bak")
|
||||
backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
logger.warning(
|
||||
f"Could not update {file_path}; backup created at {backup_path}: {e}"
|
||||
)
|
||||
except (OSError, UnicodeDecodeError) as backup_exc:
|
||||
logger.warning(
|
||||
f"Could not update {file_path} ({e}); backup also failed: {backup_exc}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Could not create Codex MCP config file {file_path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _inject_rules_block(file_path: Path, content: str) -> bool:
|
||||
"""Injects or updates a managed ARTEMIS block in a global rules file without overwriting user content."""
|
||||
try:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
begin_marker = "<!-- BEGIN ARTEMIS MOBILE TESTING RULES -->"
|
||||
end_marker = "<!-- END ARTEMIS MOBILE TESTING RULES -->"
|
||||
new_block = f"{begin_marker}\n{content.strip()}\n{end_marker}\n"
|
||||
|
||||
existing = ""
|
||||
if file_path.exists():
|
||||
try:
|
||||
existing = file_path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
existing = ""
|
||||
|
||||
if begin_marker in existing and end_marker in existing:
|
||||
prefix = existing.split(begin_marker)[0]
|
||||
suffix = existing.split(end_marker)[1]
|
||||
updated = f"{prefix.rstrip()}\n\n{new_block}\n{suffix.lstrip()}".strip() + "\n"
|
||||
else:
|
||||
updated = f"{existing.rstrip()}\n\n{new_block}".strip() + "\n"
|
||||
|
||||
if existing == updated:
|
||||
return True
|
||||
|
||||
file_path.write_text(updated, encoding="utf-8")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not inject rules block into {file_path}: {e}")
|
||||
except (OSError, ValueError, TypeError):
|
||||
logger.warning("Codex MCP 同步失败,原配置已保留;请检查 TOML 格式或文件权限。")
|
||||
return False
|
||||
|
||||
|
||||
def install_rules(client: str, project_root: str) -> list[str]:
|
||||
"""Install testing rules only into the Codex instructions file."""
|
||||
"""Compatibility entry: install the official Skill and migrate old marked rules."""
|
||||
if client != "codex":
|
||||
raise ValueError("Only Codex MCP configuration is supported")
|
||||
rules_src = Path(project_root) / "mcp_server" / "rules.md"
|
||||
if not rules_src.exists():
|
||||
rules_src = Path(__file__).resolve().parents[4] / "mcp_server" / "rules.md"
|
||||
try:
|
||||
raw_rules = rules_src.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
logger.warning(f"Could not read rules file {rules_src}")
|
||||
return []
|
||||
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
|
||||
override_file = codex_home / "AGENTS.override.md"
|
||||
agents_file = (
|
||||
override_file
|
||||
if override_file.exists() and override_file.read_text(encoding="utf-8").strip()
|
||||
else codex_home / "AGENTS.md"
|
||||
)
|
||||
return [str(agents_file)] if _inject_rules_block(agents_file, raw_rules) else []
|
||||
from artemis.integrations.codex_skill import sync_skill_safely
|
||||
|
||||
result = sync_skill_safely(Path(project_root))
|
||||
return [result["path"]] if result.get("path") else []
|
||||
|
||||
|
||||
def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[str]:
|
||||
"""Install ARTEMIS configuration and rules only for Codex."""
|
||||
"""Install Codex MCP and the official artemis-use Skill."""
|
||||
server_cfg = _get_config_snippet(client, python_exe, project_root)["mcp_servers"]["artemis"]
|
||||
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
|
||||
skill_paths = install_rules(client, project_root)
|
||||
codex_path = codex_home / "config.toml"
|
||||
if not _merge_codex_toml(codex_path, server_cfg):
|
||||
raise typer.Exit(1)
|
||||
return [str(codex_path), *install_rules(client, project_root)]
|
||||
return [str(codex_path), *skill_paths]
|
||||
|
||||
|
||||
def mcp_command(
|
||||
@@ -258,7 +180,7 @@ def mcp_command(
|
||||
typer.Option(
|
||||
"--install",
|
||||
"-i",
|
||||
help="Install ARTEMIS MCP configuration and testing rules for Codex: --install codex.",
|
||||
help="Install ARTEMIS MCP configuration and official artemis-use Skill for Codex: --install codex.",
|
||||
),
|
||||
] = None,
|
||||
generate_config: Annotated[
|
||||
@@ -283,7 +205,7 @@ def mcp_command(
|
||||
raise typer.Exit(1)
|
||||
installed_paths = install_mcp_config(client, python_exe, project_root)
|
||||
console.print(
|
||||
"[bold green]✔ Successfully installed ARTEMIS MCP server configuration & rules to:[/bold green]"
|
||||
"[bold green]✔ Successfully installed ARTEMIS MCP server configuration & artemis-use Skill to:[/bold green]"
|
||||
)
|
||||
for path in installed_paths:
|
||||
console.print(f" • [cyan]{path}[/cyan]")
|
||||
|
||||
Reference in new issue
Block a user