diff --git a/skills/codex/SKILL.md b/skills/codex/SKILL.md index e59eca2..273d2d1 100644 --- a/skills/codex/SKILL.md +++ b/skills/codex/SKILL.md @@ -18,19 +18,19 @@ Execute Codex CLI commands and parse structured JSON responses. Supports file re ## Usage -**推荐方式**(使用 uv run,自动管理 Python 环境): +**Mandatory**: Run every automated invocation through the Bash tool in the foreground with the command below, keeping the `timeout` parameter fixed at `7200000` milliseconds (do not change it or use any other entry point). ```bash uv run ~/.claude/skills/codex/scripts/codex.py "" [model] [working_dir] ``` -**备选方式**(直接执行或使用 Python): +**Optional methods** (direct execution or via Python): ```bash ~/.claude/skills/codex/scripts/codex.py "" [model] [working_dir] -# 或 +# or python3 ~/.claude/skills/codex/scripts/codex.py "" [model] [working_dir] ``` -恢复会话: +Resume a session: ```bash uv run ~/.claude/skills/codex/scripts/codex.py resume "" [model] [working_dir] ``` @@ -68,13 +68,14 @@ ERROR: Error message ### Invocation Pattern -When calling via Bash tool, always include the timeout parameter: +All automated executions may only invoke `uv run ~/.claude/skills/codex/scripts/codex.py "" ...` through the Bash tool in the foreground, and the `timeout` must remain fixed at `7200000` (non-negotiable): ``` Bash tool parameters: - command: uv run ~/.claude/skills/codex/scripts/codex.py "" [model] [working_dir] - timeout: 7200000 - description: ``` +Run every call in the foreground—never append `&` to background it—so logs and errors stay visible for timely interruption or diagnosis. Alternatives: ``` @@ -125,11 +126,23 @@ uv run ~/.claude/skills/codex/scripts/codex.py resume 019a7247-ac9d-71f3-89e2-a8 python3 ~/.claude/skills/codex/scripts/codex.py "your task here" ``` +### Large Task Protocol + +- For every large task, first produce a canonical task list that enumerates the Task ID, description, file/directory scope, dependencies, test commands, and the expected Codex Bash invocation. +- Tasks without dependencies should be executed concurrently via multiple foreground Bash calls (you can keep separate terminal windows) and each run must log start/end times plus any shared resource usage. +- Reuse context aggressively (such as @spec.md or prior analysis output), and after concurrent execution finishes, reconcile against the task list to report which items completed and which slipped. + +| ID | Description | Scope | Dependencies | Tests | Command | +| --- | --- | --- | --- | --- | --- | +| T1 | Review @spec.md to extract requirements | docs/, @spec.md | None | None | uv run ~/.claude/skills/codex/scripts/codex.py "analyze requirements @spec.md" | +| T2 | Implement the module and add test cases | src/module | T1 | npm test -- --runInBand | uv run ~/.claude/skills/codex/scripts/codex.py "implement and test @src/module" | + ## Notes - **Recommended**: Use `uv run` for automatic Python environment management (requires uv installed) - **Alternative**: Direct execution `./codex.py` (uses system Python via shebang) - Python implementation using standard library (zero dependencies) +- All automated runs must use the Bash tool with the fixed timeout to provide dual timeout protection and unified logging/exit semantics; any alternative approach is limited to manual foreground execution. - Cross-platform compatible (Windows/macOS/Linux) - PEP 723 compliant (inline script metadata) - Runs with `--dangerously-bypass-approvals-and-sandbox` for automation (new sessions only) diff --git a/skills/codex/scripts/codex.py b/skills/codex/scripts/codex.py index 4ff5e13..ada3c49 100755 --- a/skills/codex/scripts/codex.py +++ b/skills/codex/scripts/codex.py @@ -23,7 +23,6 @@ DEFAULT_MODEL = 'gpt-5.1-codex' DEFAULT_WORKDIR = '.' DEFAULT_TIMEOUT = 7200 # 2 hours in seconds FORCE_KILL_DELAY = 5 -STDIN_THRESHOLD = 800 # Auto-switch to stdin for prompts longer than 800 chars def log_error(message: str): @@ -36,6 +35,11 @@ def log_warn(message: str): sys.stderr.write(f"WARN: {message}\n") +def log_info(message: str): + """输出信息到 stderr""" + sys.stderr.write(f"INFO: {message}\n") + + def resolve_timeout() -> int: """解析超时配置(秒)""" raw = os.environ.get('CODEX_TIMEOUT', '') @@ -90,33 +94,55 @@ def parse_args(): } -def build_codex_args(params: dict, use_stdin: bool) -> list: +def read_piped_task() -> Optional[str]: + """ + 从 stdin 读取任务文本: + - 如果 stdin 是管道(非 tty)且存在内容,返回读取到的字符串 + - 否则返回 None + """ + stdin = sys.stdin + if stdin is None or stdin.isatty(): + return None + data = stdin.read() + return data if data else None + + +def should_stream_via_stdin(task_text: str, piped: bool) -> bool: + """ + 判定是否通过 stdin 传递任务: + - 有管道输入 + - 文本包含换行 + - 文本包含反斜杠 + - 文本长度 > 800 + """ + if piped: + return True + if '\n' in task_text: + return True + if '\\' in task_text: + return True + if len(task_text) > 800: + return True + return False + + +def build_codex_args(params: dict, target_arg: str) -> list: """ 构建 codex CLI 参数 Args: params: 参数字典 - use_stdin: 是否使用 stdin 模式(不在命令行参数中传递 task) + target_arg: 最终传递给 codex 的参数('-' 或具体 task 文本) """ if params['mode'] == 'resume': - if use_stdin: - return [ - 'codex', 'e', - '--skip-git-repo-check', - '--json', - 'resume', - params['session_id'], - '-' # 从 stdin 读取 - ] - else: - return [ - 'codex', 'e', - '--skip-git-repo-check', - '--json', - 'resume', - params['session_id'], - params['task'] - ] + return [ + 'codex', 'e', + '--skip-git-repo-check', + '--json', + 'resume', + params['session_id'], + target_arg + ] else: base_args = [ 'codex', 'e', @@ -124,50 +150,43 @@ def build_codex_args(params: dict, use_stdin: bool) -> list: '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '-C', params['workdir'], - '--json' + '--json', + target_arg ] - if use_stdin: - base_args.append('-') # 从 stdin 读取 - else: - base_args.append(params['task']) - return base_args -def main(): - params = parse_args() - timeout_sec = resolve_timeout() - - # **FIX: Auto-detect long inputs and enable stdin mode** - task_length = len(params['task']) - use_stdin = task_length > STDIN_THRESHOLD - - if use_stdin: - log_warn(f"Task length ({task_length} chars) exceeds threshold, using stdin mode to avoid shell escaping issues") - - codex_args = build_codex_args(params, use_stdin) - +def run_codex_process(codex_args, task_text: str, use_stdin: bool, timeout_sec: int): + """ + 启动 codex 子进程,处理 stdin / JSON 行输出和错误,成功时返回 (last_agent_message, thread_id)。 + 失败路径上负责日志和退出码。 + """ thread_id: Optional[str] = None last_agent_message: Optional[str] = None + process: Optional[subprocess.Popen] = None try: - # 启动 codex 子进程 + # 启动 codex 子进程(文本模式管道) process = subprocess.Popen( codex_args, - stdin=subprocess.PIPE if use_stdin else None, # **FIX: Enable stdin** + stdin=subprocess.PIPE if use_stdin else None, stdout=subprocess.PIPE, - stderr=sys.stderr, # 错误直接透传到 stderr + stderr=sys.stderr, text=True, - bufsize=1 # 行缓冲 + bufsize=1, ) - # **FIX: 如果使用 stdin 模式,写入任务到 stdin** - if use_stdin: - process.stdin.write(params['task']) + # 如果使用 stdin 模式,写入任务到 stdin 并关闭 + if use_stdin and process.stdin is not None: + process.stdin.write(task_text) process.stdin.close() # 逐行解析 JSON 输出 + if process.stdout is None: + log_error('Codex stdout pipe not available') + sys.exit(1) + for line in process.stdout: line = line.strip() if not line: @@ -190,33 +209,26 @@ def main(): except json.JSONDecodeError: log_warn(f"Failed to parse line: {line}") - # 等待进程结束 + # 等待进程结束并检查退出码 returncode = process.wait(timeout=timeout_sec) - - if returncode == 0: - if last_agent_message: - # 输出 agent_message - sys.stdout.write(f"{last_agent_message}\n") - - # 输出 session_id(如果存在) - if thread_id: - sys.stdout.write(f"\n---\nSESSION_ID: {thread_id}\n") - - sys.exit(0) - else: - log_error('Codex completed without agent_message output') - sys.exit(1) - else: + if returncode != 0: log_error(f'Codex exited with status {returncode}') sys.exit(returncode) + if not last_agent_message: + log_error('Codex completed without agent_message output') + sys.exit(1) + + return last_agent_message, thread_id + except subprocess.TimeoutExpired: log_error('Codex execution timeout') - process.kill() - try: - process.wait(timeout=FORCE_KILL_DELAY) - except subprocess.TimeoutExpired: - pass + if process is not None: + process.kill() + try: + process.wait(timeout=FORCE_KILL_DELAY) + except subprocess.TimeoutExpired: + pass sys.exit(124) except FileNotFoundError: @@ -224,13 +236,61 @@ def main(): sys.exit(127) except KeyboardInterrupt: - process.terminate() - try: - process.wait(timeout=FORCE_KILL_DELAY) - except subprocess.TimeoutExpired: - process.kill() + log_error("Codex interrupted by user") + if process is not None: + process.terminate() + try: + process.wait(timeout=FORCE_KILL_DELAY) + except subprocess.TimeoutExpired: + process.kill() sys.exit(130) +def main(): + params = parse_args() + timeout_sec = resolve_timeout() + + piped_task = read_piped_task() + piped = piped_task is not None + task_text = piped_task if piped else params['task'] + + use_stdin = should_stream_via_stdin(task_text, piped) + + if use_stdin: + reasons = [] + if piped: + reasons.append('piped input') + if '\n' in task_text: + reasons.append('newline') + if '\\' in task_text: + reasons.append('backslash') + if len(task_text) > 800: + reasons.append('length>800') + + if reasons: + log_warn(f"Using stdin mode for task due to: {', '.join(reasons)}") + + target_arg = '-' if use_stdin else params['task'] + codex_args = build_codex_args(params, target_arg) + + log_info('codex running...') + + last_agent_message, thread_id = run_codex_process( + codex_args=codex_args, + task_text=task_text, + use_stdin=use_stdin, + timeout_sec=timeout_sec, + ) + + # 输出 agent_message + sys.stdout.write(f"{last_agent_message}\n") + + # 输出 session_id(如果存在) + if thread_id: + sys.stdout.write(f"\n---\nSESSION_ID: {thread_id}\n") + + sys.exit(0) + + if __name__ == '__main__': main()