Migrate to Codex Autonomy Keep going until the selected migration is completely done: run the migrator, inspect the report, fix migrated Codex instructions/skills/agents/MCP config, and re-run checks without stopping to ask for confirmation of the next step. If the user has selected a target, do not ask before creating, editing, replacing, or deleting generated Codex artifacts in that target ( , , , or ). Preserve unrelated existing Codex config entries in or , such as , , , or unrelated MCP servers; do not ask about them unless they fail validation or directly conflict with the migration. Do…

)}\"\n )\n manual_notes.append(\n \"Claude `skills` preload semantics were preserved as prompt guidance. Verify this agent still discovers the intended skills at runtime.\"\n )\n\n if tools or disallowed_tools:\n tool_section_lines = [\n \"## Tools\",\n \"\",\n \"Claude tool allow/deny lists were preserved as prompt guidance, not Codex permissions.\",\n ]\n if tools:\n tool_section_lines.extend(\n [\n \"\",\n \"You're allowed to use these tools:\",\n \"\",\n format_bullets(tools),\n ]\n )\n if disallowed_tools:\n tool_section_lines.extend(\n [\n \"\",\n \"Don't use these tools:\",\n \"\",\n format_bullets(disallowed_tools),\n ]\n )\n sections.append(\"\\n\".join(tool_section_lines))\n manual_notes.append(\n \"Rebuild Claude `tools` / `disallowedTools` intent with Codex sandbox, MCP tool filters, or app tool filters if you need hard enforcement.\"\n )\n\n if unsupported_fields:\n manual_notes.append(\n \"Review unsupported Claude subagent fields manually: \"\n f\"{', '.join(f'`{field_name}`' for field_name in unsupported_fields)}.\"\n )\n\n if manual_notes:\n sections.append(format_manual_migration_block(manual_notes))\n\n if not sections:\n return body\n\n joined_sections = \"\\n\\n\".join(sections)\n return f\"{body.rstrip()}\\n\\n{joined_sections}\\n\"\n\n\ndef agent_report_detail(\n *,\n permission_mode: str | None,\n skills: tuple[str, ...],\n tools: tuple[str, ...],\n disallowed_tools: tuple[str, ...],\n unsupported_fields: tuple[str, ...],\n **_: object,\n) -> str:\n caveats: list[str] = []\n if skills:\n caveats.append(\"skills\")\n if tools:\n caveats.append(\"tools\")\n if disallowed_tools:\n caveats.append(\"disallowedTools\")\n if permission_mode and not map_permission_mode(permission_mode):\n caveats.append(\"permissionMode\")\n caveats.extend(unsupported_fields)\n if not caveats:\n return \"Converted Claude subagent.\"\n return (\n \"Manual review required for Claude subagent fields: \"\n + \", \".join(f\"`{field_name}`\" for field_name in caveats)\n + \".\"\n )\n\n\ndef agent_report_item(\n source_file: Path,\n *,\n permission_mode: str | None,\n skills: tuple[str, ...],\n tools: tuple[str, ...],\n disallowed_tools: tuple[str, ...],\n unsupported_fields: tuple[str, ...],\n **metadata: object,\n) -> MigrationReportItem:\n report_items: list[MigrationReportItem] = []\n detail = agent_report_detail(\n permission_mode=permission_mode,\n skills=skills,\n tools=tools,\n disallowed_tools=disallowed_tools,\n unsupported_fields=unsupported_fields,\n **metadata,\n )\n append_report_item(\n report_items,\n skills\n or tools\n or disallowed_tools\n or (permission_mode and not map_permission_mode(permission_mode))\n or unsupported_fields,\n CODEX_AGENTS_ROOT / f\"{source_file.stem}.toml\",\n detail,\n detail,\n )\n return report_items[0]\n\n\ndef convert_agents(source_root: Path) -> ConversionResult:\n return convert_agent_files(source_root / \".claude\" / \"agents\")\n\n\ndef convert_agent_files(source_root: Path) -> ConversionResult:\n result = ConversionResult()\n for source_file in iter_agent_files(source_root):\n artifact, report_item = convert_agent_file(source_file)\n result.artifacts.append(artifact)\n result.summary.subagents += 1\n result.report_items.append(report_item)\n return result\n\n\ndef validate_agent_files(target_root: Path) -> list[MigrationReportItem]:\n agents_root = target_root / CODEX_AGENTS_ROOT\n if not agents_root.exists():\n return []\n\n report_items: list[MigrationReportItem] = []\n for agent_file in sorted(agents_root.glob(\"*.toml\")):\n relative_path = agent_file.relative_to(target_root)\n try:\n parsed = tomllib.loads(agent_file.read_text())\n except tomllib.TOMLDecodeError as exc:\n report_items.append(\n MigrationReportItem(\"error\", relative_path, f\"invalid TOML: {exc}.\")\n )\n continue\n\n missing = [\n key\n for key in (\"name\", \"description\", \"developer_instructions\")\n if not parsed.get(key)\n ]\n if missing:\n report_items.append(\n MigrationReportItem(\n \"error\",\n relative_path,\n \"agent TOML missing \" + \", \".join(missing) + \".\",\n )\n )\n continue\n report_items.append(\n MigrationReportItem(\"ok\", relative_path, \"agent TOML has required fields.\")\n )\n return report_items\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9991,"content_sha256":"7859242aaaa9efcaf05ed03564610a96d371e085d90f77babac93d437a645bd5"},{"filename":"scripts/migrate/codex_config.py","content":"\"\"\"Render Codex config from Claude Code settings and MCP inputs.\n\nThis module owns `.codex/config.toml` generation. It reads Claude Code\nsettings for model/sandbox equivalents, asks `mcps.py` for MCP server tables,\nand adds Codex-native defaults that are not MCP-specific, such as the friendly\npersonality used for Claude Code migrations.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport tomllib\nfrom pathlib import Path\n\nfrom migrate.common import (\n CODEX_CONFIG_PATH,\n ConversionResult,\n GeneratedText,\n MigrationReportItem,\n MigrationSummary,\n PlannedArtifact,\n ScopePaths,\n json_string,\n json_string_tuple,\n load_scope_settings,\n map_model_name,\n map_permission_mode,\n)\nfrom migrate.hooks import has_convertible_hooks\nfrom migrate.mcps import (\n mcp_report_items,\n mcp_server_toml_table,\n read_claude_mcp_servers,\n validate_mcp_commands,\n)\nfrom utils.util import TomlValue, render_toml_document\n\n\nDEFAULT_CODEX_PERSONALITY = \"friendly\"\n\n\ndef convert_settings(scope: ScopePaths) -> ConversionResult:\n settings = load_scope_settings(scope.source)\n mcp_servers = read_claude_mcp_servers(scope.source)\n if not settings and not mcp_servers:\n return ConversionResult()\n\n enabled_mcp_servers = json_string_tuple(settings.get(\"enabledMcpjsonServers\"))\n disabled_mcp_servers = frozenset(\n json_string_tuple(settings.get(\"disabledMcpjsonServers\"))\n )\n config_toml = render_codex_config(\n model=json_string(settings.get(\"model\")),\n permission_mode=json_string(settings.get(\"permissionMode\")),\n enabled_mcp_servers=enabled_mcp_servers,\n disabled_mcp_servers=disabled_mcp_servers,\n mcp_servers=mcp_servers,\n codex_hooks_enabled=has_convertible_hooks(scope.source),\n )\n if not config_toml.strip():\n return ConversionResult()\n return ConversionResult(\n summary=MigrationSummary(mcp_servers=len(mcp_servers)),\n artifacts=[\n PlannedArtifact(\n relative_path=CODEX_CONFIG_PATH,\n payload=GeneratedText(config_toml),\n )\n ],\n report_items=mcp_report_items(mcp_servers),\n )\n\n\ndef render_codex_config(\n model: str | None,\n permission_mode: str | None,\n enabled_mcp_servers: tuple[str, ...],\n disabled_mcp_servers: frozenset[str],\n mcp_servers: tuple[tuple[str, dict[str, object]], ...],\n codex_hooks_enabled: bool,\n) -> str:\n document: dict[str, TomlValue] = {}\n if model:\n document[\"model\"] = map_model_name(model)\n sandbox_mode = map_permission_mode(permission_mode)\n if sandbox_mode:\n document[\"sandbox_mode\"] = sandbox_mode\n\n if mcp_servers:\n document[\"mcp_servers\"] = {\n server_name: mcp_server_toml_table(\n server_name,\n server_config,\n enabled_mcp_servers,\n disabled_mcp_servers,\n )\n for server_name, server_config in mcp_servers\n }\n\n if codex_hooks_enabled:\n document[\"features\"] = {\"codex_hooks\": True}\n\n if document:\n document = {\"personality\": DEFAULT_CODEX_PERSONALITY, **document}\n\n return render_toml_document(document)\n\n\ndef validate_config_toml(target_root: Path) -> list[MigrationReportItem]:\n config_path = target_root / CODEX_CONFIG_PATH\n if not config_path.exists():\n return [\n MigrationReportItem(\n \"warning\",\n CODEX_CONFIG_PATH,\n \"not present; no Codex config to validate.\",\n )\n ]\n\n try:\n parsed = tomllib.loads(config_path.read_text())\n except tomllib.TOMLDecodeError as exc:\n return [\n MigrationReportItem(\n \"error\",\n CODEX_CONFIG_PATH,\n f\"invalid TOML: {exc}.\",\n )\n ]\n\n report_items = [\n MigrationReportItem(\"ok\", CODEX_CONFIG_PATH, \"valid TOML.\"),\n ]\n report_items.extend(validate_mcp_commands(parsed))\n return report_items\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4031,"content_sha256":"5b092657150e24a938a55f2f17602aa8a6f4dfb6548f77613f8c7f9ed142c25d"},{"filename":"scripts/migrate/common.py","content":"\"\"\"Shared data models, frontmatter rendering, reporting, and path helpers.\n\nDefines the artifact/report model used by every migration section, the\nYAML-frontmatter adapter used for skills/agents/commands, Claude-model and\npermission-mode partial mappings, and generic filesystem/report helpers. This\nmodule should not know about one migration surface's control flow.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom collections.abc import Mapping, Sequence\nfrom dataclasses import dataclass, field, fields as dataclass_fields\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import TypeAlias\n\nfrom migrate.settings import CLAUDE_SETTINGS_JSON_RELATIVE\nfrom utils.util import (\n format_yaml_mapping,\n parse_yaml_mapping,\n read_json_mapping_file,\n)\n\n\nFRONTMATTER_RE = re.compile(r\"\\A---\\n(.*?)\\n---\\n?(.*)\\Z\", re.S)\nCODEX_CONFIG_PATH = Path(\".codex\") / \"config.toml\"\nCODEX_AGENTS_ROOT = Path(\".codex\") / \"agents\"\nCODEX_SKILLS_ROOT = Path(\".agents\") / \"skills\"\nSUMMARY_LABELS = {\n \"mcp_servers\": \"mcp servers\",\n}\nPERMISSION_MODE_MAPPINGS = {\n \"acceptEdits\": \"workspace-write\",\n \"readOnly\": \"read-only\",\n}\nYamlScalar: TypeAlias = str | bool | int | float | None\nYamlValue: TypeAlias = YamlScalar | Sequence[YamlScalar]\n\n\n@dataclass(frozen=True)\nclass ScopePaths:\n source: Path\n is_global: bool\n\n\n@dataclass(frozen=True)\nclass ModelMapping:\n source_prefix: str\n target_model: str\n effort_mapping: tuple[tuple[str, str], ...]\n\n def map_effort(self, effort: str) -> str:\n for source_effort, target_effort in self.effort_mapping:\n if effort == source_effort:\n return target_effort\n return effort\n\n\nMODEL_PREFIX_MAPPINGS = (\n ModelMapping(\n \"claude-opus\",\n \"gpt-5.4\",\n ((\"low\", \"low\"), (\"medium\", \"medium\"), (\"high\", \"high\"), (\"max\", \"xhigh\")),\n ),\n ModelMapping(\n \"claude-sonnet\",\n \"gpt-5.4-mini\",\n ((\"low\", \"medium\"), (\"medium\", \"high\"), (\"high\", \"xhigh\"), (\"max\", \"xhigh\")),\n ),\n ModelMapping(\n \"claude-haiku\",\n \"gpt-5.4-mini\",\n ((\"low\", \"low\"), (\"medium\", \"medium\"), (\"high\", \"high\"), (\"max\", \"xhigh\")),\n ),\n)\n\n\nclass ArtifactKind(Enum):\n FILE = \"file\"\n SKILL = \"skill\"\n AGENT = \"agent\"\n\n\n@dataclass(frozen=True)\nclass GeneratedText:\n content: str\n\n\n@dataclass(frozen=True)\nclass SourceCopy:\n source_path: Path\n\n\n@dataclass(frozen=True)\nclass SourceSymlink:\n source_path: Path\n\n\nArtifactPayload: TypeAlias = GeneratedText | SourceCopy | SourceSymlink\n\n\n@dataclass(frozen=True)\nclass MigrationReportItem:\n status: str\n path: Path\n detail: str\n\n\n@dataclass(frozen=True)\nclass SimpleYamlFrontmatter:\n values: dict[str, YamlValue]\n\n def required_string(self, key: str) -> str:\n return str(self.values[key])\n\n def optional_string(self, key: str) -> str | None:\n value = self.values.get(key)\n if value is None:\n return None\n return str(value)\n\n def string_tuple(self, key: str) -> tuple[str, ...]:\n value = self.values.get(key)\n if value is None:\n return ()\n\n if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):\n return tuple(str(item).strip() for item in value if str(item).strip())\n\n return tuple(\n split_item\n for split_item in (part.strip() for part in str(value).split(\",\"))\n if split_item\n )\n\n def to_dict(self) -> dict[str, YamlValue]:\n return self.values\n\n\n@dataclass(frozen=True)\nclass ParsedDocument:\n frontmatter: SimpleYamlFrontmatter\n body: str\n path: Path | None = None\n\n @classmethod\n def from_file(cls, source_file: Path) -> ParsedDocument:\n return parse_frontmatter(source_file.read_text(), source_file)\n\n\n@dataclass(frozen=True)\nclass PlannedArtifact:\n relative_path: Path\n payload: ArtifactPayload\n kind: ArtifactKind = ArtifactKind.FILE\n\n @classmethod\n def for_skill(cls, source_file: Path, content: str) -> PlannedArtifact:\n return cls(\n relative_path=CODEX_SKILLS_ROOT / source_file.parent.name / \"SKILL.md\",\n payload=GeneratedText(content),\n kind=ArtifactKind.SKILL,\n )\n\n @classmethod\n def for_agent(cls, source_file: Path, content: str) -> PlannedArtifact:\n return cls(\n relative_path=CODEX_AGENTS_ROOT / f\"{source_file.stem}.toml\",\n payload=GeneratedText(content),\n kind=ArtifactKind.AGENT,\n )\n\n @classmethod\n def from_source_file(\n cls, source_file: Path, relative_path: Path\n ) -> PlannedArtifact:\n return cls(\n relative_path=relative_path,\n payload=SourceCopy(source_file),\n )\n\n def prefixed(self, prefix: Path) -> PlannedArtifact:\n return PlannedArtifact(\n relative_path=prefix / self.relative_path,\n payload=self.payload,\n kind=self.kind,\n )\n\n def without_prefix(self) -> PlannedArtifact:\n return PlannedArtifact(\n relative_path=Path(*self.relative_path.parts[1:]),\n payload=self.payload,\n kind=self.kind,\n )\n\n@dataclass\nclass MigrationSummary:\n instructions: int = 0\n skills: int = 0\n subagents: int = 0\n mcp_servers: int = 0\n orphaned_skills: int = 0\n orphaned_subagents: int = 0\n\n def add(self, other: MigrationSummary) -> None:\n for summary_field in dataclass_fields(self):\n field_name = summary_field.name\n setattr(\n self,\n field_name,\n getattr(self, field_name) + getattr(other, field_name),\n )\n\n def render(self, deploy_mode: object, dry_run: bool) -> str:\n suffix = \" (dry-run)\" if dry_run else \"\"\n deploy_mode_value = getattr(deploy_mode, \"value\", str(deploy_mode))\n lines = [\n f\"Migration summary{suffix}:\",\n f\" deploy mode: {deploy_mode_value}\",\n ]\n for summary_field in dataclass_fields(self):\n field_name = summary_field.name\n value = getattr(self, field_name)\n label = SUMMARY_LABELS.get(field_name, field_name.replace(\"_\", \" \"))\n lines.append(f\" {label}: {value}\")\n return \"\\n\".join(lines)\n\n\n@dataclass\nclass ConversionResult:\n summary: MigrationSummary = field(default_factory=MigrationSummary)\n artifacts: list[PlannedArtifact] = field(default_factory=list)\n report_items: list[MigrationReportItem] = field(default_factory=list)\n\n def add(self, other: ConversionResult) -> None:\n self.summary.add(other.summary)\n self.artifacts.extend(other.artifacts)\n self.report_items.extend(other.report_items)\n\n def prefixed(self, prefix: Path) -> ConversionResult:\n return ConversionResult(\n summary=self.summary,\n artifacts=[artifact.prefixed(prefix) for artifact in self.artifacts],\n report_items=[\n MigrationReportItem(\n item.status,\n prefix / item.path,\n item.detail,\n )\n for item in self.report_items\n ],\n )\n\n\ndef json_object(value: object) -> Mapping[str, object]:\n if isinstance(value, Mapping):\n return value\n return {}\n\n\ndef json_string(value: object) -> str | None:\n if value is None:\n return None\n return str(value)\n\n\ndef json_string_tuple(value: object) -> tuple[str, ...]:\n if value is None:\n return ()\n if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):\n return tuple(str(item) for item in value)\n return (str(value),)\n\n\ndef load_scope_settings(scope_root: Path) -> Mapping[str, object]:\n settings: dict[str, object] = {}\n for rel in CLAUDE_SETTINGS_JSON_RELATIVE:\n outcome = read_json_mapping_file(scope_root / rel)\n if outcome.exists and outcome.ok:\n settings.update(json_object(outcome.data))\n return settings\n\n\ndef format_bullets(values: Sequence[str], prefix: str = \"\") -> str:\n return \"\\n\".join(f\"- {prefix}{value}\" for value in values)\n\n\ndef format_manual_migration_block(notes: Sequence[str]) -> str:\n return \"## MANUAL MIGRATION REQUIRED\\n\\n\" + \"\\n\\n\".join(\n note.rstrip() for note in notes if note.strip()\n )\n\n\ndef unsupported_frontmatter_fields(\n frontmatter_values: Mapping[str, YamlValue],\n supported_fields: Sequence[str],\n) -> tuple[str, ...]:\n supported = frozenset(supported_fields)\n return tuple(\n sorted(\n field_name\n for field_name in frontmatter_values\n if field_name not in supported\n )\n )\n\n\ndef append_report_item(\n report_items: list[MigrationReportItem],\n requires_manual_fix: object,\n path: Path,\n manual_detail: str,\n rewritten_detail: str,\n) -> None:\n if requires_manual_fix:\n report_items.append(manual_report_item(path, manual_detail))\n return\n report_items.append(MigrationReportItem(\"rewritten\", path, rewritten_detail))\n\n\ndef manual_report_item(path: Path, detail: str) -> MigrationReportItem:\n return MigrationReportItem(\"manual_fix_required\", path, detail)\n\n\ndef report_manual_paths(\n scope: ScopePaths,\n path_labels: Sequence[tuple[Path, str]],\n) -> ConversionResult:\n result = ConversionResult()\n\n for relative_path, label in path_labels:\n if path_exists_with_exact_case(scope.source / relative_path):\n result.report_items.append(\n manual_report_item(\n relative_path,\n f\"Manual review required for {label}; not converted by this tool.\",\n )\n )\n\n return result\n\n\ndef path_exists_with_exact_case(path: Path) -> bool:\n if not path.exists():\n return False\n try:\n return path.name in {child.name for child in path.parent.iterdir()}\n except FileNotFoundError:\n return False\n\n\ndef is_path_within_root(path: Path, root: Path) -> bool:\n try:\n path.resolve().relative_to(root.resolve())\n except ValueError:\n return False\n return True\n\n\ndef parse_frontmatter(content: str, path: Path | None = None) -> ParsedDocument:\n match = FRONTMATTER_RE.match(content)\n if not match:\n return ParsedDocument(SimpleYamlFrontmatter({}), content, path)\n\n raw_frontmatter, body = match.groups()\n return ParsedDocument(parse_yaml_frontmatter(raw_frontmatter, path), body, path)\n\n\ndef parse_yaml_frontmatter(\n content: str,\n path: Path | None = None,\n) -> SimpleYamlFrontmatter:\n return SimpleYamlFrontmatter(parse_yaml_mapping(content))\n\n\ndef format_frontmatter(frontmatter: SimpleYamlFrontmatter, body: str) -> str:\n rendered = format_yaml_mapping(frontmatter.to_dict())\n return f\"---\\n{rendered}\\n---\\n\\n{body.lstrip()}\"\n\n\ndef map_model_name(model: str) -> str:\n for mapping in MODEL_PREFIX_MAPPINGS:\n if model.startswith(mapping.source_prefix):\n return mapping.target_model\n return model\n\n\ndef map_model_effort(model: str | None, effort: str) -> str:\n if not model:\n return effort\n for mapping in MODEL_PREFIX_MAPPINGS:\n if model.startswith(mapping.source_prefix):\n return mapping.map_effort(effort)\n return effort\n\n\ndef map_permission_mode(permission_mode: str | None) -> str | None:\n if not permission_mode:\n return None\n return PERMISSION_MODE_MAPPINGS.get(permission_mode)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11492,"content_sha256":"c12107cca109dcc171b3dd48e93e5ee56d7e805e7a86c3341d0cc72c1a33845c"},{"filename":"scripts/migrate/hooks.py","content":"\"\"\"Convert supported Claude Code hooks into Codex hook config.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom collections.abc import Mapping, Sequence\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom migrate.common import (\n ConversionResult,\n GeneratedText,\n MigrationReportItem,\n PlannedArtifact,\n ScopePaths,\n json_object,\n json_string,\n path_exists_with_exact_case,\n read_json_mapping_file,\n)\nfrom migrate.settings import CLAUDE_SETTINGS_JSON_RELATIVE\n\n\nCODEX_HOOKS_PATH = Path(\".codex\") / \"hooks.json\"\nCODEX_HOOK_EVENTS = (\n \"PreToolUse\",\n \"PostToolUse\",\n \"SessionStart\",\n \"UserPromptSubmit\",\n \"Stop\",\n)\nCODEX_HOOK_MATCHER_EVENTS = frozenset((\"PreToolUse\", \"PostToolUse\", \"SessionStart\"))\n\n\n@dataclass(frozen=True)\nclass ClaudeHookCommand:\n command: str\n timeout_sec: int | None = None\n status_message: str | None = None\n\n @classmethod\n def from_mapping(cls, hook_config: Mapping[str, object]) -> ClaudeHookCommand | None:\n command = json_string(hook_config.get(\"command\"))\n if command is None or not command.strip():\n return None\n\n timeout_value = hook_config.get(\"timeout\")\n if timeout_value is None:\n timeout_value = hook_config.get(\"timeoutSec\")\n\n return cls(\n command=command,\n timeout_sec=json_int(timeout_value),\n status_message=json_string(hook_config.get(\"statusMessage\")),\n )\n\n def to_mapping(self) -> dict[str, object]:\n result: dict[str, object] = {\n \"type\": \"command\",\n \"command\": self.command,\n }\n if self.timeout_sec is not None:\n result[\"timeout\"] = self.timeout_sec\n if self.status_message is not None:\n result[\"statusMessage\"] = self.status_message\n return result\n\n\n@dataclass(frozen=True)\nclass ClaudeHookMatcherGroup:\n event_name: str\n matcher: str | None\n hooks: tuple[ClaudeHookCommand, ...]\n\n def to_mapping(self) -> dict[str, object]:\n result: dict[str, object] = {\n \"hooks\": [hook.to_mapping() for hook in self.hooks],\n }\n if self.matcher is not None:\n result[\"matcher\"] = self.matcher\n return result\n\n\n@dataclass(frozen=True)\nclass ClaudeHooks:\n matcher_groups: tuple[ClaudeHookMatcherGroup, ...] = ()\n source_paths: tuple[Path, ...] = ()\n unsupported_fields: tuple[str, ...] = ()\n\n @classmethod\n def from_scope(cls, scope_root: Path) -> ClaudeHooks:\n hook_sets = [\n cls.from_settings_mapping(relative_path, outcome.data)\n for relative_path in CLAUDE_SETTINGS_JSON_RELATIVE\n if path_exists_with_exact_case(scope_root / relative_path)\n for outcome in (read_json_mapping_file(scope_root / relative_path),)\n if outcome.exists and outcome.ok\n ]\n return cls(\n matcher_groups=tuple(\n matcher_group\n for hook_set in hook_sets\n for matcher_group in hook_set.matcher_groups\n ),\n source_paths=tuple(\n source_path\n for hook_set in hook_sets\n for source_path in hook_set.source_paths\n ),\n unsupported_fields=tuple(\n unsupported_field\n for hook_set in hook_sets\n for unsupported_field in hook_set.unsupported_fields\n ),\n )\n\n @classmethod\n def from_settings_mapping(\n cls,\n relative_path: Path,\n settings: Mapping[str, object],\n ) -> ClaudeHooks:\n hooks_config = json_object(settings.get(\"hooks\"))\n if not hooks_config:\n return cls()\n\n matcher_groups: list[ClaudeHookMatcherGroup] = []\n unsupported_fields: list[str] = []\n for event_name, groups_value in hooks_config.items():\n if event_name not in CODEX_HOOK_EVENTS:\n unsupported_fields.append(f\"hooks.{event_name}\")\n continue\n\n for group_config in json_object_tuple(groups_value):\n matcher = json_string(group_config.get(\"matcher\"))\n if matcher is not None and event_name not in CODEX_HOOK_MATCHER_EVENTS:\n unsupported_fields.append(f\"hooks.{event_name}.matcher\")\n matcher = None\n if \"if\" in group_config:\n unsupported_fields.append(f\"hooks.{event_name}.if\")\n\n hook_commands: list[ClaudeHookCommand] = []\n for hook_config in json_object_tuple(group_config.get(\"hooks\")):\n hook_type = json_string(hook_config.get(\"type\")) or \"command\"\n if hook_type != \"command\":\n unsupported_fields.append(\n f\"hooks.{event_name}.hooks[].type:{hook_type}\"\n )\n continue\n if bool(hook_config.get(\"async\")):\n unsupported_fields.append(f\"hooks.{event_name}.hooks[].async\")\n continue\n\n hook_command = ClaudeHookCommand.from_mapping(hook_config)\n if hook_command is None:\n unsupported_fields.append(f\"hooks.{event_name}.hooks[].command\")\n continue\n hook_commands.append(hook_command)\n\n if hook_commands:\n matcher_groups.append(\n ClaudeHookMatcherGroup(\n event_name=event_name,\n matcher=matcher,\n hooks=tuple(hook_commands),\n )\n )\n\n return cls(\n matcher_groups=tuple(matcher_groups),\n source_paths=(relative_path,),\n unsupported_fields=tuple(sorted(set(unsupported_fields))),\n )\n\n def render_codex_file(self) -> str:\n hooks_payload: dict[str, list[dict[str, object]]] = {}\n for matcher_group in self.matcher_groups:\n hooks_payload.setdefault(matcher_group.event_name, []).append(\n matcher_group.to_mapping()\n )\n return json.dumps({\"hooks\": hooks_payload}, indent=2) + \"\\n\"\n\n def report_detail(self) -> str:\n runtime_caveats = (\n \"Rewritten for Codex hooks; review behavior before relying on it. \"\n \"Codex hooks require `[features].codex_hooks = true`, only execute \"\n \"`command` handlers, skip `async` / `prompt` / `agent` handlers, ignore \"\n \"`matcher` for `UserPromptSubmit` and `Stop`, and `PreToolUse` / \"\n \"`PostToolUse` currently run for shell commands only.\"\n )\n if not self.unsupported_fields:\n return runtime_caveats\n return (\n \"Unsupported Claude hook fields need review: \"\n + \", \".join(f\"`{field_name}`\" for field_name in self.unsupported_fields)\n + f\". {runtime_caveats}\"\n )\n\n\ndef has_convertible_hooks(scope_root: Path) -> bool:\n return bool(ClaudeHooks.from_scope(scope_root).matcher_groups)\n\n\ndef report_hooks(scope: ScopePaths) -> ConversionResult:\n claude_hooks = ClaudeHooks.from_scope(scope.source)\n if not claude_hooks.matcher_groups:\n return ConversionResult()\n\n return ConversionResult(\n artifacts=[\n PlannedArtifact(\n relative_path=CODEX_HOOKS_PATH,\n payload=GeneratedText(claude_hooks.render_codex_file()),\n )\n ],\n report_items=[\n MigrationReportItem(\n \"rewritten\",\n CODEX_HOOKS_PATH,\n claude_hooks.report_detail(),\n )\n ],\n )\n\n\ndef json_int(value: object) -> int | None:\n if value is None or isinstance(value, bool):\n return None\n try:\n return int(str(value))\n except ValueError:\n return None\n\n\ndef json_object_tuple(value: object) -> tuple[Mapping[str, object], ...]:\n if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):\n return tuple(json_object(item) for item in value)\n return ()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8194,"content_sha256":"4465c0537a3750a6e7095db0f2a41f087eb3e63053482dc33f876967e87d8616"},{"filename":"scripts/migrate/instructions.py","content":"\"\"\"Discover and classify source instruction files for AGENTS.md migration.\n\nChooses the first supported instruction file for a project/global scope. Neutral\ninstruction files are safe for `AGENTS.md` symlinks; content with obvious\nClaude-only lifecycle, hook, subagent, or permission assumptions is treated as\nrequiring a generated Codex-specific copy and manual rewrite.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nfrom pathlib import Path\n\nfrom migrate.common import MigrationReportItem\n\n\nINSTRUCTION_SOURCE_CANDIDATES = (\n Path(\".claude\") / \"CLAUDE.md\",\n Path(\"CLAUDE.md\"),\n Path(\"claude.md\"),\n Path(\"AGENTS.md\"),\n)\n\nCLAUDE_ONLY_INSTRUCTION_MARKERS = (\n \"/hooks\",\n \".claude/agents/\",\n \".claude/settings\",\n \"Subagent\",\n \"subagent\",\n \"permissionMode\",\n \"ExitPlanMode\",\n)\nMAX_AGENTS_MD_BYTES = 32 * 1024\n\n\ndef instruction_source_file(\n source_root: Path,\n is_global: bool,\n path_exists_with_exact_case: Callable[[Path], bool],\n) -> Path | None:\n candidates = INSTRUCTION_SOURCE_CANDIDATES\n if not is_global:\n candidates = tuple(\n candidate\n for candidate in candidates\n if candidate != Path(\".claude\") / \"CLAUDE.md\"\n )\n\n for candidate in candidates:\n source_file = source_root / candidate\n if path_exists_with_exact_case(source_file):\n return source_file\n return None\n\n\ndef should_symlink_instructions(content: str) -> bool:\n return not any(marker in content for marker in CLAUDE_ONLY_INSTRUCTION_MARKERS)\n\n\ndef validate_agents_md_files(target_root: Path) -> list[MigrationReportItem]:\n report_items: list[MigrationReportItem] = []\n for agents_file in sorted(target_root.rglob(\"AGENTS.md\")):\n relative_path = agents_file.relative_to(target_root)\n size_bytes = agents_file.stat().st_size\n if size_bytes > MAX_AGENTS_MD_BYTES:\n report_items.append(\n MigrationReportItem(\n \"warning\",\n relative_path,\n f\"{size_bytes / 1024:.1f}KB exceeds the 32KB review threshold.\",\n )\n )\n continue\n report_items.append(\n MigrationReportItem(\n \"ok\",\n relative_path,\n f\"{size_bytes / 1024:.1f}KB is under the 32KB review threshold.\",\n )\n )\n return report_items\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2434,"content_sha256":"321284ce5b0b17b5691b7a4f4020d813e28e336578e8c6764f34626f2b449ea1"},{"filename":"scripts/migrate/mcps.py","content":"\"\"\"Convert Claude Code MCP/settings JSON into Codex config TOML.\n\nReads Claude settings plus `.mcp.json` / `.claude.json`, maps model and sandbox\nsettings when there is a known Codex equivalent, and renders MCP server entries\nfor `.codex/config.toml`. Header/env forms are partially normalized to Codex\n`bearer_token_env_var`, `env_http_headers`, `http_headers`, `env_vars`, and\nliteral `env` tables.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport shutil\nfrom collections.abc import Mapping\nfrom pathlib import Path\n\nfrom migrate.common import (\n CODEX_CONFIG_PATH,\n MigrationReportItem,\n json_object,\n json_string,\n json_string_tuple,\n path_exists_with_exact_case,\n)\nfrom migrate.settings import CLAUDE_MCP_JSON_RELATIVE\nfrom utils.util import TomlValue\n\n\nENV_VAR_RE = re.compile(r\"\\A\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\\}\\Z\")\nBEARER_ENV_VAR_RE = re.compile(\n r\"\\ABearer\\s+\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\\}\\Z\"\n)\n\n\ndef mcp_server_toml_table(\n server_name: str,\n server_config: Mapping[str, object],\n enabled_servers: tuple[str, ...],\n disabled_servers: frozenset[str],\n) -> dict[str, TomlValue]:\n table: dict[str, TomlValue] = {}\n enabled = mcp_enabled_state(server_config)\n if enabled is False:\n table[\"enabled\"] = False\n elif enabled_servers and server_name not in enabled_servers:\n table[\"enabled\"] = False\n elif server_name in disabled_servers:\n table[\"enabled\"] = False\n if url := json_string(server_config.get(\"url\")):\n table[\"url\"] = url\n if command := json_string(server_config.get(\"command\")):\n table[\"command\"] = command\n if args := json_string_tuple(server_config.get(\"args\")):\n table[\"args\"] = list(args)\n if \"headers\" in server_config:\n append_header_config(table, json_object(server_config[\"headers\"]))\n if \"env\" in server_config:\n append_env_config(table, json_object(server_config[\"env\"]))\n return table\n\n\ndef mcp_report_items(\n mcp_servers: tuple[tuple[str, dict[str, object]], ...],\n) -> list[MigrationReportItem]:\n report_items = [\n MigrationReportItem(\n \"rewritten\",\n CODEX_CONFIG_PATH,\n f\"Converted {len(mcp_servers)} MCP server entries.\",\n )\n ]\n for server_name, server_config in mcp_servers:\n notes = mcp_manual_notes(server_name, server_config)\n if notes:\n report_items.append(\n MigrationReportItem(\n \"manual_fix_required\",\n CODEX_CONFIG_PATH,\n f\"MCP server `{server_name}` needs review: {' '.join(notes)}\",\n )\n )\n return report_items\n\n\ndef append_header_config(\n table: dict[str, TomlValue],\n headers: Mapping[str, object],\n) -> None:\n static_headers: dict[str, str] = {}\n env_headers: dict[str, str] = {}\n\n for key, value in headers.items():\n header_value = str(value)\n bearer_match = BEARER_ENV_VAR_RE.match(header_value)\n if key.lower() == \"authorization\" and bearer_match:\n table[\"bearer_token_env_var\"] = bearer_match.group(1)\n continue\n\n env_match = ENV_VAR_RE.match(header_value)\n if env_match:\n env_headers[key] = env_match.group(1)\n continue\n\n static_headers[key] = header_value\n\n if static_headers:\n table[\"http_headers\"] = static_headers\n if env_headers:\n table[\"env_http_headers\"] = env_headers\n\n\ndef append_env_config(\n table: dict[str, TomlValue],\n env: Mapping[str, object],\n) -> None:\n static_env: dict[str, str] = {}\n env_vars: list[str] = []\n\n for key, value in env.items():\n env_value = str(value)\n env_match = ENV_VAR_RE.match(env_value)\n if env_match and env_match.group(1) == key:\n env_vars.append(key)\n continue\n\n static_env[key] = env_value\n\n if env_vars:\n table[\"env_vars\"] = env_vars\n if static_env:\n table[\"env\"] = static_env\n\n\ndef mcp_manual_notes(\n server_name: str,\n server_config: Mapping[str, object],\n) -> tuple[str, ...]:\n notes: list[str] = []\n source_type = json_string(server_config.get(\"type\"))\n if source_type and source_type not in {\"http\", \"stdio\"}:\n notes.append(\n f\"Claude MCP `type: {source_type}` was not written to Codex config; verify that the generated `url` or `command` is a supported Codex transport.\"\n )\n unsupported_fields = unsupported_mcp_server_fields(server_config)\n if unsupported_fields:\n notes.append(\n \"Review unsupported Claude MCP fields manually: \"\n + \", \".join(f\"`{field_name}`\" for field_name in unsupported_fields)\n + \".\"\n )\n return tuple(notes)\n\n\ndef mcp_enabled_state(server_config: Mapping[str, object]) -> bool | None:\n if server_config.get(\"enabled\") is False:\n return False\n if server_config.get(\"disabled\") is True:\n return False\n return None\n\n\ndef unsupported_mcp_server_fields(\n server_config: Mapping[str, object],\n) -> tuple[str, ...]:\n supported = {\n \"args\",\n \"command\",\n \"disabled\",\n \"enabled\",\n \"env\",\n \"headers\",\n \"name\",\n \"scope\",\n \"type\",\n \"url\",\n }\n return tuple(sorted(key for key in server_config if key not in supported))\n\n\ndef read_claude_mcp_servers(source_root: Path) -> tuple[tuple[str, dict[str, object]], ...]:\n servers: list[tuple[str, dict[str, object]]] = []\n for relative_path in CLAUDE_MCP_JSON_RELATIVE:\n source_file = source_root / relative_path\n if not path_exists_with_exact_case(source_file):\n continue\n mcp_config = json_object(json.loads(source_file.read_text()))\n for server_name, server_config in json_object(mcp_config.get(\"mcpServers\")).items():\n servers.append((server_name, json_object(server_config)))\n return tuple(servers)\n\n\ndef validate_mcp_commands(config: dict[str, object]) -> list[MigrationReportItem]:\n mcp_servers = config.get(\"mcp_servers\")\n if not isinstance(mcp_servers, dict):\n return []\n\n report_items: list[MigrationReportItem] = []\n for server_name, server_config in sorted(mcp_servers.items()):\n if not isinstance(server_config, dict):\n continue\n command = server_config.get(\"command\")\n if not command:\n continue\n command_text = str(command)\n if shutil.which(command_text):\n report_items.append(\n MigrationReportItem(\n \"ok\",\n CODEX_CONFIG_PATH,\n f\"MCP server `{server_name}` command `{command_text}` is on PATH.\",\n )\n )\n else:\n report_items.append(\n MigrationReportItem(\n \"warning\",\n CODEX_CONFIG_PATH,\n f\"MCP server `{server_name}` command `{command_text}` was not found on PATH.\",\n )\n )\n return report_items\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7078,"content_sha256":"3e5903a3d0a7e09d1a6d8f6d702107b81e661486eef7cdc10012014dc734e4e5"},{"filename":"scripts/migrate/plugins.py","content":"\"\"\"Report Claude Code plugin surfaces that need manual Codex migration.\n\nClaude Code plugins and plugin marketplaces can bundle commands, agents, MCP\nservers, skills, and hooks with provider-specific metadata. The migrator reports\ntheir presence as manual follow-up; it does not install Codex plugins, copy\nplugin trees, or read marketplace `source` entries.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom migrate.common import ConversionResult, ScopePaths, report_manual_paths\nfrom migrate.settings import CLAUDE_PLUGIN_MANUAL_PATHS\n\n\ndef report_plugins(scope: ScopePaths) -> ConversionResult:\n return report_manual_paths(scope, CLAUDE_PLUGIN_MANUAL_PATHS)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":660,"content_sha256":"fec8ca75b4e9467aadc5dc822aa24c8c7f44415debde77ce8761d1d8014eaf26"},{"filename":"scripts/migrate/settings.py","content":"\"\"\"Shared source path constants for migration discovery/reporting.\n\nConstants here describe where Claude Code commonly stores instructions,\ncommands, skills, agents, MCP config, plugin references, and hooks. Paths are\nrelative to `ScopePaths.source`, the directory containing `.claude`, `.mcp.json`,\nand similar source roots.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nCLAUDE_SETTINGS_JSON_RELATIVE = (\n Path(\".claude\") / \"settings.json\",\n Path(\".claude\") / \"settings.local.json\",\n)\n\nCLAUDE_MCP_JSON_RELATIVE = (\n Path(\".mcp.json\"),\n Path(\".claude.json\"),\n)\n\nCLAUDE_PLUGIN_MANUAL_PATHS = (\n (Path(\".claude\") / \"plugins\", \"Claude Code plugins\"),\n (\n Path(\".claude\") / \"plugin-marketplaces.json\",\n \"Claude Code plugin marketplace registry\",\n ),\n (\n Path(\".claude-plugin\") / \"marketplace.json\",\n \"Claude Code plugin marketplace\",\n ),\n)\n\nSOURCE_SCAN_ROOTS = (\n (Path(\".claude\"), \"primary source\"),\n)\n\nSOURCE_SCOPE_MARKERS = (\n Path(\".claude\"),\n)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1027,"content_sha256":"13a659e2dbba2895b6d4289b1eb62c33924f95bf45d5e4f2fbdd07fe4f35639a"},{"filename":"scripts/migrate/skills.py","content":"\"\"\"Convert Claude Code skills and commands into Codex skills.\n\nReads `.claude/skills/\u003cname>/SKILL.md` and `.claude/skills/\u003cname>.md`, then emits\n`.agents/skills/\u003cname>/SKILL.md` plus supported helper directories for directory\nskills. Also wraps `.claude/commands/*.md` as\none-file Codex skills. Runtime placeholders, file expansion, shell\ninterpolation, and unsupported metadata are preserved with manual-review\ncaveats.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom collections.abc import Sequence\nfrom pathlib import Path\n\nfrom migrate.common import (\n CODEX_SKILLS_ROOT,\n ArtifactKind,\n ConversionResult,\n GeneratedText,\n MigrationReportItem,\n ParsedDocument,\n PlannedArtifact,\n SimpleYamlFrontmatter,\n append_report_item,\n format_bullets,\n format_frontmatter,\n format_manual_migration_block,\n is_path_within_root,\n manual_report_item,\n parse_frontmatter,\n unsupported_frontmatter_fields,\n)\nfrom utils.util import slugify_name\n\n\nCOMMAND_FILE_SOURCES = (\n (Path(\".claude\") / \"commands\", \"source-command\", \"source command\"),\n)\n\nSKILL_SOURCE_ROOTS = (\n Path(\".claude\") / \"skills\",\n)\nSKILL_SUPPORT_DIRS = (\"scripts\", \"references\", \"assets\")\n\n\ndef iter_skill_files(source_root: Path) -> tuple[Path, ...]:\n if not source_root.exists():\n return ()\n single_file_skills = tuple(\n source_file\n for source_file in sorted(source_root.glob(\"*.md\"))\n if source_file.stem != \"README\"\n )\n directory_skills = tuple(sorted(source_root.glob(\"*/SKILL.md\")))\n return single_file_skills + directory_skills\n\n\ndef skill_target_name(source_file: Path) -> str:\n if source_file.name == \"SKILL.md\":\n return source_file.parent.name\n return source_file.stem\n\n\ndef command_caveats(\n template: str,\n unsupported_fields: Sequence[str],\n) -> tuple[str, ...]:\n caveats: list[str] = []\n if re.search(r\"\\$(ARGUMENTS|\\d+)\\b\", template):\n caveats.append(\n \"Provider argument placeholders like `$ARGUMENTS` or `$1` were preserved as text; rewrite them into natural-language instructions for Codex.\"\n )\n if \"{{\" in template and \"}}\" in template:\n caveats.append(\n \"Provider template variables like `{{name}}` were preserved as text; rewrite them into natural-language instructions for Codex.\"\n )\n if re.search(r\"!\\s*`\", template):\n caveats.append(\n \"Provider shell-output interpolation like ``!`command` `` was preserved as text; replace it with explicit Codex instructions to run the command when needed.\"\n )\n if re.search(r\"(^|\\s)@[\\w./~:-]+\", template):\n caveats.append(\n \"Provider automatic file-reference expansion was preserved as text; verify Codex should read those files explicitly.\"\n )\n if unsupported_fields:\n caveats.append(\n \"Review unsupported command metadata manually: \"\n + \", \".join(f\"`{field_name}`\" for field_name in unsupported_fields)\n + \".\"\n )\n return tuple(caveats)\n\n\ndef convert_skills(source_root: Path) -> ConversionResult:\n result = convert_skill_files(source_root / \".claude\" / \"skills\")\n result.add(convert_command_skills(source_root))\n return result\n\n\ndef convert_skill_files(source_root: Path) -> ConversionResult:\n result = ConversionResult()\n for source_file in iter_skill_files(source_root):\n artifacts, report_item = convert_skill_file(source_file)\n result.artifacts.extend(artifacts)\n result.summary.skills += 1\n result.report_items.append(report_item)\n return result\n\n\ndef convert_command_skills(source_root: Path) -> ConversionResult:\n result = ConversionResult()\n for command_source_root, name_prefix, provider in COMMAND_FILE_SOURCES:\n result.add(\n convert_markdown_command_files(\n source_root / command_source_root,\n name_prefix,\n provider,\n )\n )\n return result\n\n\ndef codex_skill_frontmatter(name: str, description: str) -> SimpleYamlFrontmatter:\n return SimpleYamlFrontmatter(\n {\n \"name\": name,\n \"description\": description,\n }\n )\n\n\ndef convert_skill_file(source_file: Path) -> tuple[list[PlannedArtifact], MigrationReportItem]:\n document = ParsedDocument.from_file(source_file)\n name = document.frontmatter.required_string(\"name\")\n description = document.frontmatter.required_string(\"description\")\n allowed_tools = document.frontmatter.string_tuple(\"allowed-tools\")\n unsupported_fields = unsupported_frontmatter_fields(\n document.frontmatter.to_dict(),\n (\"name\", \"description\", \"allowed-tools\"),\n )\n artifacts = [\n PlannedArtifact(\n relative_path=CODEX_SKILLS_ROOT / skill_target_name(source_file) / \"SKILL.md\",\n payload=GeneratedText(\n render_skill(\n document.body,\n name=name,\n description=description,\n allowed_tools=allowed_tools,\n unsupported_fields=unsupported_fields,\n )\n ),\n kind=ArtifactKind.SKILL,\n )\n ]\n artifacts.extend(skill_support_artifacts(source_file))\n return artifacts, skill_report_item(source_file, allowed_tools, unsupported_fields)\n\n\ndef skill_support_artifacts(source_file: Path) -> list[PlannedArtifact]:\n if source_file.name != \"SKILL.md\":\n return []\n\n artifacts: list[PlannedArtifact] = []\n skill_root = source_file.parent\n target_root = CODEX_SKILLS_ROOT / skill_root.name\n source_files: list[Path] = []\n for dirname in SKILL_SUPPORT_DIRS:\n source_dir = skill_root / dirname\n if not source_dir.exists():\n continue\n source_files.extend(\n source_file\n for source_file in source_dir.rglob(\"*\")\n if source_file.is_file() and is_path_within_root(source_file, skill_root)\n )\n for support_file in sorted(\n source_files,\n key=lambda path: path.relative_to(skill_root).as_posix(),\n ):\n artifacts.append(\n PlannedArtifact.from_source_file(\n support_file,\n target_root / support_file.relative_to(skill_root),\n )\n )\n return artifacts\n\n\ndef render_skill(\n body: str,\n *,\n name: str,\n description: str,\n allowed_tools: tuple[str, ...],\n unsupported_fields: tuple[str, ...],\n) -> str:\n return format_frontmatter(\n codex_skill_frontmatter(name, description),\n render_skill_body(body, allowed_tools, unsupported_fields),\n )\n\n\ndef render_skill_body(\n body: str,\n allowed_tools: tuple[str, ...],\n unsupported_fields: tuple[str, ...],\n) -> str:\n manual_notes: list[str] = []\n if allowed_tools:\n manual_notes.append(\n \"Claude `allowed-tools` was preserved as prompt guidance, not a Codex permission boundary.\\n\\n\"\n \"You're allowed to use these tools:\\n\\n\"\n f\"{format_bullets(allowed_tools)}\"\n )\n if unsupported_fields:\n manual_notes.append(\n \"Review unsupported Claude skill fields manually: \"\n f\"{', '.join(f'`{field_name}`' for field_name in unsupported_fields)}.\"\n )\n\n if not manual_notes:\n return body\n\n return f\"{body.rstrip()}\\n\\n{format_manual_migration_block(manual_notes)}\\n\"\n\n\ndef skill_report_detail(\n allowed_tools: tuple[str, ...],\n unsupported_fields: tuple[str, ...],\n) -> str:\n caveats: list[str] = []\n if allowed_tools:\n caveats.append(\"allowed-tools\")\n caveats.extend(unsupported_fields)\n if not caveats:\n return \"Converted Claude skill.\"\n return (\n \"Manual review required for Claude skill fields: \"\n + \", \".join(f\"`{field_name}`\" for field_name in caveats)\n + \".\"\n )\n\n\ndef skill_report_item(\n source_file: Path,\n allowed_tools: tuple[str, ...],\n unsupported_fields: tuple[str, ...],\n) -> MigrationReportItem:\n report_items: list[MigrationReportItem] = []\n detail = skill_report_detail(allowed_tools, unsupported_fields)\n append_report_item(\n report_items,\n allowed_tools or unsupported_fields,\n CODEX_SKILLS_ROOT / skill_target_name(source_file) / \"SKILL.md\",\n detail,\n detail,\n )\n return report_items[0]\n\n\ndef convert_markdown_command_files(\n source_root: Path,\n name_prefix: str,\n provider: str,\n) -> ConversionResult:\n result = ConversionResult()\n if not source_root.exists():\n return result\n for source_file in sorted(source_root.rglob(\"*.md\")):\n artifact, report_item = convert_command_file(\n source_root,\n source_file,\n name_prefix,\n provider,\n )\n result.artifacts.append(artifact)\n result.summary.skills += 1\n result.report_items.append(report_item)\n return result\n\n\ndef convert_command_file(\n source_root: Path,\n source_file: Path,\n name_prefix: str,\n provider: str,\n) -> tuple[PlannedArtifact, MigrationReportItem]:\n document = ParsedDocument.from_file(source_file)\n source_name = \"-\".join(source_file.relative_to(source_root).with_suffix(\"\").parts)\n name = slugify_name(f\"{name_prefix}-{source_name}\")\n description = document.frontmatter.optional_string(\"description\")\n if not description:\n description = f\"Run the migrated {provider} `{source_name}`.\"\n unsupported_fields = unsupported_frontmatter_fields(\n document.frontmatter.to_dict(),\n (\"description\",),\n )\n caveats = command_caveats(document.body, unsupported_fields)\n artifact = PlannedArtifact(\n relative_path=CODEX_SKILLS_ROOT / name / \"SKILL.md\",\n payload=GeneratedText(\n render_command_skill(\n document.body,\n name=name,\n description=description,\n provider=provider,\n source_name=source_name,\n caveats=caveats,\n )\n ),\n kind=ArtifactKind.SKILL,\n )\n return artifact, command_report_item(name, provider, source_name)\n\n\ndef render_command_skill(\n body: str,\n *,\n name: str,\n description: str,\n provider: str,\n source_name: str,\n caveats: tuple[str, ...],\n) -> str:\n manual_notes = [\n f\"Migrated from {provider} `{source_name}` into a Codex skill. \"\n f\"Invoke it as `${name}` and manually rewrite any slash-command behavior that depended on provider-specific runtime expansion.\"\n ]\n manual_notes.extend(caveats)\n template_body = body.strip() or \"No command template body was found.\"\n return format_frontmatter(\n codex_skill_frontmatter(name, description),\n f\"# {name}\\n\\n\"\n \"Use this skill when the user asks to run the migrated \"\n f\"{provider} `{source_name}`.\\n\\n\"\n \"## Command Template\\n\\n\"\n f\"{template_body}\\n\\n\"\n f\"{format_manual_migration_block(manual_notes)}\\n\",\n )\n\n\ndef validate_skill_files(target_root: Path) -> list[MigrationReportItem]:\n skills_root = target_root / CODEX_SKILLS_ROOT\n if not skills_root.exists():\n return []\n\n report_items: list[MigrationReportItem] = []\n for skill_file in sorted(skills_root.glob(\"*/SKILL.md\")):\n relative_path = skill_file.relative_to(target_root)\n document = parse_frontmatter(skill_file.read_text(), skill_file)\n missing = [\n key\n for key in (\"name\", \"description\")\n if not document.frontmatter.optional_string(key)\n ]\n if missing:\n report_items.append(\n MigrationReportItem(\n \"error\",\n relative_path,\n \"skill frontmatter missing \" + \", \".join(missing) + \".\",\n )\n )\n continue\n report_items.append(\n MigrationReportItem(\n \"ok\",\n relative_path,\n \"skill frontmatter has name and description.\",\n )\n )\n return report_items\n\n\ndef command_report_detail(provider: str, source_name: str) -> str:\n return (\n f\"Converted {provider} `{source_name}` to a single-file Codex skill; \"\n \"review invocation and template placeholder semantics.\"\n )\n\n\ndef command_report_item(\n name: str,\n provider: str,\n source_name: str,\n) -> MigrationReportItem:\n return manual_report_item(\n CODEX_SKILLS_ROOT / name / \"SKILL.md\",\n command_report_detail(provider, source_name),\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12605,"content_sha256":"738f25b09345d84f79cc517e3d0862e4c154b612cf459ab6db00099b827e6eea"},{"filename":"scripts/utils/__init__.py","content":"# Migration script helper modules.\n","content_type":"text/x-python; charset=utf-8","language":"python","size":35,"content_sha256":"884b593e41718aed59d3fd46ff77ddafccf422554f0ca0e39a37722ad4b148b4"},{"filename":"scripts/utils/scan.py","content":"from __future__ import annotations\n\nfrom collections.abc import Callable, Sequence\nfrom pathlib import Path\n\n\ndef should_skip_inventory_child(child: Path) -> bool:\n return child.name in {\".DS_Store\", \"__pycache__\"}\n\n\ndef command_file_inventory(\n source_root: Path,\n command_file_sources: Sequence[tuple[Path, str, str]],\n) -> tuple[tuple[str, tuple[str, ...]], ...]:\n inventory: list[tuple[str, tuple[str, ...]]] = []\n for relative_root, _name_prefix, provider in command_file_sources:\n absolute_root = source_root / relative_root\n if not absolute_root.exists():\n continue\n command_names = tuple(\n sorted(\n source_file.relative_to(absolute_root).with_suffix(\"\").as_posix()\n for source_file in absolute_root.rglob(\"*.md\")\n )\n )\n if command_names:\n inventory.append((provider, command_names))\n return tuple(inventory)\n\n\ndef render_named_inventory(\n lines: list[str],\n label: str,\n values: Sequence[str],\n) -> None:\n if not values:\n lines.append(f\" inactive: {label} - none found\")\n return\n lines.append(f\" active: {label} - {len(values)} found\")\n for value in values:\n lines.append(f\" - {value}\")\n\n\ndef render_scope_inventory(\n source_root: Path,\n instruction_source_candidates: Sequence[Path],\n command_file_sources: Sequence[tuple[Path, str, str]],\n skill_source_roots: Sequence[Path],\n agent_source_roots: Sequence[Path],\n iter_skill_files: Callable[[Path], Sequence[Path]],\n iter_agent_files: Callable[[Path], Sequence[Path]],\n path_exists_with_exact_case: Callable[[Path], bool],\n) -> str:\n lines = [\"\", \"Migration inventory:\"]\n instruction_candidates = tuple(\n candidate.as_posix()\n for candidate in instruction_source_candidates\n if path_exists_with_exact_case(source_root / candidate)\n )\n skill_names = tuple(\n sorted(\n {\n source_file.parent.name\n for relative_root in skill_source_roots\n for source_file in iter_skill_files(source_root / relative_root)\n }\n )\n )\n agent_names = tuple(\n sorted(\n {\n source_file.stem\n for relative_root in agent_source_roots\n for source_file in iter_agent_files(source_root / relative_root)\n }\n )\n )\n\n render_named_inventory(lines, \"instruction files\", instruction_candidates)\n render_named_inventory(lines, \"skills\", skill_names)\n\n command_inventory = command_file_inventory(source_root, command_file_sources)\n if not command_inventory:\n lines.append(\" inactive: command sources - none found\")\n else:\n total_commands = sum(\n len(command_names) for _, command_names in command_inventory\n )\n lines.append(f\" active: command sources - {total_commands} found\")\n for provider, command_names in command_inventory:\n lines.append(f\" provider: {provider} ({len(command_names)})\")\n for command_name in command_names:\n lines.append(f\" - {command_name}\")\n\n render_named_inventory(lines, \"subagents\", agent_names)\n return \"\\n\".join(lines)\n\n\ndef render_source_inventory(\n source_root: Path,\n source_scan_roots: Sequence[tuple[Path, str]],\n path_exists_with_exact_case: Callable[[Path], bool],\n) -> str:\n lines = [\"\", \"Source inventory:\"]\n discovered = False\n\n for relative_root, label in source_scan_roots:\n absolute_root = source_root / relative_root\n if not path_exists_with_exact_case(absolute_root):\n continue\n discovered = True\n lines.append(f\" detected: {relative_root.as_posix()} - {label}\")\n try:\n children = sorted(\n absolute_root.iterdir(), key=lambda child: child.name.lower()\n )\n except FileNotFoundError:\n continue\n for child in children:\n if should_skip_inventory_child(child):\n continue\n child_kind = \"dir\" if child.is_dir() else \"file\"\n lines.append(f\" {child_kind}: {(relative_root / child.name).as_posix()}\")\n\n if not discovered:\n lines.append(\" inactive: No supported source directories found.\")\n\n return \"\\n\".join(lines)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4370,"content_sha256":"d7fcce39222a732cc91eff58c23e4319e3bd437e10eaf4e2084193c245a21ab0"},{"filename":"scripts/utils/util.py","content":"from __future__ import annotations\n\nimport glob\nimport json\nimport re\nfrom collections.abc import Mapping, Sequence\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import TypeAlias\n\n\nYamlScalar: TypeAlias = str | bool | int | float | None\nYamlValue: TypeAlias = YamlScalar | Sequence[YamlScalar]\nTomlScalar: TypeAlias = str | bool | int | float | None\nTomlValue: TypeAlias = object\n\n\ndef detected_json_keys(content: str, keys: Sequence[str]) -> tuple[str, ...]:\n return tuple(key for key in keys if re.search(rf'\"{re.escape(key)}\"\\s*:', content))\n\n\ndef strip_jsonc_comments(content: str) -> str:\n lines: list[str] = []\n for line in content.splitlines():\n in_string = False\n escaped = False\n result: list[str] = []\n index = 0\n while index \u003c len(line):\n char = line[index]\n if escaped:\n result.append(char)\n escaped = False\n index += 1\n continue\n if char == \"\\\\\" and in_string:\n result.append(char)\n escaped = True\n index += 1\n continue\n if char == '\"':\n in_string = not in_string\n result.append(char)\n index += 1\n continue\n if (\n not in_string\n and char == \"/\"\n and index + 1 \u003c len(line)\n and line[index + 1] == \"/\"\n ):\n break\n result.append(char)\n index += 1\n lines.append(\"\".join(result))\n return \"\\n\".join(lines)\n\n\ndef load_jsonc_object(content: str, json_object: callable) -> Mapping[str, object]:\n without_comments = strip_jsonc_comments(content)\n without_trailing_commas = re.sub(r\",\\s*([}\\]])\", r\"\\1\", without_comments)\n return json_object(json.loads(without_trailing_commas))\n\n\ndef parse_jsonc_mapping_text(text: str) -> Mapping[str, object] | None:\n \"\"\"Return the top-level JSON object, or None if the text is not a JSON object.\"\"\"\n try:\n without_comments = strip_jsonc_comments(text)\n without_trailing_commas = re.sub(r\",\\s*([}\\]])\", r\"\\1\", without_comments)\n parsed = json.loads(without_trailing_commas)\n except (json.JSONDecodeError, TypeError, ValueError):\n return None\n if isinstance(parsed, Mapping):\n return parsed\n return None\n\n\n@dataclass(frozen=True)\nclass JsonMappingFileRead:\n exists: bool\n ok: bool\n data: Mapping[str, object]\n\n\ndef read_json_mapping_file(path: Path) -> JsonMappingFileRead:\n \"\"\"Read a JSON/JSONC file. ``ok`` is False when the file exists but could not be parsed.\"\"\"\n if not path.is_file():\n return JsonMappingFileRead(exists=False, ok=True, data={})\n text = path.read_text()\n parsed = parse_jsonc_mapping_text(text)\n if parsed is None:\n return JsonMappingFileRead(exists=True, ok=False, data={})\n return JsonMappingFileRead(exists=True, ok=True, data=parsed)\n\n\n@dataclass(frozen=True)\nclass TomlMultilineString:\n value: str\n\n\ndef parse_yaml_mapping(content: str) -> dict[str, YamlValue]:\n \"\"\"Parse the small YAML-frontmatter subset used by Claude metadata.\"\"\"\n result: dict[str, YamlValue] = {}\n current_key: str | None = None\n\n for raw_line in content.splitlines():\n if not raw_line.strip():\n continue\n\n if raw_line.startswith(\" - \") and current_key:\n current_value = result.setdefault(current_key, [])\n if not isinstance(current_value, list):\n current_value = [current_value]\n result[current_key] = current_value\n current_value.append(parse_yaml_value(raw_line[4:].strip()))\n continue\n\n key, separator, value = raw_line.partition(\":\")\n if not separator:\n continue\n\n current_key = key.strip()\n value = value.strip()\n result[current_key] = parse_yaml_value(value) if value else []\n\n return result\n\n\ndef parse_yaml_value(value: str) -> YamlValue:\n if value in {\"true\", \"True\"}:\n return True\n if value in {\"false\", \"False\"}:\n return False\n if value in {\"null\", \"Null\", \"~\"}:\n return None\n if value.startswith(\"[\") and value.endswith(\"]\"):\n return tuple(\n parse_yaml_value(item)\n for item in split_delimited_values(value[1:-1])\n if item\n )\n if value.startswith('\"') and value.endswith('\"'):\n try:\n return json.loads(value)\n except json.JSONDecodeError:\n return value[1:-1]\n if value.startswith(\"'\") and value.endswith(\"'\"):\n return value[1:-1].replace(\"''\", \"'\")\n return value\n\n\ndef split_delimited_values(content: str) -> tuple[str, ...]:\n values: list[str] = []\n token: list[str] = []\n quote: str | None = None\n escaped = False\n for char in content:\n if escaped:\n token.append(char)\n escaped = False\n continue\n if char == \"\\\\\" and quote == '\"':\n token.append(char)\n escaped = True\n continue\n if quote:\n token.append(char)\n if char == quote:\n quote = None\n continue\n if char in {\"'\", '\"'}:\n token.append(char)\n quote = char\n continue\n if char == \",\":\n values.append(\"\".join(token).strip())\n token = []\n continue\n token.append(char)\n values.append(\"\".join(token).strip())\n return tuple(values)\n\n\ndef format_yaml_mapping(values: Mapping[str, YamlValue]) -> str:\n return \"\\n\".join(\n f\"{key}: {format_yaml_value(value)}\" for key, value in values.items()\n )\n\n\ndef format_yaml_value(value: YamlValue) -> str:\n if isinstance(value, bool):\n return \"true\" if value else \"false\"\n if value is None:\n return \"null\"\n if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):\n return \"[\" + \", \".join(format_yaml_value(item) for item in value) + \"]\"\n return json.dumps(str(value))\n\n\ndef render_toml_document(values: Mapping[str, TomlValue]) -> str:\n lines: list[str] = []\n append_toml_entries(lines, values)\n for key, value in values.items():\n if isinstance(value, Mapping):\n append_toml_table(lines, (key,), value)\n return \"\\n\".join(lines).rstrip() + \"\\n\"\n\n\ndef append_toml_table(\n lines: list[str],\n path: tuple[str, ...],\n values: Mapping[str, TomlValue],\n) -> None:\n append_blank_line(lines)\n lines.append(\"[\" + \".\".join(format_toml_key(path_part) for path_part in path) + \"]\")\n append_toml_entries(lines, values)\n\n for key, value in values.items():\n if isinstance(value, Mapping):\n append_toml_table(lines, (*path, key), value)\n\n\ndef append_toml_entries(lines: list[str], values: Mapping[str, TomlValue]) -> None:\n for key, value in values.items():\n if isinstance(value, Mapping):\n continue\n lines.append(f\"{format_toml_key(key)} = {format_toml_value(value)}\")\n\n\ndef append_blank_line(lines: list[str]) -> None:\n if lines and lines[-1]:\n lines.append(\"\")\n\n\ndef format_toml_key(key: str) -> str:\n if re.fullmatch(r\"[A-Za-z0-9_-]+\", key):\n return key\n return json.dumps(key)\n\n\ndef format_toml_value(value: TomlValue) -> str:\n if isinstance(value, TomlMultilineString):\n return format_toml_multiline_string(value.value)\n if isinstance(value, bool):\n return \"true\" if value else \"false\"\n if value is None:\n return '\"\"'\n if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):\n return \"[\" + \", \".join(format_toml_value(item) for item in value) + \"]\"\n return json.dumps(str(value))\n\n\ndef format_toml_multiline_string(value: str) -> str:\n escaped = value.replace(\"\\\\\", \"\\\\\\\\\").replace('\"\"\"', '\\\\\"\\\\\"\\\\\"')\n return f'\"\"\"{escaped}\"\"\"'\n\n\ndef slugify_name(value: str) -> str:\n result = re.sub(r\"[^A-Za-z0-9_-]+\", \"-\", value.strip()).strip(\"-\").lower()\n return result or \"migrated-command\"\n\n\ndef first_markdown_heading(content: str) -> str | None:\n for line in content.splitlines():\n match = re.match(r\"^#\\s+(.+?)\\s*$\", line)\n if match:\n return match.group(1).strip()\n return None\n\n\ndef format_backtick_list(values: Sequence[str]) -> str:\n if not values:\n return \"\"\n if len(values) == 1:\n return f\"`{values[0]}`\"\n return \", \".join(f\"`{value}`\" for value in values[:-1]) + f\", and `{values[-1]}`\"\n\n\ndef normalize_source_scope_root(\n path: Path, source_scope_markers: Sequence[Path]\n) -> Path:\n resolved = path\n for marker in source_scope_markers:\n if resolved.parts[-len(marker.parts) :] == marker.parts:\n return resolved.parents[len(marker.parts) - 1]\n return resolved\n\n\ndef resolve_source_root(source: str) -> Path:\n if not glob.has_magic(source):\n return Path(source)\n\n matches = [Path(match) for match in glob.glob(source, recursive=True)]\n if not matches:\n raise FileNotFoundError(f\"No matches for source pattern: {source}\")\n\n for match in matches:\n if (\n match.is_dir()\n and (match / \"global\").exists()\n and (match / \"project\").exists()\n ):\n return match\n\n static_prefix = source.split(\"*\", 1)[0].rstrip(\"/\")\n return Path(static_prefix)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9441,"content_sha256":"d8eb5a1e99d60bbef3b17b118fc6a082edd6c39edacadab5d9c89a11311a5c0c"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Migrate to Codex","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Autonomy","type":"text"}]},{"type":"paragraph","content":[{"text":"Keep going until the selected migration is completely done: run the migrator, inspect the report, fix migrated Codex instructions/skills/agents/MCP config, and re-run checks without stopping to ask for confirmation of the next step. If the user has selected a target, do not ask before creating, editing, replacing, or deleting generated Codex artifacts in that target (","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".codex/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".agents/","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"~/.codex/","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Preserve unrelated existing Codex config entries in ","type":"text"},{"text":".codex/config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"~/.codex/config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":", such as ","type":"text"},{"text":"notify","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"projects","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"marketplaces","type":"text","marks":[{"type":"code_inline"}]},{"text":", or unrelated MCP servers; do not ask about them unless they fail validation or directly conflict with the migration. Do not edit source Claude Code files (","type":"text"},{"text":".claude/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"~/.claude/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".mcp.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":".claude.json","type":"text","marks":[{"type":"code_inline"}]},{"text":"), unrelated project code, secrets, or another repository.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Migration Order","type":"text"}]},{"type":"paragraph","content":[{"text":"Run the migration in this order for each selected global or project source:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start by using Codex's built-in TODO/task list tool. Do not create ","type":"text"},{"text":"MIGRATION_TODOS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" or any TODO file unless the user explicitly asks. The TODO list input has a ","type":"text"},{"text":"plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" array whose items each have ","type":"text"},{"text":"step","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":"; use statuses ","type":"text"},{"text":"pending","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"in_progress","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"completed","type":"text","marks":[{"type":"code_inline"}]},{"text":". Make the TODOs specific to the selected artifacts. Before finishing, update the TODO list so every finished step is marked ","type":"text"},{"text":"completed","type":"text","marks":[{"type":"code_inline"}]},{"text":" and no step remains ","type":"text"},{"text":"in_progress","type":"text","marks":[{"type":"code_inline"}]},{"text":". Use literal source → Codex target labels, for example:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inspect ","type":"text"},{"text":".claude/commands","type":"text","marks":[{"type":"code_inline"}]},{"text":" → Codex skills/prompts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inspect ","type":"text"},{"text":".claude/agents","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":".codex/agents","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inspect ","type":"text"},{"text":".mcp.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":".codex/config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" MCP servers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inspect ","type":"text"},{"text":".claude/settings.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" hooks → ","type":"text"},{"text":".codex/hooks.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Migrate safe selected artifacts → Codex files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate generated ","type":"text"},{"text":".codex/config.toml","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate generated ","type":"text"},{"text":".codex/agents","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report migrated artifacts and manual-review items","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/differences.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (and refresh Codex docs if its ","type":"text"},{"text":"Docs last checked","type":"text","marks":[{"type":"code_inline"}]},{"text":" date is old).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scan and inspect before writing:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--scan-only","type":"text","marks":[{"type":"code_inline"}]},{"text":" lists active and inactive source surfaces.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" prints staged Codex artifact paths and report rows.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":" summarizes readiness, manual-review work, and validation risks.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Convert surfaces in the same order the CLI uses:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"instructions: ","type":"text"},{"text":"CLAUDE.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"plugins: report Claude plugin trees and marketplaces as manual migration work","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hooks: rewrite supported Claude hooks into ","type":"text"},{"text":".codex/hooks.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" and enable ","type":"text"},{"text":"[features].codex_hooks = true","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"skills and commands: write Codex skills under ","type":"text"},{"text":".agents/skills/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"config: write ","type":"text"},{"text":".codex/config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" from Claude model/sandbox settings and MCP servers, including ","type":"text"},{"text":"personality = \"friendly\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" when config is generated","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"subagents: write Codex custom agents under ","type":"text"},{"text":".codex/agents/","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dry-run, then write the selected target. Use ","type":"text"},{"text":"--replace","type":"text","marks":[{"type":"code_inline"}]},{"text":" only when orphan generated skills or agents should be deleted.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inspect the terminal output and ","type":"text"},{"text":".codex/migrate-to-codex-report.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" after real runs.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Review generated artifacts in this order: ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".agents/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".codex/config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".codex/hooks.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".codex/agents/","type":"text","marks":[{"type":"code_inline"}]},{"text":", then report-only plugin items.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"--validate-target","type":"text","marks":[{"type":"code_inline"}]},{"text":" against each target after edits.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Re-run checks and ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" after edits.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Return the final migration report as one markdown table per scope that has rows. The tables cover only the non-native follow-up migration work you performed, such as skills created from slash commands, subagents, MCP servers, hooks, unsupported/local plugin notes, and manual-review caveats. Include programmatic native import rows for config, instructions, skills, or supported plugins only if you personally migrated them in this follow-up run.","type":"text"}]},{"type":"paragraph","content":[{"text":"If only one scope has rows, render only the table with no heading. If multiple scopes have rows, render one heading before each table. Use ","type":"text"},{"text":"**User Config**","type":"text","marks":[{"type":"code_inline"}]},{"text":" for user-scope rows. For project-scope rows, use the actual project folder name as the heading, for example ","type":"text"},{"text":"**northstar-support-portal**","type":"text","marks":[{"type":"code_inline"}]},{"text":"; do not use ","type":"text"},{"text":"Current Project","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the heading. Do not add prose before or after the table output.","type":"text"}]},{"type":"paragraph","content":[{"text":"Use exactly these columns:","type":"text"}]},{"type":"paragraph","content":[{"text":"northstar-support-portal","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Item","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Added","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Slash command","type":"text","marks":[{"type":"code_inline"}]},{"text":" pr-review","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Converted into a Codex skill","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Added","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Subagent","type":"text","marks":[{"type":"code_inline"}]},{"text":" release-lead","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Added as a Codex subagent","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check before using","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hook","type":"text","marks":[{"type":"code_inline"}]},{"text":" PreToolUse","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Converted, but some Claude hook behavior differs in Codex","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not Added","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hook","type":"text","marks":[{"type":"code_inline"}]},{"text":" Notification","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Codex does not have an equivalent notification hook","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not Added","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":" team-macros","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plugin needs manual setup","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Status","type":"text","marks":[{"type":"code_inline"}]},{"text":" must be ","type":"text"},{"text":"Added","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Check before using","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"Not Added","type":"text","marks":[{"type":"code_inline"}]},{"text":". Use ","type":"text"},{"text":"Added","type":"text","marks":[{"type":"code_inline"}]},{"text":" when a Codex-facing artifact was created or changed and needs no special review. Use ","type":"text"},{"text":"Check before using","type":"text","marks":[{"type":"code_inline"}]},{"text":" when a Codex-facing artifact was created or changed but the migration changed semantics, inferred behavior, preserved tool rules as guidance, or dropped unsupported behavior. Use ","type":"text"},{"text":"Not Added","type":"text","marks":[{"type":"code_inline"}]},{"text":" when a source artifact was detected but no Codex-facing artifact was created. ","type":"text"},{"text":"Item","type":"text","marks":[{"type":"code_inline"}]},{"text":" combines the artifact type and concrete item name in one cell. Artifact type must be singular: ","type":"text"},{"text":"Skill","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Slash command","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Subagent","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"MCP","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Hook","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"Plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":". Wrap the artifact type in inline code; write the item name as plain text after it. ","type":"text"},{"text":"Notes","type":"text","marks":[{"type":"code_inline"}]},{"text":" is always required; never leave it empty. Keep notes short, plain, and literal. Avoid internal implementation terms such as runtime expansion. Prefer phrases like ","type":"text"},{"text":"Converted into a Codex skill","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Added as a Codex subagent","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Added to Codex config","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Converted into a Codex hook","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Converted, but some Claude hook behavior differs in Codex","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Codex does not have an equivalent notification hook","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Plugin needs manual setup","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"Plugin marketplace needs manual setup","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Self-Healing Loop","type":"text"}]},{"type":"paragraph","content":[{"text":"Keep looping until the selected migration is complete:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"--plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"--doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run the migration with ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run the migration for real.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fix every generated ","type":"text"},{"text":"## MANUAL MIGRATION REQUIRED","type":"text","marks":[{"type":"code_inline"}]},{"text":" block and every ","type":"text"},{"text":"manual_fix_required","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"skipped","type":"text","marks":[{"type":"code_inline"}]},{"text":" report row that can be resolved inside Codex artifacts.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"--validate-target","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Re-run the migrator and validator until the report and validator have no actionable generated-artifact fixes left.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Do not edit source Claude Code files, unrelated project code, secrets, or another repository during this loop. If a report row requires source-provider changes or product judgment, leave the generated Codex artifact with clear manual guidance instead of changing the source.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Commands","type":"text"}]},{"type":"paragraph","content":[{"text":"Choose the migrator command.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"MIGRATE_TO_CODEX='python3 .codex/skills/migrate-to-codex/scripts/migrate-to-codex.py'","type":"text"}]},{"type":"paragraph","content":[{"text":"Inspect the migration before writing.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"$MIGRATE_TO_CODEX --source ~/.claude/ --scan-only\n$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/ --plan\n$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/ --doctor","type":"text"}]},{"type":"paragraph","content":[{"text":"Dry-run, then run without ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":", for global and project.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/ --dry-run\n$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/\n$MIGRATE_TO_CODEX --source ./.claude/ --target ./.codex/ --dry-run\n$MIGRATE_TO_CODEX --source ./.claude/ --target ./.codex/","type":"text"}]},{"type":"paragraph","content":[{"text":"Run the post-migration validator against each target after edits.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"$MIGRATE_TO_CODEX --validate-target ~/.codex/\n$MIGRATE_TO_CODEX --validate-target ./.codex/","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Run ","type":"text"},{"text":"$MIGRATE_TO_CODEX --help","type":"text","marks":[{"type":"code_inline"}]},{"text":" for flags (","type":"text"},{"text":"--scan-only","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--plan","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--validate-target","type":"text","marks":[{"type":"code_inline"}]},{"text":", defaults, and so on). Deep tables and more links are in ","type":"text"},{"text":"references/differences.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"migrate-to-codex","author":"@skillopedia","source":{"stars":21051,"repo_name":"skills","origin_url":"https://github.com/openai/skills/blob/HEAD/skills/.curated/migrate-to-codex/SKILL.md","repo_owner":"openai","body_sha256":"712c01b0a6d25add13df4fa9a5b2febe18458804647d50a3d725c9992d8a29b3","cluster_key":"e5e4a2a74653f8f39485a0f07538e62e700faa5ebdc1e4fa1cb6c2a8a5eaf7f8","clean_bundle":{"format":"clean-skill-bundle-v1","source":"openai/skills/skills/.curated/migrate-to-codex/SKILL.md","attachments":[{"id":"a433369e-814a-584a-b9f3-5ff23485d2a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a433369e-814a-584a-b9f3-5ff23485d2a9/attachment.yaml","path":"agents/openai.yaml","size":144,"sha256":"d59cec74a85ea881e5ddf8517302026242af004f1d2a7667e1efc433b51295c2","contentType":"application/yaml; charset=utf-8"},{"id":"c84ffd1c-748f-5143-afac-eff698fcca7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c84ffd1c-748f-5143-afac-eff698fcca7f/attachment.md","path":"references/differences.md","size":13735,"sha256":"6e32f5b3140863863ba89b1736b92e4a378f6ccc4785b9f427fc1f13a625916c","contentType":"text/markdown; charset=utf-8"},{"id":"c1f67544-8e5e-5117-9245-c23d9b8f28c8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c1f67544-8e5e-5117-9245-c23d9b8f28c8/attachment.py","path":"scripts/cli.py","size":30124,"sha256":"b392ec310e683d6a42eae00b88e7b9851ed42b3d4aa99bd6aa327e5a8d5545c8","contentType":"text/x-python; charset=utf-8"},{"id":"39f3ae94-b62f-59ea-872b-fb7c4b9e4923","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/39f3ae94-b62f-59ea-872b-fb7c4b9e4923/attachment.py","path":"scripts/migrate-to-codex.py","size":208,"sha256":"6c0b8cd8c0ea610ebe1c4c9184fad3dd5f71520f6341e7a605976544417d12ea","contentType":"text/x-python; charset=utf-8"},{"id":"ec6b2566-0bb2-5ffc-9c74-c2dafcbdc526","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ec6b2566-0bb2-5ffc-9c74-c2dafcbdc526/attachment.py","path":"scripts/migrate/__init__.py","size":285,"sha256":"45c26b88f1b130dfd4294b1ff1c18eb7f8ab9cf14508589292d7f5a7c6133ac9","contentType":"text/x-python; charset=utf-8"},{"id":"ca77d678-5321-5856-a68f-9cd48755a4bf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ca77d678-5321-5856-a68f-9cd48755a4bf/attachment.py","path":"scripts/migrate/agents.py","size":9991,"sha256":"7859242aaaa9efcaf05ed03564610a96d371e085d90f77babac93d437a645bd5","contentType":"text/x-python; charset=utf-8"},{"id":"fc9c4e05-bed0-57db-b7bb-38436800b6a6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fc9c4e05-bed0-57db-b7bb-38436800b6a6/attachment.py","path":"scripts/migrate/codex_config.py","size":4031,"sha256":"5b092657150e24a938a55f2f17602aa8a6f4dfb6548f77613f8c7f9ed142c25d","contentType":"text/x-python; charset=utf-8"},{"id":"69da24e9-8f37-5f12-b762-fd4729e466cb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/69da24e9-8f37-5f12-b762-fd4729e466cb/attachment.py","path":"scripts/migrate/common.py","size":11492,"sha256":"c12107cca109dcc171b3dd48e93e5ee56d7e805e7a86c3341d0cc72c1a33845c","contentType":"text/x-python; charset=utf-8"},{"id":"82b37236-0a56-5d6c-8847-0e9430f7ab41","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/82b37236-0a56-5d6c-8847-0e9430f7ab41/attachment.py","path":"scripts/migrate/hooks.py","size":8194,"sha256":"4465c0537a3750a6e7095db0f2a41f087eb3e63053482dc33f876967e87d8616","contentType":"text/x-python; charset=utf-8"},{"id":"72aaa9b9-a4f5-5003-9d19-a0c897fd0656","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/72aaa9b9-a4f5-5003-9d19-a0c897fd0656/attachment.py","path":"scripts/migrate/instructions.py","size":2434,"sha256":"321284ce5b0b17b5691b7a4f4020d813e28e336578e8c6764f34626f2b449ea1","contentType":"text/x-python; charset=utf-8"},{"id":"a178d992-cd48-5543-81d6-988d91c9357d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a178d992-cd48-5543-81d6-988d91c9357d/attachment.py","path":"scripts/migrate/mcps.py","size":7078,"sha256":"3e5903a3d0a7e09d1a6d8f6d702107b81e661486eef7cdc10012014dc734e4e5","contentType":"text/x-python; charset=utf-8"},{"id":"b5efbe41-a2da-59db-b06e-042a88951fd7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b5efbe41-a2da-59db-b06e-042a88951fd7/attachment.py","path":"scripts/migrate/plugins.py","size":660,"sha256":"fec8ca75b4e9467aadc5dc822aa24c8c7f44415debde77ce8761d1d8014eaf26","contentType":"text/x-python; charset=utf-8"},{"id":"781ec2dd-ad1a-5f56-ab11-25a58780f370","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/781ec2dd-ad1a-5f56-ab11-25a58780f370/attachment.py","path":"scripts/migrate/settings.py","size":1027,"sha256":"13a659e2dbba2895b6d4289b1eb62c33924f95bf45d5e4f2fbdd07fe4f35639a","contentType":"text/x-python; charset=utf-8"},{"id":"6c6c1e63-e259-5a81-b111-99959d2996ac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6c6c1e63-e259-5a81-b111-99959d2996ac/attachment.py","path":"scripts/migrate/skills.py","size":12605,"sha256":"738f25b09345d84f79cc517e3d0862e4c154b612cf459ab6db00099b827e6eea","contentType":"text/x-python; charset=utf-8"},{"id":"101f636e-4aa4-5f8d-8138-827bc2fc26f4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/101f636e-4aa4-5f8d-8138-827bc2fc26f4/attachment.py","path":"scripts/utils/__init__.py","size":35,"sha256":"884b593e41718aed59d3fd46ff77ddafccf422554f0ca0e39a37722ad4b148b4","contentType":"text/x-python; charset=utf-8"},{"id":"7ea1bf97-eae9-5d64-a0b1-7868dc86094c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7ea1bf97-eae9-5d64-a0b1-7868dc86094c/attachment.py","path":"scripts/utils/scan.py","size":4370,"sha256":"d7fcce39222a732cc91eff58c23e4319e3bd437e10eaf4e2084193c245a21ab0","contentType":"text/x-python; charset=utf-8"},{"id":"7fb2b498-1d95-5f06-9eff-76302fa21532","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7fb2b498-1d95-5f06-9eff-76302fa21532/attachment.py","path":"scripts/utils/util.py","size":9441,"sha256":"d8eb5a1e99d60bbef3b17b118fc6a082edd6c39edacadab5d9c89a11311a5c0c","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"9be9fba2413c716a708b09c1d5f2f8350a9403c356e89decfebde67a5ce77f03","attachment_count":17,"text_attachments":17,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/.curated/migrate-to-codex/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"ai-agent-development","category_label":"AI"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"ai-agent-development","import_tag":"clean-skills-v1","description":"Migrate supported instruction files, skills, agents, and MCP config into Codex project and global files."}},"renderedAt":1782987310857}

Migrate to Codex Autonomy Keep going until the selected migration is completely done: run the migrator, inspect the report, fix migrated Codex instructions/skills/agents/MCP config, and re-run checks without stopping to ask for confirmation of the next step. If the user has selected a target, do not ask before creating, editing, replacing, or deleting generated Codex artifacts in that target ( , , , or ). Preserve unrelated existing Codex config entries in or , such as , , , or unrelated MCP servers; do not ask about them unless they fail validation or directly conflict with the migration. Do…