@trendai-crem/claude-skills 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,28 +10,51 @@ npx @trendai-crem/claude-skills
10
10
 
11
11
  That's it. The command installs:
12
12
 
13
- 1. **External skills** — [superpowers](https://github.com/obra/superpowers))
13
+ 1. **External skills** — [superpowers](https://github.com/obra/superpowers) (brainstorming, debugging, TDD, and more)
14
14
  2. **Team skills** — custom skills maintained in this repo (override same-named externals)
15
15
 
16
16
  ## Update
17
17
 
18
- Re-run the same command to update all skills to the latest version:
18
+ Re-run the same command to get the latest version. If an update is available, you'll be notified automatically.
19
19
 
20
20
  ```bash
21
- npx @trendai-crem/claude-skills
21
+ npx @trendai-crem/claude-skills@latest
22
22
  ```
23
23
 
24
24
  ## Team Skills
25
25
 
26
- | Skill | Description |
27
- |-------|-------------|
28
- | _(coming soon)_ | Add your team skills to `skills/` |
26
+ ### Engineering Quality
27
+
28
+ | Skill | Trigger | Description |
29
+ |-------|---------|-------------|
30
+ | **code-review** | "review my code", "code review" | Multi-perspective review — 5 reviewers + Codex baseline, enforces TM RDSec policy and Secure Coding Dojo checkpoints |
31
+ | **reviewing-prs** | "review this PR", "review pull request" | Structured PR review with 3 independent sub-agents covering correctness, security, and requirements |
32
+
33
+ ### Atlassian
34
+
35
+ | Skill | Trigger | Description |
36
+ |-------|---------|-------------|
37
+ | **atlassian-tools** | Any Confluence/Jira URL or mention | Confluence wiki and Jira issue management — create, update, search pages and tickets |
38
+ | **wiki-generation** | "create a wiki page", "generate Confluence docs" | Generate Confluence documentation with proper ADF format and Mermaid diagram support |
39
+
40
+ ### Google Style Guides
41
+
42
+ | Skill | Trigger | Description |
43
+ |-------|---------|-------------|
44
+ | **java** | "java style", "java coding standards" | Google Java Style Guide reference |
45
+ | **python** | "python style", "python coding standards" | Google Python Style Guide reference |
46
+ | **go** | "go style", "go coding standards" | Google Go Style Guide reference |
47
+ | **typescript** | "typescript style", "typescript coding standards" | Google TypeScript Style Guide reference |
48
+ | **javascript** | "javascript style", "javascript coding standards" | Google JavaScript Style Guide reference |
49
+ | **shell** | "shell style", "bash coding standards" | Google Shell Style Guide reference |
50
+ | **cpp** | "c++ style", "cpp coding standards" | Google C++ Style Guide reference |
29
51
 
30
52
  ## For Maintainers
31
53
 
32
54
  ### Adding a team skill
33
55
 
34
- 1. Create `skills/<skill-name>/SKILL.md` with frontmatter:
56
+ 1. Create a branch: `git checkout -b feat/add-<skill-name>`
57
+ 2. Add `skills/<skill-name>/SKILL.md` with frontmatter:
35
58
  ```markdown
36
59
  ---
37
60
  name: skill-name
@@ -40,21 +63,23 @@ npx @trendai-crem/claude-skills
40
63
 
41
64
  # Skill content here
42
65
  ```
43
- 2. Commit and push
44
- 3. Bump the npm version: `npm version patch && npm publish`
66
+ 3. Commit, push, and open a PR to `main`
67
+ 4. Bump version in PR: `npm version minor`
68
+ 5. Merging to `main` triggers automated publish via CI
45
69
 
46
- ### Updating the package version
70
+ ### Version bumping
47
71
 
48
72
  ```bash
49
- npm version patch # bug fixes
73
+ npm version patch # bug fixes / skill content updates
50
74
  npm version minor # new skills added
51
75
  npm version major # breaking changes
52
- npm publish
53
76
  ```
54
77
 
78
+ Push to `main` — CI handles the publish automatically.
79
+
55
80
  ### External skill sources
56
81
 
57
- Configured in `cli.js` under `EXTERNAL_SOURCES`. To add or remove external sources, edit that array and publish a new version.
82
+ Configured in `cli.js` under `EXTERNAL_SOURCES`. Edit the array and bump the version to add or remove external sources.
58
83
 
59
84
  ## Requirements
60
85
 
package/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
- import { readFileSync } from 'fs';
4
+ import { readFileSync, readdirSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { dirname, join } from 'path';
7
7
 
@@ -29,12 +29,21 @@ const externalResults = EXTERNAL_SOURCES.map(({ repo, flags, label }) =>
29
29
  );
30
30
 
31
31
  // 2. Team skills — required, overrides same-named externals
32
+ const teamSkills = readdirSync(join(__dir, 'skills'), { withFileTypes: true })
33
+ .filter(e => e.isDirectory())
34
+ .map(e => e.name)
35
+ .sort();
36
+
32
37
  const teamOk = run(['skills', 'add', __dir, '--all', '-g', '-y'], 'team skills');
33
38
 
34
39
  // Summary
35
40
  console.log('\nResults:');
36
- [...externalResults, { label: 'team skills', ok: teamOk }]
37
- .forEach(({ label, ok }) => console.log(` ${ok ? '✓' : '✗'} ${label}`));
41
+ externalResults.forEach(({ label, ok }) => console.log(` ${ok ? '' : '✗'} ${label}`));
42
+ if (teamOk) {
43
+ teamSkills.forEach(name => console.log(` ✓ ${name}`));
44
+ } else {
45
+ console.log(` ✗ team skills`);
46
+ }
38
47
 
39
48
  if (!teamOk) {
40
49
  console.error('\nFATAL: Team skills installation failed.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendai-crem/claude-skills",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code skills installer for the trendai-crem team",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: codex
3
+ description: Delegate coding tasks to Codex CLI for execution. Only invoke this skill when the user explicitly asks to use Codex — e.g., "用 codex 来做", "让 codex 执行", "ask codex to...", "codex 帮我写". Do not proactively delegate to Codex for general coding requests the user didn't specifically ask Codex to handle. Codex is an autonomous coding agent with the same tools as Claude (file read/write, grep, bash) — it explores the codebase and implements changes on its own. Claude's role is to understand the problem clearly and frame it well for Codex to execute.
4
+ ---
5
+
6
+ ## Language
7
+
8
+ Respond in the same language the user is using. If the user writes in Chinese, respond in Chinese. If in English, respond in English.
9
+
10
+ ## Prerequisites check
11
+
12
+ **Before anything else**, verify Codex CLI is installed and configured:
13
+
14
+ ```bash
15
+ command -v codex >/dev/null 2>&1 && echo "available" || echo "not found"
16
+ ```
17
+
18
+ If Codex is **not found**, stop immediately and inform the user:
19
+ > "Codex CLI is not installed or not in PATH. Please install it first: `npm install -g @openai/codex`"
20
+
21
+ Do NOT attempt to proceed without a working Codex installation.
22
+
23
+ ## Critical rules
24
+
25
+ - Use the bundled shell script rather than calling `codex` CLI directly — the script handles output capture, session tracking, and real-time progress streaming correctly.
26
+ - Run the script once per task. If it succeeds (exit code 0), read the output file and proceed. Don't re-run just because the output seems short — Codex often makes changes quietly without narrating every step.
27
+ - Quote file paths containing `[`, `]`, spaces, or special characters (e.g. `--file "src/app/[locale]/page.tsx"`). Without quotes, zsh treats `[...]` as a glob pattern and fails with "no matches found".
28
+ - **Keep the task prompt to the goal and constraints, not the implementation steps.** Aim for under ~500 words. Codex has the same tools as Claude and will explore the codebase itself — spelling out every file to change or every step tends to constrain it rather than help.
29
+ - **Don't paste file contents into the prompt.** Use `--file` to point Codex to key files — it reads them directly at their current version. Pasting contents wastes tokens and risks passing stale code.
30
+ - **Don't mention this skill or its configuration in the prompt.** Codex doesn't need to know about it.
31
+
32
+ ## How to call the script
33
+
34
+ ### Linux/macOS (bash)
35
+
36
+ The script path is:
37
+
38
+ ```
39
+ ~/.claude/skills/codex/scripts/ask_codex.sh
40
+ ```
41
+
42
+ Minimal invocation (with xhigh reasoning by default):
43
+
44
+ ```bash
45
+ ~/.claude/skills/codex/scripts/ask_codex.sh "Your request in natural language" \
46
+ --reasoning xhigh
47
+ ```
48
+
49
+ With file context:
50
+
51
+ ```bash
52
+ ~/.claude/skills/codex/scripts/ask_codex.sh "Refactor these components to use the new API" \
53
+ --reasoning xhigh \
54
+ --file src/components/UserList.tsx \
55
+ --file src/components/UserDetail.tsx
56
+ ```
57
+
58
+ Multi-turn conversation (continue a previous session):
59
+
60
+ ```bash
61
+ ~/.claude/skills/codex/scripts/ask_codex.sh "Also add retry logic with exponential backoff" \
62
+ --reasoning xhigh \
63
+ --session <session_id from previous run>
64
+ ```
65
+
66
+ ### Windows (PowerShell)
67
+
68
+ The script path is:
69
+
70
+ ```
71
+ ~/.claude/skills/codex/scripts/ask_codex.ps1
72
+ ```
73
+
74
+ Minimal invocation:
75
+
76
+ ```powershell
77
+ & ~/.claude/skills/codex/scripts/ask_codex.ps1 "Your request in natural language" `
78
+ --reasoning xhigh
79
+ ```
80
+
81
+ ### Output format
82
+
83
+ The script prints on success:
84
+
85
+ ```
86
+ session_id=<thread_id>
87
+ output_path=<path to markdown file>
88
+ ```
89
+
90
+ Read the file at `output_path` to get Codex's response. Save `session_id` if you plan follow-up calls.
91
+
92
+ ## Workflow
93
+
94
+ ### Step 1 — Understand the problem
95
+
96
+ Read the key files to grasp what's broken or needed. Focus on being able to describe the problem and goal clearly — you don't need to design the full solution. Codex will explore the codebase itself.
97
+
98
+ ### Step 2 — Gather coding conventions
99
+
100
+ Before calling the script, collect the team's coding conventions and style guides to inject as context. Check in this order:
101
+
102
+ **a) Project-level conventions:**
103
+
104
+ ```bash
105
+ # Check for CLAUDE.md or project rules
106
+ ls ~/.claude/CLAUDE.md 2>/dev/null
107
+ ls .claude/CLAUDE.md 2>/dev/null
108
+ ls .claude/rules/ 2>/dev/null
109
+ ```
110
+
111
+ If found, include relevant files via `--file`:
112
+
113
+ ```bash
114
+ --file ~/.claude/CLAUDE.md
115
+ ```
116
+
117
+ **b) Language-specific Google Style Guides:**
118
+
119
+ Check if style guide skills are installed for the language being used:
120
+
121
+ ```bash
122
+ ls ~/.claude/skills/java/SKILL.md 2>/dev/null # Java
123
+ ls ~/.claude/skills/python/SKILL.md 2>/dev/null # Python
124
+ ls ~/.claude/skills/go/SKILL.md 2>/dev/null # Go
125
+ ls ~/.claude/skills/typescript/SKILL.md 2>/dev/null
126
+ ls ~/.claude/skills/javascript/SKILL.md 2>/dev/null
127
+ ls ~/.claude/skills/shell/SKILL.md 2>/dev/null
128
+ ls ~/.claude/skills/cpp/SKILL.md 2>/dev/null
129
+ ```
130
+
131
+ If the relevant style guide exists, include it:
132
+
133
+ ```bash
134
+ --file ~/.claude/skills/java/SKILL.md
135
+ ```
136
+
137
+ **c) No conventions found:**
138
+
139
+ If neither project rules nor style guides are found, skip this step entirely — let Codex discover conventions from the codebase on its own. Do not block or warn the user.
140
+
141
+ ### Step 3 — Build and run the prompt
142
+
143
+ Construct the task description embedding the relevant conventions context, then run:
144
+
145
+ ```bash
146
+ ~/.claude/skills/codex/scripts/ask_codex.sh "<task description with convention constraints>" \
147
+ --reasoning xhigh \
148
+ --file <entry-point files> \
149
+ --file <convention files if found>
150
+ ```
151
+
152
+ Pass 1–4 entry-point files as starting hints. Codex will discover related files on its own.
153
+
154
+ For discussion or analysis without changes, use `--read-only`.
155
+
156
+ ### Step 4 — Read and review
157
+
158
+ Read the output file, then review the changes in the workspace.
159
+
160
+ For multi-step projects, use `--session <id>` to continue with full conversation history. For independent parallel tasks, use the Task tool with `run_in_background: true`.
161
+
162
+ ## Failure handling
163
+
164
+ - **`script: tcgetattr/ioctl: Operation not supported on socket`** (exit code 1): the script detects this automatically and falls back to direct execution. Update to the latest version if you still see this error.
165
+ - **Exit code 137**: the task was interrupted (user cancel or OOM). Not a Codex bug — retry or break the task into smaller pieces.
166
+ - **`ERROR codex_core::codex: failed to load skill ...`** in stderr: one of Codex's own installed skills has a broken YAML file. This warning is harmless — ignore it.
167
+ - **`(no response from codex)`** in the output file: Codex ran but produced no readable output. Check stderr for clues; the task may have hit a sandbox restriction.
168
+
169
+ ## Options
170
+
171
+ - `--workspace <path>` — Target workspace directory (defaults to current directory).
172
+ - `--file <path>` — Point Codex to key entry-point files (repeatable, workspace-relative or absolute).
173
+ - `--session <id>` — Resume a previous session for multi-turn conversation.
174
+ - `--model <name>` — Override model (default: uses Codex config).
175
+ - `--reasoning <level>` — Reasoning effort: `low`, `medium`, `high`, `xhigh` (default: **`xhigh`**).
176
+ - `--sandbox <mode>` — Override sandbox policy (default: workspace-write via full-auto).
177
+ - `--read-only` — Read-only mode for pure discussion/analysis, no file changes.
178
+
179
+ ## Resume mode limitations
180
+
181
+ When using `--session` to resume a previous conversation, note these limitations:
182
+
183
+ - **Must run in a git repository** — The `codex exec resume` command requires a git-trusted directory. It does not support `--skip-git-repo-check`.
184
+ - **Limited options** — Resume mode only supports `-c/--config` and `--last`. The following options are **not supported** in resume mode:
185
+ - `--sandbox`
186
+ - `--full-auto`
187
+ - `--read-only`
188
+ - `--model`
189
+ - `--workspace` (resumes in the original session's context)
190
+ - **Text output only** — Resume mode returns plain text instead of JSON-structured output.
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env powershell
2
+ # Windows PowerShell 5.1+ compatible script
3
+ [CmdletBinding()]
4
+ param(
5
+ [Parameter(Position = 0)]
6
+ [string]$Task,
7
+
8
+ [Alias('t')]
9
+ [string]$TaskText,
10
+
11
+ [Alias('w')]
12
+ [string]$Workspace = (Get-Location).Path,
13
+
14
+ [Alias('f')]
15
+ [string[]]$File,
16
+
17
+ [string]$Session,
18
+
19
+ [string]$Model,
20
+
21
+ [ValidateSet('low', 'medium', 'high')]
22
+ [string]$Reasoning = 'medium',
23
+
24
+ [string]$Sandbox,
25
+
26
+ [switch]$ReadOnly,
27
+
28
+ [switch]$FullAuto,
29
+
30
+ [Alias('o')]
31
+ [string]$Output,
32
+
33
+ [switch]$Help
34
+ )
35
+
36
+ $ErrorActionPreference = 'Stop'
37
+
38
+ function Show-Usage {
39
+ @'
40
+ Usage:
41
+ ask_codex.ps1 <task> [options]
42
+ ask_codex.ps1 -Task <task> [options]
43
+
44
+ Task input:
45
+ <task> First positional argument is the task text
46
+ -Task, -t <text> Alias for positional task
47
+
48
+ File context (optional, repeatable):
49
+ -File, -f <path> Priority file path
50
+
51
+ Multi-turn:
52
+ -Session <id> Resume a previous session (thread_id from prior run)
53
+
54
+ Options:
55
+ -Workspace, -w <path> Workspace directory (default: current directory)
56
+ -Model <name> Model override
57
+ -Reasoning <level> Reasoning effort: low, medium, high (default: medium)
58
+ -Sandbox <mode> Sandbox mode override
59
+ -ReadOnly Read-only sandbox (no file changes)
60
+ -FullAuto Full-auto mode (default)
61
+ -Output, -o <path> Output file path
62
+ -Help Show this help
63
+
64
+ Output (on success):
65
+ session_id=<thread_id> Use with -Session for follow-up calls
66
+ output_path=<file> Path to response markdown
67
+
68
+ Examples:
69
+ # New task (positional)
70
+ ask_codex.ps1 "Add error handling to api.ts" -f src/api.ts
71
+
72
+ # With explicit workspace
73
+ ask_codex.ps1 "Fix the bug" -w C:\other\repo
74
+
75
+ # Continue conversation
76
+ ask_codex.ps1 "Also add retry logic" -Session <id>
77
+ '@
78
+ }
79
+
80
+ function Test-Command {
81
+ param([string]$Name)
82
+ if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
83
+ Write-Error "[ERROR] Missing required command: $Name"
84
+ exit 1
85
+ }
86
+ }
87
+
88
+ function Trim-Whitespace {
89
+ param([string]$Text)
90
+ if ([string]::IsNullOrEmpty($Text)) { return '' }
91
+ return $Text.Trim() -replace '\s+', ' '
92
+ }
93
+
94
+ function Resolve-FileRef {
95
+ param(
96
+ [string]$Workspace,
97
+ [string]$RawPath
98
+ )
99
+
100
+ $cleaned = Trim-Whitespace $RawPath
101
+ if ([string]::IsNullOrWhiteSpace($cleaned)) { return '' }
102
+
103
+ # Remove line number suffixes (#L123 or :123-456)
104
+ $cleaned = $cleaned -replace '#L\d+$', ''
105
+ $cleaned = $cleaned -replace ':\d+(-\d+)?$', ''
106
+
107
+ # Make absolute if relative
108
+ if (-not [System.IO.Path]::IsPathRooted($cleaned)) {
109
+ $cleaned = Join-Path $Workspace $cleaned
110
+ }
111
+
112
+ # Normalize path
113
+ if (Test-Path $cleaned) {
114
+ return (Resolve-Path $cleaned -ErrorAction SilentlyContinue).Path
115
+ }
116
+ return $cleaned
117
+ }
118
+
119
+ function Write-File-NoBOM {
120
+ param([string]$Path, [string]$Content)
121
+ $utf8NoBom = New-Object System.Text.UTF8Encoding $false
122
+ [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
123
+ }
124
+
125
+ # Show help if requested
126
+ if ($Help) {
127
+ Show-Usage
128
+ exit 0
129
+ }
130
+
131
+ # Check required commands
132
+ Test-Command 'codex'
133
+ Test-Command 'jq'
134
+
135
+ # Resolve task text from either positional or named parameter
136
+ if ([string]::IsNullOrEmpty($Task) -and -not [string]::IsNullOrEmpty($TaskText)) {
137
+ $Task = $TaskText
138
+ }
139
+
140
+ # Validate workspace
141
+ if (-not (Test-Path $Workspace -PathType Container)) {
142
+ Write-Error "[ERROR] Workspace does not exist: $Workspace"
143
+ exit 1
144
+ }
145
+ $Workspace = (Resolve-Path $Workspace).Path
146
+
147
+ # Validate task
148
+ $Task = Trim-Whitespace $Task
149
+ if ([string]::IsNullOrEmpty($Task)) {
150
+ Write-Error "[ERROR] Request text is empty. Pass a positional arg or -Task."
151
+ exit 1
152
+ }
153
+
154
+ # Prepare output path
155
+ if ([string]::IsNullOrEmpty($Output)) {
156
+ $timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss')
157
+ $skillDir = Split-Path $PSScriptRoot -Parent
158
+ $runtimeDir = Join-Path $skillDir '.runtime'
159
+ if (-not (Test-Path $runtimeDir)) {
160
+ New-Item -ItemType Directory -Path $runtimeDir -Force | Out-Null
161
+ }
162
+ $Output = Join-Path $runtimeDir "$timestamp.md"
163
+ }
164
+
165
+ # Build file context block
166
+ $fileBlock = ''
167
+ if ($File -and $File.Count -gt 0) {
168
+ $fileBlock = "`nPriority files (read these first before making changes):"
169
+ foreach ($ref in $File) {
170
+ $resolved = Resolve-FileRef -Workspace $Workspace -RawPath $ref
171
+ if (-not [string]::IsNullOrEmpty($resolved)) {
172
+ $existsTag = if (Test-Path $resolved) { 'exists' } else { 'missing' }
173
+ $fileBlock += "`n- $resolved ($existsTag)"
174
+ }
175
+ }
176
+ }
177
+
178
+ # Build prompt
179
+ $prompt = $Task
180
+ if (-not [string]::IsNullOrEmpty($fileBlock)) {
181
+ $prompt += $fileBlock
182
+ }
183
+
184
+ # Build codex command
185
+ $codexArgs = @()
186
+
187
+ if (-not [string]::IsNullOrEmpty($Session)) {
188
+ # Resume mode: continue a previous session
189
+ # Note: resume only supports -c/--config and --last flags (no --json, --sandbox, etc.)
190
+ $codexArgs = @('exec', 'resume', '-c', "model_reasoning_effort=`"$Reasoning`"", '-c', 'skip_git_repo_check=true')
191
+ $codexArgs += $Session
192
+ } else {
193
+ # New session
194
+ $codexArgs = @('exec', '--cd', $Workspace, '--skip-git-repo-check', '--json', '-c', "model_reasoning_effort=`"$Reasoning`"")
195
+ if ($ReadOnly) {
196
+ $codexArgs += '--sandbox', 'read-only'
197
+ } elseif (-not [string]::IsNullOrEmpty($Sandbox)) {
198
+ $codexArgs += '--sandbox', $Sandbox
199
+ } elseif ($FullAuto) {
200
+ $codexArgs += '--full-auto'
201
+ }
202
+ if (-not [string]::IsNullOrEmpty($Model)) {
203
+ $codexArgs += '-m', $Model
204
+ }
205
+ }
206
+
207
+ # Create temp files
208
+ $tempDir = [System.IO.Path]::GetTempPath()
209
+ $guid = [guid]::NewGuid().ToString()
210
+ $stderrFile = Join-Path $tempDir "codex_stderr_$guid.txt"
211
+ $jsonFile = Join-Path $tempDir "codex_json_$guid.txt"
212
+ $promptFile = Join-Path $tempDir "codex_prompt_$guid.txt"
213
+
214
+ # Cleanup function
215
+ $cleanupScript = {
216
+ Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
217
+ Remove-Item -Path $jsonFile -Force -ErrorAction SilentlyContinue
218
+ Remove-Item -Path $promptFile -Force -ErrorAction SilentlyContinue
219
+ }
220
+
221
+ try {
222
+ # Write prompt to temp file (UTF-8 without BOM)
223
+ Write-File-NoBOM -Path $promptFile -Content $prompt
224
+
225
+ # Initialize json file
226
+ Write-File-NoBOM -Path $jsonFile -Content ''
227
+
228
+ # Setup process with async reading for real-time output
229
+ # On Windows, codex is installed as a .ps1 script, so we need to use cmd.exe or pwsh to run it
230
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
231
+ if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) {
232
+ # Use cmd.exe to run codex (works with .cmd/.ps1 wrappers)
233
+ $psi.FileName = 'cmd.exe'
234
+ $psi.Arguments = '/c codex ' + ($codexArgs -join ' ')
235
+ } else {
236
+ $psi.FileName = 'codex'
237
+ $psi.Arguments = $codexArgs -join ' '
238
+ }
239
+ $psi.WorkingDirectory = $Workspace
240
+ $psi.UseShellExecute = $false
241
+ $psi.RedirectStandardInput = $true
242
+ $psi.RedirectStandardOutput = $true
243
+ $psi.RedirectStandardError = $true
244
+ $psi.CreateNoWindow = $true
245
+ $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
246
+ $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
247
+
248
+ $process = New-Object System.Diagnostics.Process
249
+ $process.StartInfo = $psi
250
+
251
+ # StringBuilder for collecting output
252
+ $jsonOutput = New-Object System.Text.StringBuilder
253
+ $stderrOutput = New-Object System.Text.StringBuilder
254
+ $outputLock = New-Object Object
255
+
256
+ # Event handler script blocks
257
+ $jsonOutputRef = $jsonOutput
258
+ $stderrOutputRef = $stderrOutput
259
+
260
+ # Register event handlers for async reading
261
+ $isResumeMode = -not [string]::IsNullOrEmpty($Session)
262
+ $textOutput = New-Object System.Text.StringBuilder
263
+
264
+ $stdOutAction = {
265
+ param([object]$sender, [System.Diagnostics.DataReceivedEventArgs]$e)
266
+ if ($e.Data) {
267
+ $line = $e.Data
268
+ # Strip terminal artifacts
269
+ $line = $line -replace "`r", ''
270
+ $line = $line -replace [char]4, ''
271
+
272
+ if (-not [string]::IsNullOrEmpty($line)) {
273
+ if ($line.StartsWith('{')) {
274
+ # JSON line (new session mode)
275
+ [System.Threading.Monitor]::Enter($Event.MessageData)
276
+ try {
277
+ $Event.MessageData.AppendLine($line) | Out-Null
278
+ } finally {
279
+ [System.Threading.Monitor]::Exit($Event.MessageData)
280
+ }
281
+
282
+ # Print progress for relevant events
283
+ if ($line -match '"item\.started"' -or $line -match '"item\.completed"') {
284
+ if ($line -match '"item\.started"' -and $line -match '"command_execution"') {
285
+ try {
286
+ $json = $line | ConvertFrom-Json -ErrorAction SilentlyContinue
287
+ $cmd = $json.item.command
288
+ if ($cmd) {
289
+ $cmd = $cmd -replace '^/bin/(zsh|bash) (-lc|-c) ', ''
290
+ if ($cmd.Length -gt 100) { $cmd = $cmd.Substring(0, 100) }
291
+ Write-Host "[codex] > $cmd" -ForegroundColor Gray
292
+ }
293
+ } catch {}
294
+ }
295
+ if ($line -match '"item\.completed"' -and $line -match '"agent_message"') {
296
+ try {
297
+ $json = $line | ConvertFrom-Json -ErrorAction SilentlyContinue
298
+ $text = $json.item.text
299
+ if ($text) {
300
+ $preview = $text.Split("`n")[0]
301
+ if ($preview.Length -gt 120) { $preview = $preview.Substring(0, 120) }
302
+ Write-Host "[codex] $preview" -ForegroundColor Gray
303
+ }
304
+ } catch {}
305
+ }
306
+ }
307
+ } else {
308
+ # Plain text line (resume mode)
309
+ [System.Threading.Monitor]::Enter($Event.MessageData)
310
+ try {
311
+ $Event.MessageData.AppendLine($line) | Out-Null
312
+ } finally {
313
+ [System.Threading.Monitor]::Exit($Event.MessageData)
314
+ }
315
+ # Show progress for text output
316
+ $preview = $line
317
+ if ($preview.Length -gt 120) { $preview = $preview.Substring(0, 120) }
318
+ Write-Host "[codex] $preview" -ForegroundColor Gray
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ $stdErrAction = {
325
+ param([object]$sender, [System.Diagnostics.DataReceivedEventArgs]$e)
326
+ if ($e.Data) {
327
+ [System.Threading.Monitor]::Enter($Event.MessageData)
328
+ try {
329
+ $Event.MessageData.AppendLine($e.Data) | Out-Null
330
+ } finally {
331
+ [System.Threading.Monitor]::Exit($Event.MessageData)
332
+ }
333
+ Write-Host $e.Data -ForegroundColor Yellow
334
+ }
335
+ }
336
+
337
+ # Register events - use textOutput for resume mode, jsonOutput for new session
338
+ $outputData = if ($isResumeMode) { $textOutput } else { $jsonOutput }
339
+ $stdOutEvent = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -Action $stdOutAction -MessageData $outputData
340
+ $stdErrEvent = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action $stdErrAction -MessageData $stderrOutput
341
+
342
+ try {
343
+ # Start process
344
+ $process.Start() | Out-Null
345
+
346
+ # Begin async reading
347
+ $process.BeginOutputReadLine()
348
+ $process.BeginErrorReadLine()
349
+
350
+ # Write prompt to stdin
351
+ $process.StandardInput.Write($prompt)
352
+ $process.StandardInput.Close()
353
+
354
+ # Wait for process to exit
355
+ $process.WaitForExit()
356
+ $exitCode = $process.ExitCode
357
+
358
+ } finally {
359
+ # Unregister events
360
+ Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction SilentlyContinue
361
+ Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction SilentlyContinue
362
+ $process.Dispose()
363
+ }
364
+
365
+ # Process output based on mode
366
+ $threadId = $null
367
+ $outputContent = @()
368
+
369
+ if ($isResumeMode) {
370
+ # Resume mode: plain text output
371
+ $textContent = $textOutput.ToString().Trim()
372
+
373
+ # Check for errors
374
+ $stderrText = $stderrOutput.ToString()
375
+ $hasValidOutput = -not [string]::IsNullOrWhiteSpace($textContent)
376
+
377
+ if ($stderrText -match '\[ERROR\]' -and -not $hasValidOutput) {
378
+ Write-Error "[ERROR] Codex command failed"
379
+ Write-Error $stderrText
380
+ exit 1
381
+ }
382
+
383
+ if ($exitCode -ne 0 -and -not $hasValidOutput) {
384
+ Write-Error "[ERROR] Codex exited with code $exitCode"
385
+ exit 1
386
+ }
387
+
388
+ # Use session ID from parameter
389
+ $threadId = $Session
390
+ if (-not [string]::IsNullOrWhiteSpace($textContent)) {
391
+ $outputContent += $textContent
392
+ }
393
+ } else {
394
+ # New session mode: JSON output
395
+ $jsonText = $jsonOutput.ToString()
396
+ Write-File-NoBOM -Path $jsonFile -Content $jsonText
397
+
398
+ # Check for errors - but only fail if no valid output was received
399
+ $stderrText = $stderrOutput.ToString()
400
+ $hasValidOutput = -not [string]::IsNullOrWhiteSpace($jsonText) -and $jsonText -match '"thread_id"'
401
+
402
+ if ($stderrText -match '\[ERROR\]' -and -not $hasValidOutput) {
403
+ Write-Error "[ERROR] Codex command failed"
404
+ Write-Error $stderrText
405
+ exit 1
406
+ }
407
+
408
+ if ($exitCode -ne 0 -and -not $hasValidOutput) {
409
+ Write-Error "[ERROR] Codex exited with code $exitCode"
410
+ exit 1
411
+ }
412
+
413
+ # Extract thread_id and messages from JSON stream
414
+ if (-not [string]::IsNullOrWhiteSpace($jsonText)) {
415
+ # Find thread_id
416
+ if ($jsonText -match '"thread_id"\s*:\s*"([^"]+)"') {
417
+ $threadId = $matches[1]
418
+ }
419
+
420
+ # Parse JSON lines using PowerShell native parsing (more reliable on Windows)
421
+ $jsonLines = $jsonText -split "`n" | Where-Object { $_.Trim() -and $_.TrimStart().StartsWith('{') }
422
+
423
+ foreach ($line in $jsonLines) {
424
+ try {
425
+ $obj = $line | ConvertFrom-Json -ErrorAction SilentlyContinue
426
+ if (-not $obj) { continue }
427
+
428
+ # Process completed items
429
+ if ($obj.type -eq 'item.completed' -and $obj.item) {
430
+ $item = $obj.item
431
+
432
+ # Agent messages
433
+ if ($item.type -eq 'agent_message' -and $item.text) {
434
+ $outputContent += $item.text
435
+ }
436
+
437
+ # Command executions
438
+ if ($item.type -eq 'command_execution' -and $item.command) {
439
+ $cmd = $item.command -replace '^/bin/(zsh|bash) (-lc|-c) ', ''
440
+ $cmdPreview = $cmd.Substring(0, [Math]::Min(200, $cmd.Length))
441
+ $outPreview = ''
442
+ if ($item.aggregated_output) {
443
+ $outPreview = $item.aggregated_output.Substring(0, [Math]::Min(500, $item.aggregated_output.Length))
444
+ }
445
+ $outputContent += "### Shell: ``$cmdPreview```n$outPreview"
446
+ }
447
+
448
+ # Tool calls (file operations)
449
+ if ($item.type -eq 'tool_call' -and $item.name) {
450
+ $args = $null
451
+ try {
452
+ $args = $item.arguments | ConvertFrom-Json -ErrorAction SilentlyContinue
453
+ } catch {}
454
+
455
+ if ($item.name -eq 'write_file' -and $args.path) {
456
+ $outputContent += "### File written: $($args.path)"
457
+ }
458
+ if ($item.name -eq 'patch_file' -and $args.path) {
459
+ $outputContent += "### File patched: $($args.path)"
460
+ }
461
+ if ($item.name -eq 'shell' -and $args.command) {
462
+ $cmdPreview = $args.command.Substring(0, [Math]::Min(200, $args.command.Length))
463
+ $outPreview = ''
464
+ if ($item.output) {
465
+ $outPreview = $item.output.Substring(0, [Math]::Min(500, $item.output.Length))
466
+ }
467
+ $outputContent += "### Shell: ``$cmdPreview```n$outPreview"
468
+ }
469
+ }
470
+ }
471
+ } catch {
472
+ # Skip malformed lines
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ # Ensure output directory exists
479
+ $outputDir = Split-Path $Output -Parent
480
+ if (-not (Test-Path $outputDir)) {
481
+ New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
482
+ }
483
+
484
+ # Write output
485
+ if ($outputContent.Count -gt 0) {
486
+ Write-File-NoBOM -Path $Output -Content ($outputContent -join "`n")
487
+ } else {
488
+ Write-File-NoBOM -Path $Output -Content "(no response from codex)"
489
+ }
490
+
491
+ # Output results
492
+ if (-not [string]::IsNullOrEmpty($threadId)) {
493
+ Write-Output "session_id=$threadId"
494
+ }
495
+ Write-Output "output_path=$Output"
496
+
497
+ } finally {
498
+ & $cleanupScript
499
+ }
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'USAGE'
6
+ Usage:
7
+ ask_codex.sh <task> [options]
8
+ ask_codex.sh -t <task> [options]
9
+
10
+ Task input:
11
+ <task> First positional argument is the task text
12
+ -t, --task <text> Alias for positional task (backward compat)
13
+ (stdin) Pipe task text via stdin if no arg/flag given
14
+
15
+ File context (optional, repeatable):
16
+ -f, --file <path> Priority file path
17
+
18
+ Multi-turn:
19
+ --session <id> Resume a previous session (thread_id from prior run)
20
+
21
+ Options:
22
+ -w, --workspace <path> Workspace directory (default: current directory)
23
+ --model <name> Model override
24
+ --reasoning <level> Reasoning effort: low, medium, high (default: medium)
25
+ --sandbox <mode> Sandbox mode override
26
+ --read-only Read-only sandbox (no file changes)
27
+ --full-auto Full-auto mode (default)
28
+ -o, --output <path> Output file path
29
+ -h, --help Show this help
30
+
31
+ Output (on success):
32
+ session_id=<thread_id> Use with --session for follow-up calls
33
+ output_path=<file> Path to response markdown
34
+
35
+ Examples:
36
+ # New task (positional)
37
+ ask_codex.sh "Add error handling to api.ts" -f src/api.ts
38
+
39
+ # With explicit workspace
40
+ ask_codex.sh "Fix the bug" -w /other/repo
41
+
42
+ # Continue conversation
43
+ ask_codex.sh "Also add retry logic" --session <id>
44
+ USAGE
45
+ }
46
+
47
+ require_cmd() {
48
+ if ! command -v "$1" >/dev/null 2>&1; then
49
+ echo "[ERROR] Missing required command: $1" >&2
50
+ exit 1
51
+ fi
52
+ }
53
+
54
+ trim_whitespace() {
55
+ awk 'BEGIN { RS=""; ORS="" } { gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, ""); print }' <<<"$1"
56
+ }
57
+
58
+ to_abs_if_exists() {
59
+ local target="$1"
60
+ if [[ -e "$target" ]]; then
61
+ local dir
62
+ dir="$(cd "$(dirname "$target")" && pwd)"
63
+ echo "$dir/$(basename "$target")"
64
+ return
65
+ fi
66
+ echo "$target"
67
+ }
68
+
69
+ resolve_file_ref() {
70
+ local workspace="$1" raw="$2" cleaned
71
+ cleaned="$(trim_whitespace "$raw")"
72
+ [[ -z "$cleaned" ]] && { echo ""; return; }
73
+ if [[ "$cleaned" =~ ^(.+)#L[0-9]+$ ]]; then cleaned="${BASH_REMATCH[1]}"; fi
74
+ if [[ "$cleaned" =~ ^(.+):[0-9]+(-[0-9]+)?$ ]]; then cleaned="${BASH_REMATCH[1]}"; fi
75
+ if [[ "$cleaned" != /* ]]; then cleaned="$workspace/$cleaned"; fi
76
+ to_abs_if_exists "$cleaned"
77
+ }
78
+
79
+ append_file_refs() {
80
+ local raw="$1" item
81
+ IFS=',' read -r -a items <<< "$raw"
82
+ for item in "${items[@]}"; do
83
+ local trimmed
84
+ trimmed="$(trim_whitespace "$item")"
85
+ [[ -n "$trimmed" ]] && file_refs+=("$trimmed")
86
+ done
87
+ }
88
+
89
+ # --- Parse arguments ---
90
+
91
+ workspace="${PWD}"
92
+ task_text=""
93
+ model=""
94
+ reasoning_effort=""
95
+ sandbox_mode=""
96
+ read_only=false
97
+ full_auto=true
98
+ output_path=""
99
+ session_id=""
100
+ file_refs=()
101
+
102
+ while [[ $# -gt 0 ]]; do
103
+ case "$1" in
104
+ -w|--workspace) workspace="${2:-}"; shift 2 ;;
105
+ -t|--task) task_text="${2:-}"; shift 2 ;;
106
+ -f|--file|--focus) append_file_refs "${2:-}"; shift 2 ;;
107
+ --model) model="${2:-}"; shift 2 ;;
108
+ --reasoning) reasoning_effort="${2:-}"; shift 2 ;;
109
+ --sandbox) sandbox_mode="${2:-}"; full_auto=false; shift 2 ;;
110
+ --read-only) read_only=true; full_auto=false; shift ;;
111
+ --full-auto) full_auto=true; shift ;;
112
+ --session) session_id="${2:-}"; shift 2 ;;
113
+ -o|--output) output_path="${2:-}"; shift 2 ;;
114
+ -h|--help) usage; exit 0 ;;
115
+ -*) echo "[ERROR] Unknown option: $1" >&2; usage >&2; exit 1 ;;
116
+ *) if [[ -z "$task_text" ]]; then task_text="$1"; shift; else echo "[ERROR] Unexpected argument: $1" >&2; usage >&2; exit 1; fi ;;
117
+ esac
118
+ done
119
+
120
+ require_cmd codex
121
+ require_cmd jq
122
+
123
+ # --- Validate inputs ---
124
+
125
+ if [[ ! -d "$workspace" ]]; then
126
+ echo "[ERROR] Workspace does not exist: $workspace" >&2; exit 1
127
+ fi
128
+ workspace="$(cd "$workspace" && pwd)"
129
+
130
+ if [[ -z "$task_text" && ! -t 0 ]]; then
131
+ task_text="$(cat)"
132
+ fi
133
+ task_text="$(trim_whitespace "$task_text")"
134
+
135
+ if [[ -z "$task_text" ]]; then
136
+ echo "[ERROR] Request text is empty. Pass a positional arg, --task, or stdin." >&2; exit 1
137
+ fi
138
+
139
+ # --- Prepare output path ---
140
+
141
+ if [[ -z "$output_path" ]]; then
142
+ timestamp="$(date -u +"%Y%m%d-%H%M%S")"
143
+ skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
144
+ output_path="$skill_dir/.runtime/${timestamp}.md"
145
+ fi
146
+ mkdir -p "$(dirname "$output_path")"
147
+
148
+ # --- Build file context block ---
149
+
150
+ file_block=""
151
+ if (( ${#file_refs[@]} > 0 )); then
152
+ file_block=$'\nPriority files (read these first before making changes):'
153
+ for ref in "${file_refs[@]}"; do
154
+ resolved="$(resolve_file_ref "$workspace" "$ref")"
155
+ [[ -z "$resolved" ]] && continue
156
+ exists_tag="missing"
157
+ [[ -e "$resolved" ]] && exists_tag="exists"
158
+ file_block+=$'\n- '"${resolved} (${exists_tag})"
159
+ done
160
+ fi
161
+
162
+ # --- Build prompt ---
163
+
164
+ prompt="$task_text"
165
+ if [[ -n "$file_block" ]]; then
166
+ prompt+=$'\n'"$file_block"
167
+ fi
168
+
169
+ # --- Determine reasoning effort ---
170
+
171
+ if [[ -z "$reasoning_effort" ]]; then
172
+ reasoning_effort="medium"
173
+ fi
174
+
175
+ # --- Build codex command ---
176
+
177
+ if [[ -n "$session_id" ]]; then
178
+ # Resume mode: continue a previous session
179
+ # Note: resume only supports -c/--config and --last flags (no --json, --sandbox, etc.)
180
+ cmd=(codex exec resume -c "model_reasoning_effort=\"$reasoning_effort\"" -c "skip_git_repo_check=true")
181
+ cmd+=("$session_id")
182
+ else
183
+ # New session
184
+ cmd=(codex exec --cd "$workspace" --skip-git-repo-check --json -c "model_reasoning_effort=\"$reasoning_effort\"")
185
+ if [[ "$read_only" == true ]]; then
186
+ cmd+=(--sandbox read-only)
187
+ elif [[ -n "$sandbox_mode" ]]; then
188
+ cmd+=(--sandbox "$sandbox_mode")
189
+ elif [[ "$full_auto" == true ]]; then
190
+ cmd+=(--full-auto)
191
+ fi
192
+ [[ -n "$model" ]] && cmd+=(-m "$model")
193
+ fi
194
+
195
+ # --- Progress watcher function ---
196
+
197
+ print_progress() {
198
+ local line="$1"
199
+ local item_type cmd_str preview
200
+ # Fast string checks before calling jq
201
+ case "$line" in
202
+ *'"item.started"'*'"command_execution"'*)
203
+ cmd_str=$(printf '%s' "$line" | jq -r '.item.command // empty' 2>/dev/null | sed 's|^/bin/zsh -lc ||; s|^/bin/bash -c ||' | cut -c1-100)
204
+ [[ -n "$cmd_str" ]] && echo "[codex] > $cmd_str" >&2
205
+ ;;
206
+ *'"item.completed"'*'"agent_message"'*)
207
+ preview=$(printf '%s' "$line" | jq -r '.item.text // empty' 2>/dev/null | head -1 | cut -c1-120)
208
+ [[ -n "$preview" ]] && echo "[codex] $preview" >&2
209
+ ;;
210
+ esac
211
+ }
212
+
213
+ # --- Execute and capture output ---
214
+
215
+ stderr_file="$(mktemp)"
216
+ json_file="$(mktemp)"
217
+ text_file="$(mktemp)"
218
+ prompt_file="$(mktemp)"
219
+ trap 'rm -f "$stderr_file" "$json_file" "$text_file" "$prompt_file"' EXIT
220
+
221
+ # Write prompt to a temp file and pipe from there to avoid shell argument
222
+ # length issues and encoding problems with very long or multi-byte prompts.
223
+ printf "%s" "$prompt" > "$prompt_file"
224
+
225
+ # Run codex and capture its output.
226
+ # We prefer `script` to allocate a pseudo-TTY, which forces codex to line-buffer
227
+ # its output so progress events arrive in real time. However, `script` requires a
228
+ # real controlling terminal and fails with "tcgetattr/ioctl: Operation not supported
229
+ # on socket" in socket-based environments (e.g. some Claude Code sandboxes). We
230
+ # detect this upfront and fall back to direct execution — output may arrive all at
231
+ # once at the end, but the task still completes correctly.
232
+ run_codex() {
233
+ # BSD script (macOS): script [-q] [file [command...]]
234
+ # util-linux script (Linux): script [-q] -c <command> [file]
235
+ # Probe the local variant and use matching syntax for PTY allocation.
236
+ # Falls back to direct execution if neither probe succeeds (e.g. socket stdin).
237
+ local os
238
+ os="$(uname -s)"
239
+ if [[ "$os" == "Darwin" ]]; then
240
+ if script -q /dev/null true >/dev/null 2>&1; then
241
+ script -q /dev/null /bin/bash -c \
242
+ "cd $(printf '%q' "$workspace") && $(printf '%q ' "${cmd[@]}") < $(printf '%q' "$prompt_file") 2>$(printf '%q' "$stderr_file")"
243
+ return
244
+ fi
245
+ else
246
+ if script -q -c "true" /dev/null >/dev/null 2>&1; then
247
+ script -q -c \
248
+ "cd $(printf '%q' "$workspace") && $(printf '%q ' "${cmd[@]}") < $(printf '%q' "$prompt_file") 2>$(printf '%q' "$stderr_file")" \
249
+ /dev/null
250
+ return
251
+ fi
252
+ fi
253
+ # Fallback: direct execution (no PTY; progress events arrive in batch)
254
+ (cd "$workspace" && "${cmd[@]}" < "$prompt_file" 2>"$stderr_file")
255
+ }
256
+
257
+ if [[ -n "$session_id" ]]; then
258
+ # Resume mode: plain text output (no JSON support)
259
+ run_codex | while IFS= read -r line; do
260
+ # Strip terminal artifacts (carriage return, ^D EOF marker)
261
+ cleaned="${line//$'\r'/}"
262
+ cleaned="${cleaned//$'\004'/}"
263
+ [[ -z "$cleaned" ]] && continue
264
+ # Write to text_file for later output
265
+ printf '%s\n' "$cleaned" >> "$text_file"
266
+ # Print progress
267
+ preview="${cleaned:0:120}"
268
+ echo "[codex] $preview" >&2
269
+ done
270
+ else
271
+ # New session: JSON output
272
+ run_codex | while IFS= read -r line; do
273
+ # Strip terminal artifacts (carriage return, ^D EOF marker)
274
+ cleaned="${line//$'\r'/}"
275
+ cleaned="${cleaned//$'\004'/}"
276
+ [[ -z "$cleaned" ]] && continue
277
+ # Only process JSON lines (must start with '{')
278
+ [[ "$cleaned" != \{* ]] && continue
279
+ # Write to json_file for later parsing
280
+ printf '%s\n' "$cleaned" >> "$json_file"
281
+ # Only parse progress-relevant events (fast string check before jq)
282
+ case "$cleaned" in
283
+ *'"item.started"'*|*'"item.completed"'*) print_progress "$cleaned" ;;
284
+ esac
285
+ done
286
+ fi
287
+
288
+ if [[ -s "$stderr_file" ]] && grep -q '\[ERROR\]' "$stderr_file" 2>/dev/null; then
289
+ echo "[ERROR] Codex command failed" >&2
290
+ cat "$stderr_file" >&2
291
+ exit 1
292
+ fi
293
+
294
+ if [[ -s "$stderr_file" ]]; then
295
+ cat "$stderr_file" >&2
296
+ fi
297
+
298
+ # --- Process output based on mode ---
299
+
300
+ if [[ -n "$session_id" ]]; then
301
+ # Resume mode: use plain text output
302
+ thread_id="$session_id"
303
+ if [[ -s "$text_file" ]]; then
304
+ cat "$text_file" > "$output_path"
305
+ else
306
+ echo "(no response from codex)" > "$output_path"
307
+ fi
308
+ else
309
+ # New session: Extract thread_id and all messages from JSON stream
310
+ thread_id="$(jq -r 'select(.type == "thread.started") | .thread_id' < "$json_file" | head -1)"
311
+
312
+ # Collect all completed items: file changes, tool calls, and agent messages.
313
+ # This gives full visibility into what codex actually did, not just the last message.
314
+ {
315
+ # 1. Show command executions — skip pure file-reading/searching commands.
316
+ # Codex explores the codebase heavily (sed/cat/nl/rg/grep/awk/wc/find/ls), but
317
+ # those reads produce no signal for Claude Code — it can read files directly if needed.
318
+ # Keep build, test, git, and mutation commands that reflect actual work done.
319
+ #
320
+ # Note: zsh wraps commands in quotes, so after stripping the shell prefix the
321
+ # command may start with " or ' — the regex accounts for this with [\"']?.
322
+ jq -r '
323
+ select(.type == "item.completed" and .item.type == "command_execution")
324
+ | .item
325
+ | ((.command // "") | gsub("^/bin/zsh -lc "; "") | gsub("^/bin/bash -c "; "")) as $cmd
326
+ | select($cmd | test("^[\"'"'"']?(sed |cat |head |tail |nl |rg |grep |awk |wc |find |ls )") | not)
327
+ | "### Shell: `" + ($cmd[0:200]) + "`\n" + (.aggregated_output // "" | .[0:500])
328
+ ' < "$json_file" 2>/dev/null
329
+
330
+ # 2. Show file write/patch operations (tool_call style, if any)
331
+ jq -r '
332
+ select(.type == "item.completed" and .item.type == "tool_call")
333
+ | .item
334
+ | if .name == "write_file" then
335
+ "### File written: " + (.arguments | fromjson | .path // "unknown")
336
+ elif .name == "patch_file" then
337
+ "### File patched: " + (.arguments | fromjson | .path // "unknown")
338
+ elif .name == "shell" then
339
+ "### Shell: `" + (.arguments | fromjson | .command // "unknown")[0:200] + "`\n" + (.output // "" | .[0:500])
340
+ else empty
341
+ end
342
+ ' < "$json_file" 2>/dev/null
343
+
344
+ # 3. Show all agent messages. Short messages (lint results, "tests failed",
345
+ # "no changes needed") carry high signal and must not be dropped by a length
346
+ # threshold. In practice, Codex tends to emit a small number of large blocks
347
+ # rather than many tiny fragments, so this produces clean output without filtering.
348
+ jq -r '
349
+ select(.type == "item.completed" and .item.type == "agent_message") | .item.text
350
+ ' < "$json_file" 2>/dev/null
351
+ } > "$output_path"
352
+
353
+ # If nothing was captured, write a fallback
354
+ if [[ ! -s "$output_path" ]]; then
355
+ echo "(no response from codex)" > "$output_path"
356
+ fi
357
+ fi
358
+
359
+ # --- Output results ---
360
+
361
+ if [[ -n "$thread_id" ]]; then
362
+ echo "session_id=$thread_id"
363
+ fi
364
+ echo "output_path=$output_path"