diff --git a/.gitignore b/.gitignore index 0540e0d..fd8f416 100755 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ Test-SecondBrain/ .claude/settings.local.json .mcp.json # GSD planning artifacts (internal workflow — not for upstream) -.planning/ \ No newline at end of file +.planning/ diff --git a/README.md b/README.md index f76cc97..ebb2383 100755 --- a/README.md +++ b/README.md @@ -471,12 +471,15 @@ My-Brain-Is-Full-Crew/ ← cloned inside your vault ├── docs/ User-facing documentation │ ├── getting-started.md Step-by-step setup guide │ ├── examples.md Real-world usage examples +│ ├── codex-cli.md Codex CLI platform guide +│ ├── codex-migration.md Migrating to Codex CLI from other platforms │ └── agents/ Deep-dive into each agent ├── adapters/ Platform adapters (build system) │ ├── lib.sh Shared parsing and rewrite helpers │ ├── claude-code/ Claude Code adapter │ ├── gemini-cli/ Gemini CLI adapter -│ └── opencode/ OpenCode adapter +│ ├── opencode/ OpenCode adapter +│ └── codex-cli/ Codex CLI adapter ├── mcp/servers.yaml MCP server definitions (source of truth) ├── LICENSE ├── README.md You are here diff --git a/adapters/codex-cli/adapter.sh b/adapters/codex-cli/adapter.sh index be6c773..5cfa287 100644 --- a/adapters/codex-cli/adapter.sh +++ b/adapters/codex-cli/adapter.sh @@ -39,24 +39,24 @@ rewrite_codex_paths() { # path references are already resolved. # # Rewrites applied (T-01-06): -# - `AskUserQuestion` / AskUserQuestion → "ask the user" (preserves one-at-a-time constraint phrasing elsewhere) -# - `request_user_input` / request_user_input → "ask the user" +# - `AskUserQuestion` / AskUserQuestion → `request_user_input` (Codex native equivalent) # - "Skill tool" → "invoke the skill" # - "Agent tool" → "invoke the agent" # - "Read tool" → "read files" # - "Glob tool" → "search files" # - "Grep tool" → "search files" # - "Bash tool" → "shell" +# +# NOTE: `request_user_input` is a real Codex CLI tool — do NOT rewrite it. rewrite_tool_compat() { local file="$1" [[ -f "$file" ]] || return 0 # Use perl for reliable in-place multi-substitution across all platforms. # Each substitution is a plain string replacement (no regex heavy-lifting). + # request_user_input is intentionally NOT rewritten — it exists in Codex CLI. perl -i -pe ' - s/`AskUserQuestion`/ask the user/g; - s/\bAskUserQuestion\b/ask the user/g; - s/`request_user_input`/ask the user/g; - s/\brequest_user_input\b/ask the user/g; + s/`AskUserQuestion`/`request_user_input`/g; + s/\bAskUserQuestion\b/request_user_input/g; s/\bSkill tool\b/invoke the skill/g; s/\bAgent tool\b/invoke the agent/g; s/\bRead tool\b/read files/g; @@ -100,7 +100,6 @@ normalize_codex_routing_contract() { perl -0pi -e ' s/\binvoke the skill\b/follow the skill instructions directly in the root context/g; s/\binvoke the agent\b/spawn a bounded child agent from the root context/g; - s/\bask the user\b/ask the user directly in chat and wait for the reply/g; ' "$file" } @@ -194,12 +193,14 @@ _cc_rewrite_skill_markdown() { case "$skill_name" in onboarding) + # After rewrite_tool_compat, AskUserQuestion → request_user_input. + # Now normalise the phrasing around request_user_input for Codex's + # conversational flow (one question at a time, resume from state file). perl -0pi -e ' - s/You MUST use the ask the user tool for EVERY question in every phase\. This is not optional\. This is how the onboarding works:/Use direct chat for every question in every phase. Ask one direct plain-text question, wait for the user'\''s reply before continuing, and resume from the saved state file if the flow is already active. This is not optional. This is how the onboarding works:/g; - s/1\. Ask ONE question using ask the user/1. Ask one direct plain-text question/g; - s/2\. Read the user'\''s answer/2. Wait for the user'\''s reply before continuing/g; - s/4\. Ask the NEXT question using ask the user/4. Ask the next direct plain-text question/g; - s/\*\*ONE question per ask the user call\.\*\* Never bundle 2\+ questions in one message\./**One direct plain-text question at a time.** Never bundle 2+ questions in one message./g; + s/You MUST use the `?request_user_input`? tool for EVERY question in every phase\. This is not optional\. This is how the onboarding works:/Use `request_user_input` for every question in every phase. Ask one question at a time, wait for the user'\''s reply before continuing, and resume from the saved state file if the flow is already active. This is not optional. This is how the onboarding works:/g; + s/1\. Ask ONE question using `?request_user_input`?/1. Ask one question using `request_user_input`/g; + s/4\. Ask the NEXT question using `?request_user_input`?/4. Ask the next question using `request_user_input`/g; + s/\*\*ONE question per `?request_user_input`? call\.\*\* Never bundle 2\+ questions in one message\./**One question per `request_user_input` call.** Never bundle 2+ questions in one message./g; s/\.codex\/agents\/\{name\}\.md/.codex\/agents\/{name}.toml/g; s/\.mcp\.json/.codex\/config.toml/g; s/\$HOME\/\.platform\/agents/\$HOME\/.codex\/agents/g; @@ -207,18 +208,17 @@ _cc_rewrite_skill_markdown() { ;; create-agent) perl -0pi -e ' - s/You MUST use the ask the user tool for EVERY question in every phase\. This is not optional\. This is how the conversation works:/Use direct chat for every question in every phase. Ask one direct plain-text question, wait for the user'\''s reply before continuing, and resume from the saved state file if the flow is already active. This is not optional. This is how the conversation works:/g; - s/1\. Ask ONE question using ask the user/1. Ask one direct plain-text question/g; - s/2\. Read the user'\''s answer/2. Wait for the user'\''s reply before continuing/g; - s/4\. Ask the NEXT question using ask the user/4. Ask the next direct plain-text question/g; - s/\*\*ONE question per ask the user call\.\*\* Never bundle 2\+ questions\./**One direct plain-text question at a time.** Never bundle 2+ questions./g; + s/You MUST use the `?request_user_input`? tool for EVERY question in every phase\. This is not optional\. This is how the conversation works:/Use `request_user_input` for every question in every phase. Ask one direct plain-text question at a time, wait for the user'\''s reply before continuing, and resume from the saved state file if the flow is already active. This is not optional. This is how the conversation works:/g; + s/1\. Ask ONE question using `?request_user_input`?/1. Ask one direct plain-text question using `request_user_input`/g; + s/4\. Ask the NEXT question using `?request_user_input`?/4. Ask the next question using `request_user_input`/g; + s/\*\*ONE question per `?request_user_input`? call\.\*\* Never bundle 2\+ questions\./**One question per `request_user_input` call.** Never bundle 2+ questions./g; s/\.codex\/agents\/\{name\}\.md/.codex\/agents\/{name}.toml/g; ' "$file" ;; manage-agent) perl -0pi -e ' - s/using `ask the user`/by asking the user directly/g; - s/Use `ask the user` to/Ask one direct plain-text question to/g; + s/using `request_user_input`/using `request_user_input`/g; + s/Use `request_user_input` to/Use `request_user_input` to/g; s/If the user specifies a name, read `\.codex\/agents\/\{name\}\.md`/If the user specifies a name, read `\.codex\/agents\/{name}.toml`/g; s/Modify the agent file at `\.codex\/agents\/\{name\}\.md`/Modify the agent file at `\.codex\/agents\/{name}.toml`/g; s/locate `\.codex\/agents\/\{name\}\.md`/locate `\.codex\/agents\/{name}.toml`/g; @@ -229,15 +229,15 @@ _cc_rewrite_skill_markdown() { ## Codex Conversation Flow -- Ask one direct plain-text question when clarification is required. -- Wait for the user's reply before continuing. +- Use `request_user_input` when clarification is required. +- Ask one question at a time, wait for the user's reply before continuing. - Resume from the saved state file if the flow is already active. EOF fi ;; transcribe) perl -0pi -e ' - s/Use ask the user to collect:/Use direct chat to collect this intake context: ask one direct plain-text question, wait for the user'\''s reply before continuing, and resume from the saved state file if the flow is already active. Collect:/g; + s/Use `?request_user_input`? to collect:/Use `request_user_input` to collect this intake context: ask one direct plain-text question at a time, wait for the user'\''s reply before continuing, and resume from the saved state file if the flow is already active. Collect:/g; ' "$file" ;; esac @@ -295,24 +295,45 @@ adapter_translate_skills() { } # _cc_toml_quote_key -# Emits the TOML table key for [mcp_servers.]. -# Codex CLI requires MCP server names to match ^[a-zA-Z0-9_-]+$, so any -# character outside that set is replaced with a hyphen before writing. +# Emits the TOML table header for [mcp_servers.]. +# If the name contains only bare-key chars (A-Za-z0-9_-), it is emitted +# unquoted. Otherwise it is wrapped in double-quotes with backslash and +# double-quote characters escaped, per TOML spec §3.1. +# +# Security: never interpolate unsanitized user input into TOML keys — this +# function is the single gate for all MCP server names. _cc_toml_quote_key() { local name="$1" - # Sanitize: replace any character not in [a-zA-Z0-9_-] with a hyphen - local safe_name="${name//[^a-zA-Z0-9_-]/-}" - printf '[mcp_servers.%s]' "$safe_name" + # Security: reject control characters (newlines, tabs) that would break + # the TOML table header across lines and enable injection. + if [[ "$name" == *$'\n'* ]] || [[ "$name" == *$'\r'* ]] || [[ "$name" == *$'\t'* ]]; then + echo "# ERROR: server name contains control characters — skipped: $name" >&2 + return 1 + fi + if [[ "$name" =~ ^[A-Za-z0-9_-]+$ ]]; then + # Bare key — safe as-is + printf '[mcp_servers.%s]' "$name" + else + # Quoted key — escape \ and " per TOML basic-string rules + local escaped + escaped="${name//\\/\\\\}" + escaped="${escaped//\"/\\\"}" + printf '[mcp_servers."%s"]' "$escaped" + fi } # _cc_toml_escape_string -# Escapes a value for use in a TOML double-quoted string. -# Escapes backslashes and double-quotes; other characters pass through. +# Escapes a value for use in a TOML double-quoted basic string. +# Escapes backslashes, double-quotes, and control characters (newlines, tabs, +# carriage returns) to prevent line-break injection in single-line TOML values. _cc_toml_escape_string() { local val="$1" - # Escape backslash first, then double-quote + # Escape backslash first (before adding new backslashes), then other chars val="${val//\\/\\\\}" val="${val//\"/\\\"}" + val="${val//$'\n'/\\n}" + val="${val//$'\r'/\\r}" + val="${val//$'\t'/\\t}" printf '%s' "$val" } @@ -343,18 +364,19 @@ adapter_translate_config() { echo 'sandbox_mode = "workspace-write"' echo 'sandbox_workspace_write.network_access = false' echo '' - echo '# Inherit uses the top-level defaults; named profiles override them explicitly.' + echo '# Named profiles override the top-level defaults.' + echo '# Update these model IDs when newer models become available.' echo '[profiles.quality]' - echo 'model = "gpt-5.4"' + echo 'model = "o3"' echo 'model_reasoning_effort = "high"' echo '' echo '[profiles.balanced]' - echo 'model = "gpt-5.4-mini"' + echo 'model = "o4-mini"' echo 'model_reasoning_effort = "medium"' echo '' echo '[profiles.budget]' - echo 'model = "gpt-5.3-codex-spark"' - echo 'model_reasoning_effort = "medium"' + echo 'model = "o4-mini"' + echo 'model_reasoning_effort = "low"' echo '' echo '[agents]' echo 'max_depth = 1' @@ -526,24 +548,28 @@ _cc_extract_description() { # _cc_toml_escape_dquote_string # Escapes a string for embedding in a TOML double-quoted basic string. -# Handles backslash and double-quote characters. +# Handles backslash, double-quote, and control characters. _cc_toml_escape_dquote_string() { local val="$1" val="${val//\\/\\\\}" val="${val//\"/\\\"}" + val="${val//$'\n'/\\n}" + val="${val//$'\r'/\\r}" + val="${val//$'\t'/\\t}" printf '%s' "$val" } # _cc_model_to_codex # Maps the source model tier into a Codex model id. +# Uses current production OpenAI models. Update when newer models ship. _cc_model_to_codex() { local model="$1" case "$model" in - */*|gpt-*) echo "$model" ;; - low) echo "gpt-5.3-codex-spark" ;; - mid) echo "gpt-5.4-mini" ;; - high) echo "gpt-5.4" ;; - *) echo "$model" ;; + */*|gpt-*|o[0-9]*) echo "$model" ;; + low) echo "o4-mini" ;; + mid) echo "o4-mini" ;; + high) echo "o3" ;; + *) echo "$model" ;; esac } @@ -562,7 +588,9 @@ _cc_model_reasoning_effort() { # explicitly declare write/edit/bash capabilities. _cc_capabilities_to_sandbox() { local caps="$1" - for cap in $caps; do + local -a tokens + read -ra tokens <<< "$caps" + for cap in "${tokens[@]}"; do case "$cap" in write|edit|bash) echo "workspace-write" @@ -594,6 +622,13 @@ adapter_translate_agent_toml() { local name; name="$(parse_frontmatter "$agent_file" name)" [[ -z "$name" ]] && name="$(basename "$agent_file" .md)" + # Security: reject path-separator characters and traversal sequences to + # prevent writes outside the target agents directory. + if [[ "$name" == */* ]] || [[ "$name" == *..* ]] || [[ -z "$name" ]]; then + echo "ERROR: unsafe agent name rejected: '$name'" >&2 + return 1 + fi + local description; description="$(_cc_extract_description "$agent_file")" local desc_escaped; desc_escaped="$(_cc_toml_escape_dquote_string "$description")" local model_raw; model_raw="$(parse_frontmatter "$agent_file" model)" diff --git a/docs/codex-cli.md b/docs/codex-cli.md index 55a9936..6030f8f 100644 --- a/docs/codex-cli.md +++ b/docs/codex-cli.md @@ -107,14 +107,17 @@ Codex CLI enforces `agents.max_depth = 1`. This means child agents can only go o ### Tool name differences -Codex CLI does not have the `AskUserQuestion` or `request_user_input` tools. The equivalent patterns are: +Codex CLI has its own tool set. Some Claude Code tools do not exist in Codex; others have native equivalents. `request_user_input` is a real Codex CLI tool — use it directly for follow-up questions. | Source concept | Codex CLI equivalent | |---|---| -| `AskUserQuestion` | Ask a direct question in the chat thread and wait for the reply | -| `request_user_input` | Same — use the root conversation for follow-up questions | +| `AskUserQuestion` | `request_user_input` (native Codex tool) — ask a follow-up question and wait for the reply | +| `request_user_input` | Same tool name exists natively in Codex CLI — no translation needed | | `Skill tool` | Follow the skill instructions directly in the root context | | `Agent tool` | Use `spawn_agent` for a bounded child task; orchestration returns to root | +| `Read tool` | `shell` (e.g. `cat`) — Codex has no dedicated read_file tool | +| `Glob tool` / `Grep tool` | `shell` (e.g. `find`, `grep`) or `list_dir` (experimental) | +| `Bash tool` | `shell` — execute shell commands | | `max chain depth 3` | `agents.max_depth = 1` with root-only orchestration | | `.mcp.json` | `.codex/config.toml` | @@ -128,31 +131,33 @@ Claude Code uses `.mcp.json`. Codex CLI uses `.codex/config.toml`. The MCP serve Use this table to verify the Crew works correctly in a real Codex vault after install or update. Run each row and compare the result against the expected outcome. +> **Note:** Use natural-language prompts. The dispatcher in `AGENTS.md` routes to the correct agent based on intent — you do not need to address agents by name. + | Surface | Name | Prompt or command | Expected result | |---------|------|-------------------|----------------| -| Agent | Architect | `@Architect Set up my vault structure` | Architect starts onboarding conversation or confirms vault is already set up | -| Agent | Scribe | `@Scribe Save this note: quick test` | Scribe creates a note in 00-Inbox with proper frontmatter | -| Agent | Sorter | `@Sorter Triage my inbox` | Sorter reviews inbox notes and files them, or reports inbox is empty | -| Agent | Seeker | `@Seeker What do I know about this project?` | Seeker searches the vault and returns results with source citations | -| Agent | Connector | `@Connector Find connections in my recent notes` | Connector analyzes the vault graph and suggests wikilinks | -| Agent | Librarian | `@Librarian Run a vault health check` | Librarian scans for broken links, duplicates, and orphan notes | -| Agent | Transcriber | `@Transcriber Process this transcript: [paste text]` | Transcriber generates structured meeting notes | -| Agent | Postman | `@Postman Check my email` | Postman scans Gmail (or Hey) and saves actionable emails, or reports missing integration | -| Skill | onboarding | `/onboarding` | Architect starts the full onboarding conversation | -| Skill | create-agent | `/create-agent` | Architect walks through designing a new custom agent | -| Skill | manage-agent | `/manage-agent` | Architect lists, edits, or removes custom agents | -| Skill | defrag | `/defrag` | Architect runs the 5-phase vault defragmentation | -| Skill | email-triage | `/email-triage` | Postman scans and prioritizes unread emails | -| Skill | meeting-prep | `/meeting-prep` | Postman generates a comprehensive meeting brief | -| Skill | weekly-agenda | `/weekly-agenda` | Postman produces a day-by-day week overview | -| Skill | deadline-radar | `/deadline-radar` | Postman produces a unified deadline timeline | -| Skill | transcribe | `/transcribe` | Transcriber processes a recording or transcript into structured notes | -| Skill | vault-audit | `/vault-audit` | Librarian runs the full 7-phase vault audit | -| Skill | deep-clean | `/deep-clean` | Librarian runs the extended vault cleanup | -| Skill | tag-garden | `/tag-garden` | Librarian analyzes and cleans up tags | -| Skill | inbox-triage | `/inbox-triage` | Sorter processes and routes all inbox notes | -| Skill | contact-sync | `/contact-sync` | Postman syncs contacts to Apple Contacts | -| Chaining | bounded child-agent chain | `@Sorter Triage my inbox` (with notes present that mention a new project) | Sorter files notes, then dispatcher signals Architect to create the new project folder; child returns to root before Architect runs | +| Agent | Architect | `Set up my vault structure` | Architect starts onboarding conversation or confirms vault is already set up | +| Agent | Scribe | `Save this note: quick test` | Scribe creates a note in 00-Inbox with proper frontmatter | +| Agent | Sorter | `Batch sort my inbox` | Sorter reviews inbox notes and files them, or reports inbox is empty | +| Agent | Seeker | `What do I know about this project?` | Seeker searches the vault and returns results with source citations | +| Agent | Connector | `Find connections in my recent notes` | Connector analyzes the vault graph and suggests wikilinks | +| Agent | Librarian | `Run a vault health check` | Librarian scans for broken links, duplicates, and orphan notes | +| Agent | Transcriber | `Process this transcript: [paste text]` | Transcriber generates structured meeting notes | +| Agent | Postman | `Check my email` | Postman scans Gmail (or Hey) and saves actionable emails, or reports missing integration | +| Skill | onboarding | `Initialize my vault` | Architect starts the full onboarding conversation | +| Skill | create-agent | `Create a new agent` | Architect walks through designing a new custom agent | +| Skill | manage-agent | `List my agents` | Architect lists, edits, or removes custom agents | +| Skill | defrag | `Defragment the vault` | Architect runs the 5-phase vault defragmentation | +| Skill | email-triage | `What's in my inbox?` (email) | Postman scans and prioritizes unread emails | +| Skill | meeting-prep | `Prepare for the meeting` | Postman generates a comprehensive meeting brief | +| Skill | weekly-agenda | `What's this week?` | Postman produces a day-by-day week overview | +| Skill | deadline-radar | `What are my deadlines?` | Postman produces a unified deadline timeline | +| Skill | transcribe | `Transcribe this recording` | Transcriber processes a recording or transcript into structured notes | +| Skill | vault-audit | `Weekly review` | Librarian runs the full 7-phase vault audit | +| Skill | deep-clean | `Deep clean the vault` | Librarian runs the extended vault cleanup | +| Skill | tag-garden | `Clean up tags` | Librarian analyzes and cleans up tags | +| Skill | inbox-triage | `Triage the inbox` | Sorter processes and routes all inbox notes | +| Skill | contact-sync | `Sync my contacts` | Postman syncs contacts to Apple Contacts | +| Chaining | bounded child-agent chain | `Batch sort my inbox` (with notes mentioning a new project) | Sorter files notes, then dispatcher signals Architect to create the new project folder; child returns to root before Architect runs | | MCP | MCP visibility | `codex -C mcp list` | Lists the MCP servers configured in `.codex/config.toml`, or shows the auth/setup state for each server | ### Running the non-interactive discovery smoke diff --git a/docs/codex-migration.md b/docs/codex-migration.md index ca71a80..8ff9523 100644 --- a/docs/codex-migration.md +++ b/docs/codex-migration.md @@ -123,19 +123,22 @@ When you run the installer, the 8 core crew agents are automatically translated ### What the TOML format looks like +Codex CLI agent files use top-level keys (not nested under `[agent]`). The required fields are `name`, `description`, and `developer_instructions`: + ```toml -[agent] name = "budget-tracker" description = "Monitors spending notes and flags when you are close to the monthly limit" model = "o4-mini" - -[agent.prompt] -content = """ +model_reasoning_effort = "medium" +sandbox_mode = "workspace-write" +developer_instructions = ''' You are the Budget Tracker agent for the My Brain Is Full — Crew system. ... (your agent instructions here) -""" +''' ``` +See the [Codex CLI agent roles documentation](https://developers.openai.com/codex/subagents) for the full schema. + --- ## Verification after migration diff --git a/mcp/servers.yaml b/mcp/servers.yaml index 8c0f6c3..fb5a463 100755 --- a/mcp/servers.yaml +++ b/mcp/servers.yaml @@ -4,7 +4,7 @@ servers: url: "https://gmail.mcp.claude.com/mcp" env: {} exclude: [] - - name: Google-Calendar + - name: Google Calendar type: http url: "https://gcal.mcp.claude.com/mcp" env: {} diff --git a/references/codex-cli-compat.md b/references/codex-cli-compat.md index f887054..26114f3 100644 --- a/references/codex-cli-compat.md +++ b/references/codex-cli-compat.md @@ -8,10 +8,13 @@ Use this reference when source workflows mention platform-specific tools or recu | Source concept | Codex CLI mapping | Notes | |---|---|---| -| `AskUserQuestion` | Ask a direct plain-text question in chat and wait for the reply | Codex uses the root conversation for confirmations and follow-up questions. | -| `request_user_input` | Ask a direct plain-text question in chat and wait for the reply | Use the same root-thread confirmation flow as any other user interaction. | +| `AskUserQuestion` | `request_user_input` (native Codex tool) | Codex has this tool natively — use it to ask follow-up questions and wait for the reply. | +| `request_user_input` | `request_user_input` (same name) | This tool exists in Codex CLI — no translation needed. | | `Skill tool` | Follow the relevant skill instructions directly in the root context | Skills stay in the main chat; do not invent a separate Skill API. | | `Agent tool` | Use `spawn_agent` only for bounded child tasks | The root context keeps orchestration and integration decisions. | +| `Read tool` | `shell` (e.g. `cat`) | Codex has no dedicated read_file tool; use shell commands. | +| `Glob tool` / `Grep tool` | `shell` (e.g. `find`, `grep`) | Or `list_dir` (experimental). | +| `Bash tool` | `shell` | Execute shell commands. | | `max chain depth 3` | `agents.max_depth = 1` with root-only orchestration | A child can finish one bounded task; any next step is decided back in the root context. | | `.mcp.json` | `.codex/config.toml` | Codex MCP and profile settings live in the TOML config. | @@ -25,7 +28,7 @@ Source workflow wording: Codex CLI wording: -1. Ask the user directly in chat and wait for the reply. +1. Use `request_user_input` to ask the user and wait for the reply. 2. Keep the setup flow in the root context by following the skill instructions directly. 3. If a bounded side task remains, use `spawn_agent`, then return to the root context to decide what happens next. diff --git a/tests/adapters/codex-cli/adapter.test.sh b/tests/adapters/codex-cli/adapter.test.sh index 32a7c48..a0c19bc 100644 --- a/tests/adapters/codex-cli/adapter.test.sh +++ b/tests/adapters/codex-cli/adapter.test.sh @@ -198,7 +198,7 @@ test_cc_translate_skills_real_corpus_has_exact_skill_directories() { weekly-agenda ) - mapfile -t actual < <(find "$dst/.agents/skills" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) + mapfile -t actual < <(find "$dst/.agents/skills" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort) [[ "${#actual[@]}" -eq 14 ]] \ || { echo "expected 14 generated skill directories, found ${#actual[@]}: ${actual[*]}"; result=1; } @@ -229,7 +229,6 @@ test_cc_translate_skills_rewrites_high_risk_real_skills_for_codex() { [[ -f "$file" ]] || { echo "missing generated high-risk skill: $file"; result=1; continue; } content="$(cat "$file")" [[ "$content" != *'AskUserQuestion'* ]] || { echo "AskUserQuestion leaked into $(basename "$(dirname "$file")")"; result=1; } - [[ "$content" != *'request_user_input'* ]] || { echo "request_user_input leaked into $(basename "$(dirname "$file")")"; result=1; } [[ "$content" != *'.platform/'* ]] || { echo ".platform/ leaked into $(basename "$(dirname "$file")")"; result=1; } done @@ -470,12 +469,12 @@ EOF local result=0 [[ "$content" == *'sandbox_workspace_write.network_access = false'* ]] || { echo "network_access default missing"; result=1; } [[ "$content" == *'[profiles.quality]'* ]] || { echo "profiles.quality missing"; result=1; } - [[ "$content" == *'model = "gpt-5.4"'* ]] || { echo "quality model missing"; result=1; } + [[ "$content" == *'model = "o3"'* ]] || { echo "quality model missing"; result=1; } [[ "$content" == *'model_reasoning_effort = "high"'* ]] || { echo "quality reasoning effort missing"; result=1; } [[ "$content" == *'[profiles.balanced]'* ]] || { echo "profiles.balanced missing"; result=1; } - [[ "$content" == *'model = "gpt-5.4-mini"'* ]] || { echo "balanced model missing"; result=1; } + [[ "$content" == *'model = "o4-mini"'* ]] || { echo "balanced model missing"; result=1; } [[ "$content" == *'[profiles.budget]'* ]] || { echo "profiles.budget missing"; result=1; } - [[ "$content" == *'model = "gpt-5.3-codex-spark"'* ]] || { echo "budget model missing"; result=1; } + [[ "$content" == *'model = "o4-mini"'* ]] || { echo "budget model missing"; result=1; } rm -rf "$src" "$dst" return $result } @@ -1015,7 +1014,7 @@ test_cc_adapter_build_real_agent_corpus_has_exact_toml_files() { transcriber.toml ) - mapfile -t actual < <(find "$dst/.codex/agents" -maxdepth 1 -name '*.toml' -printf '%f\n' | sort) + mapfile -t actual < <(find "$dst/.codex/agents" -maxdepth 1 -name '*.toml' -exec basename {} \; | sort) [[ "${#actual[@]}" -eq 8 ]] \ || { echo "expected 8 generated agent TOML files, found ${#actual[@]}: ${actual[*]}"; result=1; } @@ -1076,12 +1075,12 @@ test_cc_adapter_build_real_agent_corpus_has_required_fields_and_metadata() { || { echo 'postman.toml should be workspace-write'; result=1; } grep -q '^sandbox_mode = "workspace-write"$' "$dst/.codex/agents/architect.toml" \ || { echo 'architect.toml should be workspace-write'; result=1; } - grep -q '^model = "gpt-5.4"$' "$dst/.codex/agents/architect.toml" \ - || { echo 'architect.toml should map to gpt-5.4'; result=1; } + grep -q '^model = "o3"$' "$dst/.codex/agents/architect.toml" \ + || { echo 'architect.toml should map to o3'; result=1; } grep -q '^model_reasoning_effort = "high"$' "$dst/.codex/agents/architect.toml" \ || { echo 'architect.toml should map to high reasoning effort'; result=1; } - grep -q '^model = "gpt-5.4-mini"$' "$dst/.codex/agents/scribe.toml" \ - || { echo 'scribe.toml should map to gpt-5.4-mini'; result=1; } + grep -q '^model = "o4-mini"$' "$dst/.codex/agents/scribe.toml" \ + || { echo 'scribe.toml should map to o4-mini'; result=1; } grep -q '^model_reasoning_effort = "medium"$' "$dst/.codex/agents/scribe.toml" \ || { echo 'scribe.toml should map to medium reasoning effort'; result=1; } @@ -1094,7 +1093,8 @@ test_cc_adapter_build_real_agent_corpus_has_required_fields_and_metadata() { # --------------------------------------------------------------------------- test_cc_rewrite_tool_compat_removes_ask_user_question() { - # AskUserQuestion (backtick and bare) must be rewritten to "ask the user" + # AskUserQuestion (backtick and bare) must be rewritten to request_user_input + # (Codex CLI native tool) local tmp; tmp="$(mktemp)" cat > "$tmp" <<'EOF' You MUST use the `AskUserQuestion` tool for every question. @@ -1103,24 +1103,23 @@ EOF rewrite_tool_compat "$tmp" local content; content="$(cat "$tmp")" local result=0 - [[ "$content" != *'AskUserQuestion'* ]] || { echo "AskUserQuestion still present: $content"; result=1; } - [[ "$content" == *'ask the user'* ]] || { echo "'ask the user' not found: $content"; result=1; } + [[ "$content" != *'AskUserQuestion'* ]] || { echo "AskUserQuestion still present: $content"; result=1; } + [[ "$content" == *'request_user_input'* ]] || { echo "'request_user_input' not found: $content"; result=1; } rm -f "$tmp" return $result } -test_cc_rewrite_tool_compat_removes_request_user_input() { - # request_user_input (backtick and bare) must be rewritten to "ask the user" +test_cc_rewrite_tool_compat_preserves_request_user_input() { + # request_user_input is a real Codex CLI tool — it must NOT be rewritten local tmp; tmp="$(mktemp)" cat > "$tmp" <<'EOF' Call `request_user_input` to get the answer. -Also request_user_input should not appear. +Also request_user_input should remain. EOF rewrite_tool_compat "$tmp" local content; content="$(cat "$tmp")" local result=0 - [[ "$content" != *'request_user_input'* ]] || { echo "request_user_input still present: $content"; result=1; } - [[ "$content" == *'ask the user'* ]] || { echo "'ask the user' not found: $content"; result=1; } + [[ "$content" == *'request_user_input'* ]] || { echo "request_user_input was removed but should be preserved: $content"; result=1; } rm -f "$tmp" return $result } @@ -1189,8 +1188,8 @@ SKILLEOF return $result } -test_cc_skill_output_contains_no_request_user_input() { - # Skills translated by adapter_translate_skills must not contain request_user_input +test_cc_skill_output_preserves_request_user_input() { + # request_user_input is a real Codex CLI tool — it must be preserved in skill output local src; src="$(mktemp -d)" local dst; dst="$(mktemp -d)" mkdir -p "$src/skills/inbox" @@ -1205,7 +1204,7 @@ SKILLEOF adapter_translate_skills "$src/skills" "$dst" local content; content="$(cat "$dst/.agents/skills/inbox/SKILL.md")" local result=0 - [[ "$content" != *'request_user_input'* ]] || { echo "request_user_input still present in skill output: $content"; result=1; } + [[ "$content" == *'request_user_input'* ]] || { echo "request_user_input should be preserved in skill output: $content"; result=1; } rm -rf "$src" "$dst" return $result } @@ -1280,7 +1279,8 @@ AGENTEOF test_cc_real_create_agent_skill_has_no_ask_user_question() { # The real skills/create-agent/SKILL.md (which has AskUserQuestion) must produce - # zero occurrences of AskUserQuestion in the Codex output + # zero occurrences of AskUserQuestion in the Codex output. + # request_user_input SHOULD be present (it is the Codex native equivalent). local dst; dst="$(mktemp -d)" adapter_translate_skills "$ROOT/skills" "$dst" local result=0 @@ -1289,8 +1289,8 @@ test_cc_real_create_agent_skill_has_no_ask_user_question() { local content; content="$(cat "$skill_out")" [[ "$content" != *'AskUserQuestion'* ]] \ || { echo "AskUserQuestion still present in real create-agent skill output"; result=1; } - [[ "$content" != *'request_user_input'* ]] \ - || { echo "request_user_input still present in real create-agent skill output"; result=1; } + [[ "$content" == *'request_user_input'* ]] \ + || { echo "request_user_input should be present in real create-agent skill output (native Codex tool)"; result=1; } fi rm -rf "$dst" return $result @@ -1343,8 +1343,9 @@ test_cc_agents_md_header_is_idempotent() { return $result } -test_cc_agents_md_has_no_tool_names_after_full_build() { - # End-to-end: after adapter_build, AGENTS.md has no unsupported tool names +test_cc_agents_md_has_no_unsupported_tool_names_after_full_build() { + # End-to-end: after adapter_build, AGENTS.md has no Claude-only tool names. + # request_user_input IS allowed (native Codex tool). local src; src="$(mktemp -d)" local dst; dst="$(mktemp -d)" mkdir -p "$src/mcp" @@ -1367,9 +1368,10 @@ EOF local content; content="$(cat "$dst/AGENTS.md")" local result=0 [[ "$content" != *'AskUserQuestion'* ]] || { echo "AskUserQuestion in AGENTS.md after build"; result=1; } - [[ "$content" != *'request_user_input'* ]] || { echo "request_user_input in AGENTS.md after build"; result=1; } [[ "$content" != *'Skill tool'* ]] || { echo "Skill tool in AGENTS.md after build"; result=1; } [[ "$content" != *'Agent tool'* ]] || { echo "Agent tool in AGENTS.md after build"; result=1; } + # request_user_input SHOULD be present (it is the Codex equivalent of AskUserQuestion) + [[ "$content" == *'request_user_input'* ]] || { echo "request_user_input should be present in AGENTS.md after build"; result=1; } grep -qF '' "$dst/AGENTS.md" \ || { echo "CODEX-ROUTING-HEADER missing after full build"; result=1; } rm -rf "$src" "$dst" @@ -1389,7 +1391,7 @@ test_cc_adapter_build_real_dispatcher_references_codex_compat_contract() { [[ "$content" != *'Skill tool'* ]] || { echo 'Skill tool leaked into real AGENTS.md output'; result=1; } [[ "$content" != *'Agent tool'* ]] || { echo 'Agent tool leaked into real AGENTS.md output'; result=1; } [[ "$content" != *'AskUserQuestion'* ]] || { echo 'AskUserQuestion leaked into real AGENTS.md output'; result=1; } - [[ "$content" != *'request_user_input'* ]] || { echo 'request_user_input leaked into real AGENTS.md output'; result=1; } + # request_user_input IS expected (native Codex tool, replaces AskUserQuestion) rm -rf "$dst" return $result @@ -1447,7 +1449,8 @@ EOF [[ "$content" != *'Skill tool'* ]] || { echo 'Skill tool should be normalized'; result=1; } [[ "$content" != *'Agent tool'* ]] || { echo 'Agent tool should be normalized'; result=1; } [[ "$content" != *'AskUserQuestion'* ]] || { echo 'AskUserQuestion should be normalized'; result=1; } - [[ "$content" != *'request_user_input'* ]] || { echo 'request_user_input should be normalized'; result=1; } + # request_user_input SHOULD be present (native Codex tool, replaces AskUserQuestion) + [[ "$content" == *'request_user_input'* ]] || { echo 'request_user_input should be present after normalization'; result=1; } [[ "$content" != *'step 3 of max 3'* ]] || { echo 'step 3 of max 3 should be normalized'; result=1; } [[ "$content" != *'max depth 3'* ]] || { echo 'max depth 3 should be normalized'; result=1; } [[ "$content" == *'max_depth = 1'* || "$content" == *'root context'* ]] \