mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-13 03:31:49 +08:00
fix codex skills running
This commit is contained in:
@@ -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 "<task>" [model] [working_dir]
|
||||
```
|
||||
|
||||
**备选方式**(直接执行或使用 Python):
|
||||
**Optional methods** (direct execution or via Python):
|
||||
```bash
|
||||
~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
# 或
|
||||
# or
|
||||
python3 ~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
```
|
||||
|
||||
恢复会话:
|
||||
Resume a session:
|
||||
```bash
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py resume <session_id> "<task>" [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 "<task>" ...` 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 "<task>" [model] [working_dir]
|
||||
- timeout: 7200000
|
||||
- description: <brief description of the task>
|
||||
```
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user