mirror of
https://github.com/gnekt/My-Brain-Is-Full-Crew.git
synced 2026-09-07 02:24:31 +00:00
feat: add Codex CLI as a first-class fourth platform
Rebuilt from scratch following Codex CLI's actual architecture (as outlined in the review on PR #26). Closes the integration gap with a proper build adapter, correct install paths, TOML agent files, and all the architectural differences documented and tested. What changed vs the previous attempt (PR #26): - Agents: build adapter generates .toml files (name/description/ developer_instructions) into dist/codex-cli/.codex/agents/ instead of copying .md files with sed transforms - Skills: installed to .agents/skills/ (correct Codex discovery path) instead of .codex/skills/ - Dispatcher: AGENTS.md uses a root-context orchestration header that works within agents.max_depth=1 constraints; named-agent routing replaced with embedded-instructions workaround for the known spawn_agents limitation (openai/codex#15250) - Tool compat: AskUserQuestion and request_user_input removed; all prompts adapted to Codex's actual tool set and approval/confirmation flow - Installer/updater: launchme.sh --platform codex-cli and updateme.sh with Codex auto-detection, creating the correct split layout (AGENTS.md + .codex/agents/ + .codex/config.toml + .agents/skills/) - Tests: new per-adapter test suite (tests/adapters/codex-cli/), install/update smoke (tests/scripts/codex-cli-install.test.sh), and a four-platform parity gate that proves Codex changes do not regress Claude Code, Gemini CLI, or OpenCode - Docs: new codex-cli.md guide, codex-migration.md for users switching from other platforms, and README/getting-started/examples updated for four-platform positioning - Bash harness: .gitattributes added to enforce LF on .sh files; harness LF-normalized so tests/run.sh works on Windows checkouts - .gitignore: .planning/ added (internal GSD workflow artifacts) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1458
tests/adapters/codex-cli/adapter.test.sh
Normal file
1458
tests/adapters/codex-cli/adapter.test.sh
Normal file
File diff suppressed because it is too large
Load Diff
79
tests/scripts/codex-cli-install.test.sh
Normal file
79
tests/scripts/codex-cli-install.test.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for Codex CLI install/update flows in scripts/launchme.sh and scripts/updateme.sh
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
test_codex_cli_scripts_define_explicit_platform_cases() {
|
||||
local result=0
|
||||
grep -q 'codex-cli)' "$ROOT/scripts/launchme.sh" \
|
||||
|| { echo 'launchme.sh should define a codex-cli) case'; result=1; }
|
||||
grep -q 'codex-cli)' "$ROOT/scripts/updateme.sh" \
|
||||
|| { echo 'updateme.sh should define a codex-cli) case'; result=1; }
|
||||
grep -q '\.codex/agents' "$ROOT/scripts/updateme.sh" \
|
||||
|| { echo 'updateme.sh should detect .codex/agents installs'; result=1; }
|
||||
return $result
|
||||
}
|
||||
|
||||
test_launchme_installs_codex_cli_layout() {
|
||||
local vault; vault="$(mktemp -d)"
|
||||
local log; log="$(mktemp)"
|
||||
|
||||
if ! bash "$ROOT/scripts/launchme.sh" --platform codex-cli --target "$vault" >"$log" 2>&1; then
|
||||
cat "$log"
|
||||
rm -rf "$vault" "$log"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local result=0
|
||||
[[ -f "$vault/AGENTS.md" ]] || { echo 'AGENTS.md missing after codex-cli install'; result=1; }
|
||||
[[ -f "$vault/.codex/config.toml" ]] || { echo '.codex/config.toml missing after codex-cli install'; result=1; }
|
||||
[[ -d "$vault/.codex/agents" ]] || { echo '.codex/agents missing after codex-cli install'; result=1; }
|
||||
[[ -f "$vault/.codex/agents/transcriber.toml" || -f "$vault/.codex/agents/architect.toml" ]] \
|
||||
|| { echo 'core codex agent TOML missing after install'; result=1; }
|
||||
[[ -f "$vault/.agents/skills/onboarding/SKILL.md" ]] || { echo '.agents/skills/onboarding/SKILL.md missing after install'; result=1; }
|
||||
|
||||
if [[ -f "$vault/AGENTS.md" ]]; then
|
||||
grep -q 'Codex CLI' "$vault/AGENTS.md" \
|
||||
|| { echo 'AGENTS.md should contain Codex CLI guidance'; result=1; }
|
||||
fi
|
||||
if [[ -f "$vault/.codex/config.toml" ]]; then
|
||||
grep -q '\[agents\]' "$vault/.codex/config.toml" \
|
||||
|| { echo '.codex/config.toml should contain [agents]'; result=1; }
|
||||
fi
|
||||
|
||||
rm -rf "$vault" "$log"
|
||||
return $result
|
||||
}
|
||||
|
||||
test_updateme_auto_detects_and_refreshes_codex_cli_install() {
|
||||
local vault; vault="$(mktemp -d)"
|
||||
local install_log; install_log="$(mktemp)"
|
||||
local update_log; update_log="$(mktemp)"
|
||||
|
||||
if ! bash "$ROOT/scripts/launchme.sh" --platform codex-cli --target "$vault" >"$install_log" 2>&1; then
|
||||
cat "$install_log"
|
||||
rm -rf "$vault" "$install_log" "$update_log"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf 'stale dispatcher\n' > "$vault/AGENTS.md"
|
||||
printf 'stale config\n' > "$vault/.codex/config.toml"
|
||||
|
||||
if ! printf 'c\n' | bash "$ROOT/scripts/updateme.sh" --target "$vault" >"$update_log" 2>&1; then
|
||||
cat "$update_log"
|
||||
rm -rf "$vault" "$install_log" "$update_log"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local result=0
|
||||
grep -q 'Detected platform: codex-cli' "$update_log" \
|
||||
|| { echo 'updateme.sh should auto-detect codex-cli'; result=1; }
|
||||
grep -q 'Codex CLI' "$vault/AGENTS.md" \
|
||||
|| { echo 'update should refresh AGENTS.md content'; result=1; }
|
||||
grep -q '\[agents\]' "$vault/.codex/config.toml" \
|
||||
|| { echo 'update should refresh .codex/config.toml content'; result=1; }
|
||||
[[ -d "$vault/.codex/agents" ]] || { echo '.codex/agents should remain after update'; result=1; }
|
||||
[[ -d "$vault/.agents/skills" ]] || { echo '.agents/skills should remain after update'; result=1; }
|
||||
|
||||
rm -rf "$vault" "$install_log" "$update_log"
|
||||
return $result
|
||||
}
|
||||
89
tests/scripts/platform-parity.test.sh
Normal file
89
tests/scripts/platform-parity.test.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# tests/scripts/platform-parity.test.sh — Four-platform build parity suite
|
||||
# =============================================================================
|
||||
# Proves that Codex CLI changes did not regress Claude Code, Gemini CLI,
|
||||
# OpenCode, or Codex CLI build artifacts. Runs as part of tests/run.sh.
|
||||
# =============================================================================
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_platform_build_matrix_produces_expected_dispatchers_and_roots
|
||||
#
|
||||
# Builds all four platforms and asserts each produces the correct dispatcher,
|
||||
# platform config directory, and at least one canonical agent file.
|
||||
# ---------------------------------------------------------------------------
|
||||
test_platform_build_matrix_produces_expected_dispatchers_and_roots() {
|
||||
local result=0
|
||||
|
||||
# Platform → expected artifacts map
|
||||
# Format: "<dispatcher>|<agent_file>|<config_file>"
|
||||
declare -A EXPECTED
|
||||
EXPECTED["claude-code"]="dist/claude-code/CLAUDE.md|dist/claude-code/.claude/agents/architect.md|dist/claude-code/.mcp.json"
|
||||
EXPECTED["gemini-cli"]="dist/gemini-cli/GEMINI.md|dist/gemini-cli/.gemini/agents/architect.md|dist/gemini-cli/.gemini/settings.json"
|
||||
EXPECTED["opencode"]="dist/opencode/AGENTS.md|dist/opencode/.opencode/agents/architect.md|dist/opencode/opencode.json"
|
||||
EXPECTED["codex-cli"]="dist/codex-cli/AGENTS.md|dist/codex-cli/.codex/agents/architect.toml|dist/codex-cli/.codex/config.toml"
|
||||
|
||||
for platform in claude-code gemini-cli opencode codex-cli; do
|
||||
if ! bash "$ROOT/scripts/build.sh" --platform "$platform" >/dev/null 2>&1; then
|
||||
echo "FAIL: build failed for platform: $platform"
|
||||
result=1
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r dispatcher agent_file config_file <<< "${EXPECTED[$platform]}"
|
||||
|
||||
[[ -f "$ROOT/$dispatcher" ]] \
|
||||
|| { echo "FAIL [$platform]: dispatcher missing: $dispatcher"; result=1; }
|
||||
[[ -f "$ROOT/$agent_file" ]] \
|
||||
|| { echo "FAIL [$platform]: agent file missing: $agent_file"; result=1; }
|
||||
[[ -f "$ROOT/$config_file" ]] \
|
||||
|| { echo "FAIL [$platform]: config file missing: $config_file"; result=1; }
|
||||
done
|
||||
|
||||
# Additional Codex-specific: skills directory
|
||||
[[ -f "$ROOT/dist/codex-cli/.agents/skills/onboarding/SKILL.md" ]] \
|
||||
|| { echo "FAIL [codex-cli]: .agents/skills/onboarding/SKILL.md missing"; result=1; }
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_claude_snapshot_regression_still_passes_after_codex_changes
|
||||
#
|
||||
# Runs the Claude Code snapshot regression test. Fails immediately if the
|
||||
# snapshot diff reports any drift — Codex changes must not touch Claude output.
|
||||
# ---------------------------------------------------------------------------
|
||||
test_claude_snapshot_regression_still_passes_after_codex_changes() {
|
||||
local log; log="$(mktemp)"
|
||||
if ! bash "$ROOT/tests/regression/run.sh" >"$log" 2>&1; then
|
||||
echo "FAIL: Claude snapshot regression reported drift:"
|
||||
cat "$log"
|
||||
rm -f "$log"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$log"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_codex_install_update_gate_remains_in_the_full_suite
|
||||
#
|
||||
# Asserts that the Codex CLI install/update test file still exists and
|
||||
# defines both required test function names. This gate ensures the parity
|
||||
# suite does not accidentally exclude Codex install regression coverage.
|
||||
# ---------------------------------------------------------------------------
|
||||
test_codex_install_update_gate_remains_in_the_full_suite() {
|
||||
local install_test="$ROOT/tests/scripts/codex-cli-install.test.sh"
|
||||
local result=0
|
||||
|
||||
[[ -f "$install_test" ]] \
|
||||
|| { echo "FAIL: tests/scripts/codex-cli-install.test.sh does not exist"; return 1; }
|
||||
|
||||
grep -q 'test_launchme_installs_codex_cli_layout' "$install_test" \
|
||||
|| { echo "FAIL: test_launchme_installs_codex_cli_layout not found in codex-cli-install.test.sh"; result=1; }
|
||||
grep -q 'test_updateme_auto_detects_and_refreshes_codex_cli_install' "$install_test" \
|
||||
|| { echo "FAIL: test_updateme_auto_detects_and_refreshes_codex_cli_install not found in codex-cli-install.test.sh"; result=1; }
|
||||
|
||||
return $result
|
||||
}
|
||||
164
tests/support/toml_smoke_check.py
Normal file
164
tests/support/toml_smoke_check.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
tests/support/toml_smoke_check.py — Validate TOML files generated by Phase 1 adapters.
|
||||
|
||||
Usage:
|
||||
python tests/support/toml_smoke_check.py path/to/file.toml
|
||||
|
||||
Exit 0 if the file parses under the subset of TOML features used in Phase 1:
|
||||
- Standard [table] headers
|
||||
- Quoted table names: [mcp_servers."Google Calendar"]
|
||||
- key = "string"
|
||||
- key = true / false
|
||||
- key = ["a", "b"] (array of strings)
|
||||
- env = { KEY = "VALUE" } (inline table)
|
||||
- Comments (# ...)
|
||||
- Blank lines
|
||||
- key = '''...''' (multiline literal string, any content)
|
||||
- key = \"\"\"...\"\"\" (multiline basic string, any content)
|
||||
|
||||
Exit 1 with a clear error message if parsing fails.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token patterns for Phase 1 TOML subset
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Blank line or comment
|
||||
RE_BLANK = re.compile(r'^\s*(#.*)?$')
|
||||
|
||||
# Standard table header: [agents] or [mcp_servers.Gmail]
|
||||
RE_TABLE = re.compile(r'^\[([A-Za-z0-9_.-]+)\]\s*(#.*)?$')
|
||||
|
||||
# TOML basic string content used inside quoted table keys for this test suite.
|
||||
# The generated keys only need literal characters plus escaped quote/backslash.
|
||||
# Keeping the subset narrow helps catch accidental raw backslashes such as
|
||||
# [mcp_servers."foo\bar"], which would change the key meaning.
|
||||
_TOML_ESCAPE = r'\\(?:["\\])'
|
||||
_TOML_SAFE_CHAR = r'[^"\\]'
|
||||
_TOML_QSTR = rf'(?:{_TOML_SAFE_CHAR}|{_TOML_ESCAPE})*'
|
||||
|
||||
# Quoted table header: [mcp_servers."Google Calendar"]
|
||||
RE_TABLE_QUOTED = re.compile(
|
||||
rf'^\[([A-Za-z0-9_.-]+"{_TOML_QSTR}")\]\s*(#.*)?$'
|
||||
)
|
||||
|
||||
# key = "string"
|
||||
RE_KEY_STRING = re.compile(r'^[A-Za-z0-9_.-]+\s*=\s*"([^"\\]|\\.)*"\s*(#.*)?$')
|
||||
|
||||
# key = true or key = false
|
||||
RE_KEY_BOOL = re.compile(r'^[A-Za-z0-9_.-]+\s*=\s*(true|false)\s*(#.*)?$')
|
||||
|
||||
# key = integer
|
||||
RE_KEY_INT = re.compile(r'^[A-Za-z0-9_.-]+\s*=\s*-?\d+\s*(#.*)?$')
|
||||
|
||||
# key = ["a", "b", ...] (array of strings — may span a single line only)
|
||||
RE_KEY_ARRAY = re.compile(r'^[A-Za-z0-9_.-]+\s*=\s*\[.*\]\s*(#.*)?$')
|
||||
|
||||
# key = { KEY = "VALUE", ... } (inline table — single line)
|
||||
RE_KEY_INLINE_TABLE = re.compile(r'^[A-Za-z0-9_.-]+\s*=\s*\{.*\}\s*(#.*)?$')
|
||||
|
||||
# key = ''' (start of multiline literal string)
|
||||
RE_MULTILINE_LIT_START = re.compile(r"^[A-Za-z0-9_.-]+\s*=\s*'''")
|
||||
|
||||
# key = """ (start of multiline basic string)
|
||||
RE_MULTILINE_BASIC_START = re.compile(r'^[A-Za-z0-9_.-]+\s*=\s*"""')
|
||||
|
||||
# Closing delimiter for multiline literal: line that is exactly ''' or ends with '''
|
||||
RE_MULTILINE_LIT_END = re.compile(r"'''$")
|
||||
|
||||
# Closing delimiter for multiline basic: line that is exactly """ or ends with """
|
||||
RE_MULTILINE_BASIC_END = re.compile(r'"""$')
|
||||
|
||||
# Collect all line-level patterns (order matters — most specific first)
|
||||
LINE_PATTERNS = [
|
||||
RE_BLANK,
|
||||
RE_TABLE_QUOTED,
|
||||
RE_TABLE,
|
||||
RE_KEY_STRING,
|
||||
RE_KEY_BOOL,
|
||||
RE_KEY_INT,
|
||||
RE_KEY_ARRAY,
|
||||
RE_KEY_INLINE_TABLE,
|
||||
RE_MULTILINE_LIT_START,
|
||||
RE_MULTILINE_BASIC_START,
|
||||
]
|
||||
|
||||
|
||||
def check_file(path: str) -> None:
|
||||
try:
|
||||
with open(path, encoding='utf-8') as fh:
|
||||
lines = fh.readlines()
|
||||
except FileNotFoundError:
|
||||
print(f"ERROR: File not found: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except OSError as exc:
|
||||
print(f"ERROR: Cannot read {path}: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
errors = []
|
||||
in_multiline_lit = False # inside ''' ... '''
|
||||
in_multiline_basic = False # inside """ ... """
|
||||
|
||||
for lineno, raw in enumerate(lines, start=1):
|
||||
line = raw.rstrip('\n')
|
||||
|
||||
# Inside a multiline literal string: any content is valid until '''
|
||||
if in_multiline_lit:
|
||||
if RE_MULTILINE_LIT_END.search(line):
|
||||
in_multiline_lit = False
|
||||
# All lines inside a multiline literal are valid — no error
|
||||
continue
|
||||
|
||||
# Inside a multiline basic string: any content is valid until """
|
||||
if in_multiline_basic:
|
||||
if RE_MULTILINE_BASIC_END.search(line):
|
||||
in_multiline_basic = False
|
||||
# All lines inside a multiline basic string are valid — no error
|
||||
continue
|
||||
|
||||
# Check if this line opens a multiline string
|
||||
if RE_MULTILINE_LIT_START.match(line):
|
||||
# Check if it's also closed on the same line (inline)
|
||||
rest = re.sub(r"^[A-Za-z0-9_.-]+\s*=\s*'''", "", line)
|
||||
if rest.endswith("'''"):
|
||||
pass # Single-line multiline literal (unusual but valid)
|
||||
else:
|
||||
in_multiline_lit = True
|
||||
continue
|
||||
|
||||
if RE_MULTILINE_BASIC_START.match(line):
|
||||
rest = re.sub(r'^[A-Za-z0-9_.-]+\s*=\s*"""', "", line)
|
||||
if rest.endswith('"""'):
|
||||
pass # Single-line multiline basic (unusual but valid)
|
||||
else:
|
||||
in_multiline_basic = True
|
||||
continue
|
||||
|
||||
matched = any(pat.match(line) for pat in LINE_PATTERNS)
|
||||
if not matched:
|
||||
errors.append(f" line {lineno}: unexpected syntax: {line!r}")
|
||||
|
||||
if in_multiline_lit:
|
||||
errors.append(" Unterminated multiline literal string (''' not closed)")
|
||||
if in_multiline_basic:
|
||||
errors.append(' Unterminated multiline basic string (""" not closed)')
|
||||
|
||||
if errors:
|
||||
print(f"TOML parse error in {path}:", file=sys.stderr)
|
||||
for err in errors:
|
||||
print(err, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"OK: {path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: {sys.argv[0]} <file.toml>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
check_file(sys.argv[1])
|
||||
Reference in New Issue
Block a user