aiwcli 0.15.1 → 0.15.2

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.
@@ -1,42 +1,35 @@
1
1
  ---
2
2
  name: codex
3
- description: Launch Codex CLI in a tmux pane. USE WHEN codex OR send to codex OR codex implement OR hand off to codex OR launch codex OR codex plan OR run codex.
3
+ description: Delegate implementation to Codex sub-agents. USE WHEN codex OR send to codex OR codex implement OR hand off to codex OR launch codex OR codex plan OR run codex.
4
4
  user-invocable: true
5
5
  ---
6
6
 
7
- # Codex CLI
7
+ Read `.aiwcli/_shared/skills/codex/SKILL.md` for delegation patterns and examples.
8
8
 
9
- Launch Codex in a tmux split pane and optionally inject a prompt.
9
+ ## Role
10
10
 
11
- ## Command
11
+ You are the orchestrator. Codex instances are your implementation sub-agents. Decide what to delegate, how to split work, and review results when summaries arrive.
12
12
 
13
- `bun .aiwcli/_shared/skills/prompt-codex/scripts/launch-codex.ts [flags] <mode>`
13
+ ## Command
14
14
 
15
- **Modes:** `plan` (inject active plan) | `--file <path>` (inject file) | `<text...>` (inject inline text)
15
+ ```
16
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [flags] <mode>
17
+ ```
16
18
 
17
- **Flags:**
18
- - `--model <name>` — Model aliases: `spark`, `codex`, `gpt`. Tiers: `fast`, `standard`, `smart`. Or any full model ID.
19
- - `--sandbox <mode>` — `read-only`, `workspace-write`, `danger-full-access`. Default: Codex default.
20
- - `--context <id>` — Pass the active context ID so Codex receives project orientation (context folder, notes path). **Always pass this when an active context exists** (check the `Active Context:` system reminder for the ID).
19
+ The script blocks until Codex exits and prints a summary — run with Bash `run_in_background: true` so you stay unblocked.
21
20
 
22
- ## Model Reference
21
+ **Modes:** `plan` | `--file <path>` | `<inline text...>`
23
22
 
24
- | Name | Resolves To |
25
- |------|-------------|
26
- | `spark` | `gpt-5.3-codex-spark` (fastest) |
27
- | `codex` | `gpt-5.3-codex` (default) |
28
- | `gpt` | `gpt-5.2` (non-Codex GPT) |
29
- | `fast` | `gpt-5.3-codex-spark` (tier) |
30
- | `standard` | `gpt-5.3-codex` (tier) |
31
- | `smart` | `gpt-5.3-codex` (tier) |
23
+ **Key flags:**
24
+ - `--context <id>` — Project orientation. Pass when implementing a plan.
25
+ - `--prompt <text>` Scope the agent's work to a specific plan section or task.
26
+ - `--model <name>` — `spark`, `codex`, `gpt`, or tier: `fast`, `standard`, `smart`.
27
+ - `--no-watch` Fire-and-forget (skip waiting for summary).
32
28
 
33
- ## Examples
29
+ ## Delegation Decision
34
30
 
35
- - `/codex --model spark --context <context-id> plan` hand off active plan to Spark with context
36
- - `/codex --model codex --context <context-id> Refactor auth to use JWT` — inline prompt with context
37
- - `/codex --file ./spec.md` — inject file contents (no context)
31
+ **One-shot:** Plan is small or tightly coupled launch one Codex with `plan` mode. Wait for the summary, then review.
38
32
 
39
- ## Requirements
33
+ **Parallel:** Plan has independent sections → launch multiple Codex instances, each scoped with `--prompt` to its section. All share the same `--context`. Review when summaries arrive, check for conflicts.
40
34
 
41
- - Must be running inside tmux
42
- - `codex` CLI must be on PATH
35
+ **Ad-hoc:** No plan, just a task → pass inline text (e.g., `"Fix the failing test in auth.ts"`).
@@ -0,0 +1,11 @@
1
+ # Codex Workflow
2
+
3
+ Use Codex CLI handoff instructions from `.aiwcli/_shared/skills/codex/SKILL.md`.
4
+
5
+ ## Command
6
+
7
+ `bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [flags] <mode>`
8
+
9
+ **Modes:** `plan` | `--file <path>` | `<inline text...>`
10
+
11
+ **Common flags:** `--model <name>`, `--sandbox <mode>`, `--context <id>`, `--prompt <text>`, `--no-yolo`, `--capture`
@@ -12,9 +12,11 @@
12
12
  import * as fs from "node:fs";
13
13
 
14
14
  import { execFileAsync, findExecutable } from "./subprocess-utils.js";
15
+ import { findBestSplit, listPanes } from "./tmux-pane-placement.js";
15
16
 
16
17
  export type DriverMode = "exec" | "repl";
17
18
  export type TmuxSplitFlag = "-h" | "-v";
19
+ export type TmuxSplitOption = TmuxSplitFlag | "auto";
18
20
 
19
21
  export interface DriverPreflightResult {
20
22
  available: boolean;
@@ -53,7 +55,7 @@ export interface LaunchDriverOptions {
53
55
  env?: Record<string, string>;
54
56
  promptPath?: string;
55
57
  sendPromptInRepl?: boolean;
56
- splitFlag?: string;
58
+ splitFlag?: TmuxSplitOption;
57
59
  splitTarget?: string;
58
60
  autoClose?: boolean;
59
61
  holdPane?: boolean;
@@ -165,6 +167,54 @@ export function normalizeSplitFlag(value: string | undefined): TmuxSplitFlag {
165
167
  return value?.trim() === "-v" ? "-v" : "-h";
166
168
  }
167
169
 
170
+ function splitFlagFromDimensions(width: number, height: number): TmuxSplitFlag {
171
+ return width >= height ? "-h" : "-v";
172
+ }
173
+
174
+ async function resolveSplitFlagForTargetPane(
175
+ tmuxPath: string,
176
+ splitTarget: string,
177
+ ): Promise<TmuxSplitFlag | null> {
178
+ const size = await execFileAsync(
179
+ tmuxPath,
180
+ ["display-message", "-p", "-t", splitTarget, "#{pane_width} #{pane_height}"],
181
+ { timeout: 3000 },
182
+ );
183
+ if (size.exitCode !== 0) return null;
184
+
185
+ const parts = size.stdout.trim().split(/\s+/);
186
+ if (parts.length < 2) return null;
187
+
188
+ const width = Number.parseInt(parts[0] ?? "", 10);
189
+ const height = Number.parseInt(parts[1] ?? "", 10);
190
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
191
+
192
+ return splitFlagFromDimensions(width, height);
193
+ }
194
+
195
+ async function resolveAutoSplit(
196
+ tmuxPath: string,
197
+ splitTarget?: string,
198
+ ): Promise<{ splitFlag: TmuxSplitFlag; splitTarget?: string }> {
199
+ const explicitTarget = splitTarget?.trim();
200
+ if (explicitTarget) {
201
+ const splitFlag = await resolveSplitFlagForTargetPane(tmuxPath, explicitTarget);
202
+ return {
203
+ splitFlag: splitFlag ?? "-h",
204
+ splitTarget: explicitTarget,
205
+ };
206
+ }
207
+
208
+ const panes = await listPanes(tmuxPath);
209
+ const placement = findBestSplit(panes);
210
+ if (!placement) return { splitFlag: "-h" };
211
+
212
+ return {
213
+ splitFlag: placement.splitFlag,
214
+ splitTarget: placement.targetPane,
215
+ };
216
+ }
217
+
168
218
  export function isTruthy(value: string | undefined): boolean {
169
219
  if (!value) return false;
170
220
  const normalized = value.trim().toLowerCase();
@@ -287,8 +337,25 @@ export async function launchDriverInTmuxOrFallback(
287
337
 
288
338
  const tmux = getTmuxAvailability();
289
339
  if (tmux.available && tmux.tmuxPath) {
290
- const splitFlag = normalizeSplitFlag(options.splitFlag);
291
- const splitTarget = options.splitTarget?.trim();
340
+ const requestedSplitFlag = options.splitFlag;
341
+ const explicitSplitTarget = options.splitTarget?.trim();
342
+ let splitFlag: TmuxSplitFlag;
343
+ let splitTarget: string | undefined;
344
+
345
+ if (requestedSplitFlag === "auto") {
346
+ try {
347
+ const resolved = await resolveAutoSplit(tmux.tmuxPath, explicitSplitTarget);
348
+ splitFlag = resolved.splitFlag;
349
+ splitTarget = resolved.splitTarget;
350
+ } catch {
351
+ splitFlag = "-h";
352
+ splitTarget = explicitSplitTarget;
353
+ }
354
+ } else {
355
+ splitFlag = normalizeSplitFlag(requestedSplitFlag);
356
+ splitTarget = explicitSplitTarget;
357
+ }
358
+
292
359
  const baseCmd = buildToolCommand(toolPath, args, envVars, mode, options.promptPath);
293
360
  const holdMessage = options.holdMessage ?? "[aiwcli] Driver exited. Pane held open.";
294
361
  const paneBody = wrapPaneCommand(
@@ -0,0 +1,70 @@
1
+ import { execFileAsync } from "./subprocess-utils.js";
2
+
3
+ export type TmuxSplitFlag = "-h" | "-v";
4
+
5
+ export interface TmuxPaneInfo {
6
+ paneId: string;
7
+ width: number;
8
+ height: number;
9
+ active: boolean;
10
+ }
11
+
12
+ export interface PlacementResult {
13
+ targetPane: string;
14
+ splitFlag: TmuxSplitFlag;
15
+ }
16
+
17
+ const LIST_PANES_FORMAT = "#{pane_id} #{pane_width} #{pane_height} #{pane_active}";
18
+
19
+ export async function listPanes(tmuxPath: string): Promise<TmuxPaneInfo[]> {
20
+ const result = await execFileAsync(tmuxPath, ["list-panes", "-F", LIST_PANES_FORMAT], {
21
+ timeout: 3000,
22
+ });
23
+ if (result.exitCode !== 0) return [];
24
+
25
+ const panes: TmuxPaneInfo[] = [];
26
+ for (const rawLine of result.stdout.split(/\r?\n/)) {
27
+ const line = rawLine.trim();
28
+ if (!line) continue;
29
+
30
+ const parts = line.split(/\s+/);
31
+ if (parts.length < 4) continue;
32
+
33
+ const paneId = parts[0] ?? "";
34
+ const width = Number.parseInt(parts[1] ?? "", 10);
35
+ const height = Number.parseInt(parts[2] ?? "", 10);
36
+ const activeRaw = parts[3] ?? "";
37
+
38
+ if (!paneId || !Number.isFinite(width) || !Number.isFinite(height)) continue;
39
+ panes.push({
40
+ paneId,
41
+ width,
42
+ height,
43
+ active: activeRaw === "1",
44
+ });
45
+ }
46
+
47
+ return panes;
48
+ }
49
+
50
+ export function findBestSplit(panes: TmuxPaneInfo[]): PlacementResult | null {
51
+ if (panes.length === 0) return null;
52
+
53
+ let best = panes[0];
54
+ let bestArea = best.width * best.height;
55
+
56
+ for (let i = 1; i < panes.length; i++) {
57
+ const pane = panes[i];
58
+ if (!pane) continue;
59
+ const area = pane.width * pane.height;
60
+ if (area > bestArea) {
61
+ best = pane;
62
+ bestArea = area;
63
+ }
64
+ }
65
+
66
+ return {
67
+ targetPane: best.paneId,
68
+ splitFlag: best.width >= best.height ? "-h" : "-v",
69
+ };
70
+ }
@@ -52,7 +52,7 @@ if (!fs.existsSync(fullPath)) {
52
52
  process.exit(1);
53
53
  }
54
54
 
55
- const result = Bun.spawnSync(["bun", fullPath], {
55
+ const result = Bun.spawnSync(["bun", fullPath, ...process.argv.slice(3)], {
56
56
  stdin: "inherit",
57
57
  stdout: "inherit",
58
58
  stderr: "inherit",
@@ -0,0 +1,77 @@
1
+ # Codex Skill
2
+
3
+ Launch Codex CLI in a tmux pane and inject a prompt into its REPL.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ codex/
9
+ ├── CLAUDE.md ← This file
10
+ ├── lib/
11
+ │ └── codex-watcher.ts ← Reusable watch/summarize library
12
+ └── scripts/
13
+ └── launch-codex.ts ← Single entry point (launch + optional watch)
14
+ ```
15
+
16
+ ## Script: launch-codex.ts
17
+
18
+ **Usage:**
19
+ ```bash
20
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--prompt <text>] [--no-yolo] [--no-watch] [--context <id>] plan
21
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--prompt <text>] [--no-yolo] [--no-watch] [--context <id>] --file <path>
22
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--prompt <text>] [--no-yolo] [--no-watch] [--context <id>] <inline text...>
23
+ ```
24
+
25
+ **Args:**
26
+ - `plan` — discover active plan via context system, inject into Codex REPL
27
+ - `--file <path>` — inject file contents into Codex REPL
28
+ - `<text...>` — join remaining args as inline prompt, write to temp file, inject
29
+ - `--model <alias|tier|id>` — Aliases: `spark` → `gpt-5.3-codex-spark`, `codex` → `gpt-5.3-codex`, `gpt` → `gpt-5.2`. Tiers: `fast`/`standard`/`smart` (resolved via `resolveModelForProvider()`). Or any full model ID. Aliases are checked first (local `CODEX_ALIASES` constant in `launch-codex.ts`), then tiers, then pass-through. Omitted = Codex default.
30
+ - `--sandbox <mode>` — `read-only`, `workspace-write`, or `danger-full-access`. Default is `danger-full-access` for implementation handoffs.
31
+ - `--prompt <text>` — append extra instructions under `## Additional Instructions` after the main prompt body.
32
+ - `--no-yolo` — Disable YOLO mode (on by default). YOLO maps to Codex CLI's `--dangerously-bypass-approvals-and-sandbox`. Use `--no-yolo` to restore normal approval prompts.
33
+ - `--no-watch` — Disable watch/summarize mode. Launch exits immediately after Codex starts.
34
+
35
+ **Plan discovery order:**
36
+ 1. `CLAUDE_SESSION_ID` env → `getContextBySessionId()` → `findLatestPlan(contextId)`
37
+ 2. Fallback: scan `_output/contexts/*/plans/*.md` by mtime (inline, no `_cc-native` import)
38
+
39
+ **Dependencies (all from `_shared/lib-ts/`):**
40
+ - `base/tmux-driver.ts` — `launchDriverInTmuxOrFallback()`, `getTmuxAvailability()`
41
+ - `base/cli-args.ts` — `resolveCodexModel()`, `codexReplSpec()`, `buildCliInvocation()`, `isCodexSandbox()`
42
+ - `base/logger.ts` — `logDebug()`, `logWarn()` (injection diagnostics)
43
+ - `context/context-store.ts` — `getContextBySessionId()`
44
+ - `context/context-formatter.ts` — `buildExternalAgentContext()` (orientation header for Codex)
45
+ - `context/plan-manager.ts` — `findLatestPlan()`
46
+
47
+ **Watch behavior (single entry point):**
48
+ - Watch is enabled by default.
49
+ - `launch-codex.ts` launches Codex, waits for pane close (or timeout), and prints a summary.
50
+ - Summary cascade:
51
+ 1. Spark transcript summary from session file
52
+ 2. `codex exec resume <session_id>` summary
53
+ 3. Transcript-line fallback
54
+ 4. Static `Summary unavailable` message
55
+ - Watch flow is best-effort and does not change launch success semantics.
56
+
57
+ **Design decisions:**
58
+ - Always creates a new tmux pane (no pane reuse/tracking)
59
+ - No exec fallback — REPL mode requires tmux
60
+ - `_shared` only — never imports from `_cc-native`
61
+ - Temp file cleanup after injection confirmed
62
+
63
+ ## Library: lib/codex-watcher.ts
64
+
65
+ Reusable side-effect-free watch/summarize functions used by launch flow:
66
+ - `waitForPaneClose(paneId, timeoutMs?)`
67
+ - `summarizeViaSessionFileSpark(sessionFile)`
68
+ - `summarizeViaResume(sessionId)`
69
+ - `summarizeFromSessionFileFallback(sessionFile)`
70
+ - `collectTranscriptLines(sessionFile)`
71
+
72
+ Constants and helper utilities are exported for reuse and testing (`POLL_INTERVAL_MS`, `SUMMARY_UNAVAILABLE_MESSAGE`, `normalizeText`, `looksLikeBadSummary`, etc.).
73
+
74
+ **Resilience policy:**
75
+ - Watch path is best-effort and never fails a successful launch
76
+ - Pane-wait timeout defaults to 4 hours; when reached, summarization continues with available transcript state
77
+ - Summary functions degrade through layered fallbacks and end with static message
@@ -0,0 +1,71 @@
1
+ # /codex
2
+
3
+ ## Role
4
+
5
+ You are the orchestrator. Each Codex launch spawns an implementation sub-agent. Delegate implementation work to Codex, then review results when summaries arrive.
6
+
7
+ The script blocks until Codex exits and prints a session summary. Run with Bash `run_in_background: true` so you stay unblocked and receive the summary as a background task notification.
8
+
9
+ ## Command
10
+
11
+ ```
12
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [flags] <mode>
13
+ ```
14
+
15
+ **Modes:** `plan` | `--file <path>` | `<inline text...>`
16
+
17
+ **Flags:**
18
+ - `--context <id>` — Project orientation for the sub-agent. Pass when implementing a plan so Codex understands the project structure.
19
+ - `--prompt <text>` — Scope the agent's work. Direct each instance to a specific plan section or task.
20
+ - `--model <name>` — Aliases: `spark`, `codex`, `gpt`. Tiers: `fast`, `standard`, `smart`. Or any full model ID.
21
+ - `--sandbox <mode>` — `read-only`, `workspace-write`, `danger-full-access`.
22
+ - `--no-yolo` — Disable YOLO mode (on by default).
23
+ - `--no-watch` — Fire-and-forget: exit immediately after launch, skip waiting for summary.
24
+
25
+ ## Delegation Patterns
26
+
27
+ ### One-shot
28
+
29
+ For small or tightly coupled plans. One sub-agent implements the whole plan.
30
+
31
+ ```bash
32
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts --context <ctx-id> plan
33
+ ```
34
+
35
+ Run with `run_in_background: true`. Wait for the summary. Review the changes.
36
+
37
+ ### Parallel
38
+
39
+ For plans with independent sections. Each sub-agent owns one section, scoped by `--prompt`.
40
+
41
+ ```bash
42
+ # Sub-agent A — section 1
43
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts \
44
+ --context <ctx-id> --prompt "Implement section 1: Extract watch logic into lib/codex-watcher.ts" plan
45
+
46
+ # Sub-agent B — section 3
47
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts \
48
+ --context <ctx-id> --prompt "Implement section 3: Update launch-codex.ts arg parsing" plan
49
+ ```
50
+
51
+ Run each with `run_in_background: true`. Summaries arrive as separate background task notifications. When all complete, review for conflicts between agents, then verify with tests or import checks.
52
+
53
+ ### Ad-hoc
54
+
55
+ For tasks outside a plan. Pass inline text or a file path.
56
+
57
+ ```bash
58
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts \
59
+ "Fix the failing test in auth.ts"
60
+
61
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts \
62
+ --file path/to/task-description.md
63
+ ```
64
+
65
+ ## Orchestrator Checklist
66
+
67
+ - **Delegate implementation.** If the work involves writing code and Codex can handle it, send it to Codex.
68
+ - **Split independent sections** into parallel sub-agents for faster execution.
69
+ - **Pass `--context`** when implementing a plan — Codex needs project orientation to make good decisions.
70
+ - **Scope with `--prompt`** when running parallel agents — each sub-agent performs better when it knows exactly which section it owns.
71
+ - **Review results** when summaries arrive. Check for merge conflicts between parallel agents, then verify with `tsc --noEmit`, tests, or manual inspection.
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env bun
2
-
3
1
  import * as fs from "node:fs";
4
2
  import * as os from "node:os";
5
3
  import * as path from "node:path";
@@ -10,15 +8,16 @@ import { CODEX_MODELS } from "../../../lib-ts/base/models.js";
10
8
  import { execFileAsync } from "../../../lib-ts/base/subprocess-utils.js";
11
9
  import { getTmuxAvailability } from "../../../lib-ts/base/tmux-driver.js";
12
10
 
13
- const POLL_INTERVAL_MS = 2000;
14
- const POLL_TIMEOUT_MS = 3000;
15
- const SUMMARY_TIMEOUT_SEC = 8;
16
- const RESUME_TIMEOUT_MS = 45000;
17
- const MAX_TRANSCRIPT_LINES = 220;
18
- const MAX_LINE_LENGTH = 500;
19
- const SUMMARY_UNAVAILABLE_MESSAGE = "Codex session completed. Summary unavailable.";
11
+ export const POLL_INTERVAL_MS = 2000;
12
+ export const POLL_TIMEOUT_MS = 3000;
13
+ export const SUMMARY_TIMEOUT_SEC = 8;
14
+ export const RESUME_TIMEOUT_MS = 45000;
15
+ export const MAX_TRANSCRIPT_LINES = 220;
16
+ export const MAX_LINE_LENGTH = 500;
17
+ export const WAIT_TIMEOUT_MS_DEFAULT = 14_400_000;
18
+ export const SUMMARY_UNAVAILABLE_MESSAGE = "Codex session completed. Summary unavailable.";
20
19
 
21
- const TRANSCRIPT_SUMMARY_PROMPT = `Summarize this Codex session transcript excerpt.
20
+ export const TRANSCRIPT_SUMMARY_PROMPT = `Summarize this Codex session transcript excerpt.
22
21
  Return 3-5 concise bullet points.
23
22
  Focus on:
24
23
  - what was accomplished
@@ -28,7 +27,7 @@ Do not ask follow-up questions.
28
27
  Do not request additional input.
29
28
  If information is partial, provide best-effort summary from available text.`;
30
29
 
31
- const RESUME_SUMMARY_PROMPT = `Summarize the previous Codex session in 3-5 concise bullet points.
30
+ export const RESUME_SUMMARY_PROMPT = `Summarize the previous Codex session in 3-5 concise bullet points.
32
31
  Focus on:
33
32
  - what was accomplished
34
33
  - files changed
@@ -37,11 +36,11 @@ Do not ask follow-up questions.
37
36
  Do not request additional input.
38
37
  If the prior session was brief, still provide a best-effort summary.`;
39
38
 
40
- function sleep(ms: number): Promise<void> {
39
+ export function sleep(ms: number): Promise<void> {
41
40
  return new Promise((resolve) => setTimeout(resolve, ms));
42
41
  }
43
42
 
44
- function safeCleanup(filePath: string): void {
43
+ export function safeCleanup(filePath: string): void {
45
44
  try {
46
45
  if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
47
46
  } catch {
@@ -49,7 +48,7 @@ function safeCleanup(filePath: string): void {
49
48
  }
50
49
  }
51
50
 
52
- function readTextIfExists(filePath: string): string {
51
+ export function readTextIfExists(filePath: string): string {
53
52
  try {
54
53
  if (!filePath || !fs.existsSync(filePath)) return "";
55
54
  return fs.readFileSync(filePath, "utf-8").trim();
@@ -58,7 +57,7 @@ function readTextIfExists(filePath: string): string {
58
57
  }
59
58
  }
60
59
 
61
- function normalizeText(text: string): string {
60
+ export function normalizeText(text: string): string {
62
61
  return text
63
62
  .replace(/\r/g, "")
64
63
  .replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "")
@@ -66,7 +65,7 @@ function normalizeText(text: string): string {
66
65
  .trim();
67
66
  }
68
67
 
69
- function getMessageContentText(content: unknown): string {
68
+ export function getMessageContentText(content: unknown): string {
70
69
  if (!Array.isArray(content)) return "";
71
70
  return content
72
71
  .map((entry: any) => {
@@ -77,7 +76,7 @@ function getMessageContentText(content: unknown): string {
77
76
  .join("\n");
78
77
  }
79
78
 
80
- function collectTranscriptLines(sessionFile: string): string[] {
79
+ export function collectTranscriptLines(sessionFile: string): string[] {
81
80
  if (!sessionFile || !fs.existsSync(sessionFile)) return [];
82
81
 
83
82
  const out: string[] = [];
@@ -126,7 +125,7 @@ function collectTranscriptLines(sessionFile: string): string[] {
126
125
  return out.slice(-MAX_TRANSCRIPT_LINES);
127
126
  }
128
127
 
129
- function looksLikeBadSummary(output: string): boolean {
128
+ export function looksLikeBadSummary(output: string): boolean {
130
129
  const normalized = output.toLowerCase();
131
130
  return (
132
131
  normalized.includes("don't see") ||
@@ -136,14 +135,22 @@ function looksLikeBadSummary(output: string): boolean {
136
135
  );
137
136
  }
138
137
 
139
- async function waitForPaneClose(paneId: string): Promise<void> {
138
+ export async function waitForPaneClose(paneId: string, timeoutMs = WAIT_TIMEOUT_MS_DEFAULT): Promise<void> {
139
+ if (!paneId) return;
140
+
140
141
  const tmux = getTmuxAvailability();
141
142
  if (!tmux.available || !tmux.tmuxPath) {
142
143
  logWarn("codex-capture", `tmux unavailable while watching pane ${paneId}: ${tmux.reason ?? "unknown reason"}`);
143
144
  return;
144
145
  }
145
146
 
147
+ const deadline = Date.now() + timeoutMs;
146
148
  while (true) {
149
+ if (Date.now() >= deadline) {
150
+ logDebug("codex-capture", `watch timeout reached for pane ${paneId} after ${timeoutMs}ms`);
151
+ return;
152
+ }
153
+
147
154
  const result = await execFileAsync(tmux.tmuxPath, ["list-panes", "-a", "-F", "#{pane_id}"], {
148
155
  timeout: POLL_TIMEOUT_MS,
149
156
  });
@@ -156,11 +163,12 @@ async function waitForPaneClose(paneId: string): Promise<void> {
156
163
  const activePaneIds = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
157
164
  if (!activePaneIds.includes(paneId)) return;
158
165
 
159
- await sleep(POLL_INTERVAL_MS);
166
+ const remainingMs = deadline - Date.now();
167
+ await sleep(Math.max(0, Math.min(POLL_INTERVAL_MS, remainingMs)));
160
168
  }
161
169
  }
162
170
 
163
- function summarizeViaSessionFileSpark(sessionFile: string): string | null {
171
+ export function summarizeViaSessionFileSpark(sessionFile: string): string | null {
164
172
  const transcriptLines = collectTranscriptLines(sessionFile);
165
173
  if (transcriptLines.length === 0) return null;
166
174
 
@@ -184,7 +192,7 @@ function summarizeViaSessionFileSpark(sessionFile: string): string | null {
184
192
  return null;
185
193
  }
186
194
 
187
- async function summarizeViaResume(sessionId: string): Promise<string | null> {
195
+ export async function summarizeViaResume(sessionId: string): Promise<string | null> {
188
196
  const outputFile = path.join(os.tmpdir(), `codex-resume-summary-${Date.now()}-${process.pid}.txt`);
189
197
 
190
198
  const result = await execFileAsync(
@@ -211,47 +219,8 @@ async function summarizeViaResume(sessionId: string): Promise<string | null> {
211
219
  return null;
212
220
  }
213
221
 
214
- function summarizeFromSessionFileFallback(sessionFile: string): string | null {
222
+ export function summarizeFromSessionFileFallback(sessionFile: string): string | null {
215
223
  const lines = collectTranscriptLines(sessionFile).slice(-12);
216
224
  if (lines.length === 0) return null;
217
225
  return `Codex session completed. Transcript fallback:\n- ${lines.join("\n- ")}`;
218
226
  }
219
-
220
- async function main(): Promise<void> {
221
- const [paneId, sessionId, sessionFile] = process.argv.slice(2);
222
-
223
- if (!paneId) {
224
- console.log(SUMMARY_UNAVAILABLE_MESSAGE);
225
- return;
226
- }
227
-
228
- await waitForPaneClose(paneId);
229
-
230
- const transcriptSummary = summarizeViaSessionFileSpark(sessionFile ?? "");
231
- if (transcriptSummary) {
232
- console.log(transcriptSummary);
233
- return;
234
- }
235
-
236
- if (sessionId) {
237
- const resumeSummary = await summarizeViaResume(sessionId);
238
- if (resumeSummary) {
239
- console.log(resumeSummary);
240
- return;
241
- }
242
- }
243
-
244
- const fallback = summarizeFromSessionFileFallback(sessionFile ?? "");
245
- if (fallback) {
246
- console.log(fallback);
247
- return;
248
- }
249
-
250
- console.log(SUMMARY_UNAVAILABLE_MESSAGE);
251
- }
252
-
253
- main().catch((error) => {
254
- logWarn("codex-capture", `watch-codex failed: ${String(error)}`);
255
- console.log(SUMMARY_UNAVAILABLE_MESSAGE);
256
- process.exit(0);
257
- });
@@ -3,9 +3,9 @@
3
3
  * Launch Codex in a tmux pane and inject a prompt into its REPL.
4
4
  *
5
5
  * Usage:
6
- * bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] [--context <id>] plan
7
- * bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] [--context <id>] --file <path>
8
- * bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] [--context <id>] <inline text...>
6
+ * bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] plan
7
+ * bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] --file <path>
8
+ * bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] <inline text...>
9
9
  */
10
10
  import * as fs from "node:fs";
11
11
  import * as os from "node:os";
@@ -179,16 +179,17 @@ function findLatestPlanByMtime(projectRoot: string): string | null {
179
179
  const rawArgs = process.argv.slice(2);
180
180
 
181
181
  if (rawArgs.length === 0) {
182
- eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
182
+ eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] plan | --file <path> | <text...>");
183
183
  process.exit(1);
184
184
  }
185
185
 
186
- // Extract --model, --sandbox, and --context flags before mode dispatch
186
+ // Extract flags before mode dispatch
187
187
  let modelFlag: string | undefined;
188
188
  let sandboxFlag: CodexSandbox | undefined;
189
189
  let contextFlag: string | undefined;
190
+ let extraPrompt: string | undefined;
190
191
  let yolo = true;
191
- let capture = false;
192
+ let watch = true;
192
193
  const args: string[] = [];
193
194
 
194
195
  for (let i = 0; i < rawArgs.length; i++) {
@@ -203,19 +204,24 @@ for (let i = 0; i < rawArgs.length; i++) {
203
204
  sandboxFlag = val;
204
205
  } else if (rawArgs[i] === "--context" && i + 1 < rawArgs.length) {
205
206
  contextFlag = rawArgs[++i];
207
+ } else if (rawArgs[i] === "--prompt" && i + 1 < rawArgs.length) {
208
+ extraPrompt = rawArgs[++i];
209
+ } else if (rawArgs[i] === "--prompt") {
210
+ eprint("Error: --prompt requires a text argument.");
211
+ process.exit(1);
206
212
  } else if (rawArgs[i] === "--yolo") {
207
213
  yolo = true;
208
214
  } else if (rawArgs[i] === "--no-yolo") {
209
215
  yolo = false;
210
- } else if (rawArgs[i] === "--capture") {
211
- capture = true;
216
+ } else if (rawArgs[i] === "--no-watch") {
217
+ watch = false;
212
218
  } else {
213
219
  args.push(rawArgs[i]);
214
220
  }
215
221
  }
216
222
 
217
223
  if (args.length === 0) {
218
- eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
224
+ eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] plan | --file <path> | <text...>");
219
225
  process.exit(1);
220
226
  }
221
227
 
@@ -303,6 +309,22 @@ if (ctx && promptPath) {
303
309
  }
304
310
  }
305
311
 
312
+ if (extraPrompt && promptPath) {
313
+ try {
314
+ const base = fs.readFileSync(promptPath, "utf-8");
315
+ const combined = `${base}\n\n---\n\n## Additional Instructions\n\n${extraPrompt}`;
316
+ const extraPromptPath = path.join(os.tmpdir(), `codex-extra-prompt-${Date.now()}.md`);
317
+ fs.writeFileSync(extraPromptPath, combined, "utf-8");
318
+ if (tempFile) {
319
+ try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
320
+ }
321
+ promptPath = extraPromptPath;
322
+ tempFile = extraPromptPath;
323
+ } catch {
324
+ logWarn("codex-skill", "Extra prompt append failed, continuing without it");
325
+ }
326
+ }
327
+
306
328
  // ---------------------------------------------------------------------------
307
329
  // Pre-flight: tmux required
308
330
  // ---------------------------------------------------------------------------
@@ -324,13 +346,14 @@ if (yolo) console.log("Mode: YOLO (bypass approvals and sandbox)");
324
346
  if (sandboxFlag) console.log(`Sandbox: ${sandboxFlag}`);
325
347
  if (resolvedModel) console.log(`Model: ${resolvedModel}${modelFlag !== resolvedModel ? ` (from "${modelFlag}")` : ""}`);
326
348
 
327
- logDebug("codex-skill", `Launching: model=${resolvedModel ?? "default"}, sandbox=${sandboxFlag ?? "default"}, yolo=${yolo}, source=${args[0]}, bytes=${promptPath ? fs.statSync(promptPath).size : 0}`);
349
+ logDebug("codex-skill", `Launching: model=${resolvedModel ?? "default"}, sandbox=${sandboxFlag ?? "default"}, yolo=${yolo}, extraPrompt=${!!extraPrompt}, source=${args[0]}, bytes=${promptPath ? fs.statSync(promptPath).size : 0}`);
328
350
  const launchStartedAtMs = Date.now();
329
351
 
330
352
  const result = await launchDriverInTmuxOrFallback({
331
353
  toolName: "codex",
332
354
  mode: "repl",
333
355
  args: codexArgs,
356
+ splitFlag: "auto",
334
357
  promptPath,
335
358
  sendPromptInRepl: true,
336
359
  allowExecFallback: false,
@@ -363,21 +386,32 @@ if (result.paneId) {
363
386
  console.log("Codex launched in tmux pane.");
364
387
  }
365
388
 
366
- if (capture && result.paneId) {
389
+ if (watch && result.paneId) {
367
390
  try {
391
+ const {
392
+ SUMMARY_UNAVAILABLE_MESSAGE,
393
+ summarizeFromSessionFileFallback,
394
+ summarizeViaResume,
395
+ summarizeViaSessionFileSpark,
396
+ waitForPaneClose,
397
+ } = await import("../lib/codex-watcher.js");
398
+
368
399
  const sessionInfo = await waitForCaptureSession(projectRoot, launchStartedAtMs);
369
- if (sessionInfo) {
370
- console.log(`CODEX_CAPTURE_PANE=${result.paneId}`);
371
- console.log(`CODEX_CAPTURE_SESSION_ID=${sessionInfo.sessionId}`);
372
- console.log(`CODEX_CAPTURE_SESSION_FILE=${sessionInfo.sessionFile}`);
373
- } else {
374
- logWarn(
375
- "codex-skill",
376
- `Capture session discovery failed for pane ${result.paneId} in ${SESSION_DISCOVERY_TIMEOUT_MS}ms`,
377
- );
378
- }
400
+ await waitForPaneClose(result.paneId);
401
+
402
+ const sessionFile = sessionInfo?.sessionFile ?? "";
403
+ const sessionId = sessionInfo?.sessionId ?? "";
404
+ const summary = summarizeViaSessionFileSpark(sessionFile)
405
+ ?? (sessionId ? await summarizeViaResume(sessionId) : null)
406
+ ?? summarizeFromSessionFileFallback(sessionFile)
407
+ ?? SUMMARY_UNAVAILABLE_MESSAGE;
408
+
409
+ console.log("\n--- Codex Session Summary ---");
410
+ console.log(summary);
379
411
  } catch (error) {
380
- logWarn("codex-skill", `Capture session discovery threw for ${result.paneId}: ${String(error)}`);
412
+ logWarn("codex-skill", `Watch flow failed for ${result.paneId}: ${String(error)}`);
413
+ console.log("\n--- Codex Session Summary ---");
414
+ console.log("Codex session completed. Summary unavailable.");
381
415
  }
382
416
  }
383
417
 
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Thin CLI wrapper over lib/codex-watcher.ts for backward compatibility.
4
+ *
5
+ * Usage:
6
+ * bun watch-codex.ts <pane_id> <session_id> <session_file>
7
+ *
8
+ * Prefer using launch-codex.ts directly (watch is built-in by default).
9
+ */
10
+ import {
11
+ SUMMARY_UNAVAILABLE_MESSAGE,
12
+ summarizeFromSessionFileFallback,
13
+ summarizeViaResume,
14
+ summarizeViaSessionFileSpark,
15
+ waitForPaneClose,
16
+ } from "../lib/codex-watcher.js";
17
+
18
+ async function main(): Promise<void> {
19
+ const [paneId, sessionId, sessionFile] = process.argv.slice(2);
20
+ if (!paneId) {
21
+ console.log(SUMMARY_UNAVAILABLE_MESSAGE);
22
+ return;
23
+ }
24
+
25
+ await waitForPaneClose(paneId);
26
+
27
+ const sf = sessionFile ?? "";
28
+ const sid = sessionId ?? "";
29
+ const summary =
30
+ summarizeViaSessionFileSpark(sf) ??
31
+ (sid ? await summarizeViaResume(sid) : null) ??
32
+ summarizeFromSessionFileFallback(sf) ??
33
+ SUMMARY_UNAVAILABLE_MESSAGE;
34
+
35
+ console.log("\n--- Codex Session Summary ---");
36
+ console.log(summary);
37
+ }
38
+
39
+ main().catch((error) => {
40
+ console.error(`watch-codex error: ${String(error)}`);
41
+ console.log(SUMMARY_UNAVAILABLE_MESSAGE);
42
+ });
@@ -434,5 +434,5 @@
434
434
  ]
435
435
  }
436
436
  },
437
- "version": "0.15.1"
437
+ "version": "0.15.2"
438
438
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiwcli",
3
3
  "description": "AI Workflow CLI - Command-line interface for AI-powered workflows",
4
- "version": "0.15.1",
4
+ "version": "0.15.2",
5
5
  "author": "jofu-tofu",
6
6
  "bin": {
7
7
  "aiw": "bin/run.js"
@@ -1,30 +0,0 @@
1
- # /codex
2
-
3
- | Command | Description |
4
- |---|---|
5
- | `bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] plan` | Launch Codex REPL with active plan. |
6
- | `bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] --file <path>` | Launch Codex REPL with file contents. |
7
- | `bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] <inline text...>` | Launch Codex REPL with inline prompt. |
8
-
9
- - `--model`: model tier (`fast`/`standard`/`smart`) resolves to `gpt-5.3-codex-spark` / `gpt-5.3-codex`, or any explicit Codex model id. Aliases: `spark`, `codex`, `gpt`.
10
- - `--sandbox`: `read-only`, `workspace-write`, or `danger-full-access`. Default is `danger-full-access` for implementation handoffs.
11
- - YOLO mode is **on by default** — bypasses all approvals and sandbox (`--dangerously-bypass-approvals-and-sandbox`). Use `--no-yolo` to disable.
12
- - `--capture`: best-effort session capture. If setup succeeds, launch output includes:
13
- - `CODEX_CAPTURE_PANE=<pane_id>`
14
- - `CODEX_CAPTURE_SESSION_ID=<session_id>`
15
- - `CODEX_CAPTURE_SESSION_FILE=<path>`
16
-
17
- If launch output includes `CODEX_CAPTURE_PANE` and `CODEX_CAPTURE_SESSION_ID`:
18
-
19
- 1. Parse pane/session metadata from stdout.
20
- 2. Start a background watcher:
21
- ```bash
22
- bun .aiwcli/_shared/skills/prompt-codex/scripts/watch-codex.ts <pane_id> <session_id> <session_file>
23
- ```
24
- Use `Bash` with `run_in_background: true`.
25
- 3. Tell the user: `Codex is running in the tmux pane. I'll receive a summary when you exit.`
26
- 4. Continue with other work; the background task output will arrive as a notification.
27
-
28
- Watcher behavior:
29
- - Primary: summarize from `CODEX_CAPTURE_SESSION_FILE` with Spark.
30
- - Fallback: use `codex exec resume <session_id>` if transcript summarization fails.
@@ -1,71 +0,0 @@
1
- # Prompt Codex Skill
2
-
3
- Launch Codex CLI in a tmux pane and inject a prompt into its REPL.
4
-
5
- ## Directory Structure
6
-
7
- ```
8
- prompt-codex/
9
- ├── CLAUDE.md ← This file
10
- └── scripts/
11
- ├── launch-codex.ts ← CLI entry point
12
- └── watch-codex.ts ← Capture watcher and summarizer
13
- ```
14
-
15
- ## Script: launch-codex.ts
16
-
17
- **Usage:**
18
- ```bash
19
- bun .aiwcli/_shared/skills/prompt-codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--full-auto] plan
20
- bun .aiwcli/_shared/skills/prompt-codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--full-auto] --file <path>
21
- bun .aiwcli/_shared/skills/prompt-codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--full-auto] <inline text...>
22
- bun .aiwcli/_shared/skills/prompt-codex/scripts/launch-codex.ts [--model <tier|id>] [--sandbox <sandbox-mode>] [--full-auto] [--capture] <mode>
23
- ```
24
-
25
- **Args:**
26
- - `plan` — discover active plan via context system, inject into Codex REPL
27
- - `--file <path>` — inject file contents into Codex REPL
28
- - `<text...>` — join remaining args as inline prompt, write to temp file, inject
29
- - `--model <alias|tier|id>` — Aliases: `spark` → `gpt-5.3-codex-spark`, `codex` → `gpt-5.3-codex`, `gpt` → `gpt-5.2`. Tiers: `fast`/`standard`/`smart` (resolved via `resolveModelForProvider()`). Or any full model ID. Aliases are checked first (local `CODEX_ALIASES` constant in `launch-codex.ts`), then tiers, then pass-through. Omitted = Codex default.
30
- - `--sandbox <mode>` — `read-only`, `workspace-write`, or `danger-full-access`. Default is `danger-full-access` for implementation handoffs.
31
- - `--no-yolo` — Disable YOLO mode (on by default). YOLO maps to Codex CLI's `--dangerously-bypass-approvals-and-sandbox`. Use `--no-yolo` to restore normal approval prompts.
32
- - `--capture` — Best-effort session capture. On success, prints:
33
- - `CODEX_CAPTURE_PANE=<pane_id>`
34
- - `CODEX_CAPTURE_SESSION_ID=<session_id>`
35
- - `CODEX_CAPTURE_SESSION_FILE=<session_file>`
36
- These are consumed by the skill prompt to run `watch-codex.ts` as a background task.
37
-
38
- **Plan discovery order:**
39
- 1. `CLAUDE_SESSION_ID` env → `getContextBySessionId()` → `findLatestPlan(contextId)`
40
- 2. Fallback: scan `_output/contexts/*/plans/*.md` by mtime (inline, no `_cc-native` import)
41
-
42
- **Dependencies (all from `_shared/lib-ts/`):**
43
- - `base/tmux-driver.ts` — `launchDriverInTmuxOrFallback()`, `getTmuxAvailability()`
44
- - `base/cli-args.ts` — `resolveCodexModel()`, `codexReplSpec()`, `buildCliInvocation()`, `isCodexSandbox()`
45
- - `base/logger.ts` — `logDebug()`, `logWarn()` (injection diagnostics)
46
- - `context/context-store.ts` — `getContextBySessionId()`
47
- - `context/context-formatter.ts` — `buildExternalAgentContext()` (orientation header for Codex)
48
- - `context/plan-manager.ts` — `findLatestPlan()`
49
-
50
- **Design decisions:**
51
- - Always creates a new tmux pane (no pane reuse/tracking)
52
- - No exec fallback — REPL mode requires tmux
53
- - `_shared` only — never imports from `_cc-native`
54
- - Temp file cleanup after injection confirmed
55
-
56
- ## Script: watch-codex.ts
57
-
58
- **Usage:**
59
- ```bash
60
- bun .aiwcli/_shared/skills/prompt-codex/scripts/watch-codex.ts <pane_id> <session_id> <session_file>
61
- ```
62
-
63
- **Behavior:**
64
- - Polls tmux until `<pane_id>` closes
65
- - Primary: parses `<session_file>` transcript and summarizes via Spark (`inference()` + `CODEX_MODELS.spark`)
66
- - Fallback: runs `codex exec resume <session_id>` if transcript summarization fails
67
- - Final fallback: emits concise transcript lines directly from `<session_file>`
68
-
69
- **Resilience policy:**
70
- - Capture path is best-effort and never blocks Codex launch
71
- - Watcher exits cleanly on poll/summary/parse failures with fallback text