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:
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