aiwcli 0.15.1 → 0.15.3

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.
Files changed (23) hide show
  1. package/dist/commands/launch.d.ts +1 -0
  2. package/dist/commands/launch.js +24 -8
  3. package/dist/templates/_shared/.claude/skills/codex/SKILL.md +19 -26
  4. package/dist/templates/_shared/.codex/workflows/codex.md +11 -0
  5. package/dist/templates/_shared/lib-ts/agent-exec/backends/tmux.ts +23 -49
  6. package/dist/templates/_shared/lib-ts/base/launchers/tmux-launcher.ts +173 -0
  7. package/dist/templates/_shared/lib-ts/base/launchers/window-launcher.ts +93 -0
  8. package/dist/templates/_shared/lib-ts/base/launchers/wt-launcher.ts +64 -0
  9. package/dist/templates/_shared/lib-ts/base/pane-launcher.ts +55 -0
  10. package/dist/templates/_shared/lib-ts/base/sentinel-ipc.ts +87 -0
  11. package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +160 -200
  12. package/dist/templates/_shared/lib-ts/base/tmux-pane-placement.ts +78 -0
  13. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +0 -3
  14. package/dist/templates/_shared/scripts/resolve-run.ts +1 -1
  15. package/dist/templates/_shared/skills/codex/CLAUDE.md +70 -0
  16. package/dist/templates/_shared/skills/codex/SKILL.md +71 -0
  17. package/dist/templates/_shared/skills/{prompt-codex/scripts/watch-codex.ts → codex/lib/codex-watcher.ts} +78 -63
  18. package/dist/templates/_shared/skills/{prompt-codex → codex}/scripts/launch-codex.ts +106 -61
  19. package/dist/templates/_shared/skills/codex/scripts/watch-codex.ts +42 -0
  20. package/oclif.manifest.json +2 -2
  21. package/package.json +1 -1
  22. package/dist/templates/_shared/.claude/skills/codex/prompt.md +0 -30
  23. package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +0 -71
@@ -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,70 @@
1
+ # Codex Skill
2
+
3
+ Launch Codex CLI in a visible pane (tmux on Unix, Windows Terminal/window fallback on native Windows) and pass the prompt at process start.
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, send it as startup prompt to Codex
27
+ - `--file <path>` — read file contents and send as startup prompt
28
+ - `<text...>` — join remaining args as inline prompt
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.
30
+ - `--sandbox <mode>` — `read-only`, `workspace-write`, or `danger-full-access`. Default is `danger-full-access`.
31
+ - `--prompt <text>` — append extra instructions under `## Additional Instructions`.
32
+ - `--no-yolo` — Disable YOLO mode (`--dangerously-bypass-approvals-and-sandbox`).
33
+ - `--no-watch` — Disable watch/summarize mode.
34
+
35
+ **Plan discovery order:**
36
+ 1. `CLAUDE_SESSION_ID` env → `getContextBySessionId()` → `findLatestPlan(contextId)`
37
+ 2. Fallback: scan `_output/contexts/*/plans/*.md` by mtime
38
+
39
+ **Dependencies (all from `_shared/lib-ts/`):**
40
+ - `base/tmux-driver.ts` — pane launcher orchestration with cross-platform fallback
41
+ - `base/pane-launcher.ts` + `base/launchers/*` — tmux / wt / window launchers
42
+ - `base/cli-args.ts` — model/sandbox/yolo CLI arg generation
43
+ - `base/sentinel-ipc.ts` — completion sentinel file lifecycle
44
+ - `context/*` — context lookup, formatting, plan discovery
45
+
46
+ **Watch behavior (single entry point):**
47
+ - Watch is enabled by default.
48
+ - `launch-codex.ts` launches Codex, waits for completion (pane close or sentinel), and prints a summary.
49
+ - Summary cascade:
50
+ 1. Spark transcript summary from session file
51
+ 2. `codex exec resume <session_id>` summary
52
+ 3. Transcript-line fallback
53
+ 4. Static `Summary unavailable` message
54
+
55
+ **Design decisions:**
56
+ - Prompt is delivered at launch time (no tmux buffer paste/capture workflow)
57
+ - Pane backend detection order: tmux (in-session) → Windows Terminal split pane → Windows new window → non-interactive exec fallback
58
+ - `_shared` only — never imports from `_cc-native`
59
+ - Watch path is best-effort and does not change launch success semantics
60
+
61
+ ## Library: lib/codex-watcher.ts
62
+
63
+ Reusable side-effect-free watch/summarize functions used by launch flow:
64
+ - `waitForPaneClose(target, timeoutMs?)` where `target` can be tmux pane id or `{ backend, paneId, sentinelPath }`
65
+ - `summarizeViaSessionFileSpark(sessionFile)`
66
+ - `summarizeViaResume(sessionId)`
67
+ - `summarizeFromSessionFileFallback(sessionFile)`
68
+ - `collectTranscriptLines(sessionFile)`
69
+
70
+ Constants and helper utilities are exported for reuse and testing (`POLL_INTERVAL_MS`, `SUMMARY_UNAVAILABLE_MESSAGE`, `normalizeText`, `looksLikeBadSummary`, etc.).
@@ -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";
@@ -7,18 +5,26 @@ import * as path from "node:path";
7
5
  import { inference } from "../../../lib-ts/base/inference.js";
8
6
  import { logDebug, logWarn } from "../../../lib-ts/base/logger.js";
9
7
  import { CODEX_MODELS } from "../../../lib-ts/base/models.js";
8
+ import type { PaneBackend } from "../../../lib-ts/base/pane-launcher.js";
10
9
  import { execFileAsync } from "../../../lib-ts/base/subprocess-utils.js";
11
10
  import { getTmuxAvailability } from "../../../lib-ts/base/tmux-driver.js";
12
11
 
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.";
12
+ export const POLL_INTERVAL_MS = 2000;
13
+ export const POLL_TIMEOUT_MS = 3000;
14
+ export const SUMMARY_TIMEOUT_SEC = 8;
15
+ export const RESUME_TIMEOUT_MS = 45000;
16
+ export const MAX_TRANSCRIPT_LINES = 220;
17
+ export const MAX_LINE_LENGTH = 500;
18
+ export const WAIT_TIMEOUT_MS_DEFAULT = 14_400_000;
19
+ export const SUMMARY_UNAVAILABLE_MESSAGE = "Codex session completed. Summary unavailable.";
20
+
21
+ export interface PaneWatchTarget {
22
+ backend?: PaneBackend;
23
+ paneId?: string;
24
+ sentinelPath?: string;
25
+ }
20
26
 
21
- const TRANSCRIPT_SUMMARY_PROMPT = `Summarize this Codex session transcript excerpt.
27
+ export const TRANSCRIPT_SUMMARY_PROMPT = `Summarize this Codex session transcript excerpt.
22
28
  Return 3-5 concise bullet points.
23
29
  Focus on:
24
30
  - what was accomplished
@@ -28,7 +34,7 @@ Do not ask follow-up questions.
28
34
  Do not request additional input.
29
35
  If information is partial, provide best-effort summary from available text.`;
30
36
 
31
- const RESUME_SUMMARY_PROMPT = `Summarize the previous Codex session in 3-5 concise bullet points.
37
+ export const RESUME_SUMMARY_PROMPT = `Summarize the previous Codex session in 3-5 concise bullet points.
32
38
  Focus on:
33
39
  - what was accomplished
34
40
  - files changed
@@ -37,11 +43,11 @@ Do not ask follow-up questions.
37
43
  Do not request additional input.
38
44
  If the prior session was brief, still provide a best-effort summary.`;
39
45
 
40
- function sleep(ms: number): Promise<void> {
46
+ export function sleep(ms: number): Promise<void> {
41
47
  return new Promise((resolve) => setTimeout(resolve, ms));
42
48
  }
43
49
 
44
- function safeCleanup(filePath: string): void {
50
+ export function safeCleanup(filePath: string): void {
45
51
  try {
46
52
  if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
47
53
  } catch {
@@ -49,7 +55,7 @@ function safeCleanup(filePath: string): void {
49
55
  }
50
56
  }
51
57
 
52
- function readTextIfExists(filePath: string): string {
58
+ export function readTextIfExists(filePath: string): string {
53
59
  try {
54
60
  if (!filePath || !fs.existsSync(filePath)) return "";
55
61
  return fs.readFileSync(filePath, "utf-8").trim();
@@ -58,7 +64,7 @@ function readTextIfExists(filePath: string): string {
58
64
  }
59
65
  }
60
66
 
61
- function normalizeText(text: string): string {
67
+ export function normalizeText(text: string): string {
62
68
  return text
63
69
  .replace(/\r/g, "")
64
70
  .replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "")
@@ -66,7 +72,7 @@ function normalizeText(text: string): string {
66
72
  .trim();
67
73
  }
68
74
 
69
- function getMessageContentText(content: unknown): string {
75
+ export function getMessageContentText(content: unknown): string {
70
76
  if (!Array.isArray(content)) return "";
71
77
  return content
72
78
  .map((entry: any) => {
@@ -77,7 +83,7 @@ function getMessageContentText(content: unknown): string {
77
83
  .join("\n");
78
84
  }
79
85
 
80
- function collectTranscriptLines(sessionFile: string): string[] {
86
+ export function collectTranscriptLines(sessionFile: string): string[] {
81
87
  if (!sessionFile || !fs.existsSync(sessionFile)) return [];
82
88
 
83
89
  const out: string[] = [];
@@ -126,7 +132,7 @@ function collectTranscriptLines(sessionFile: string): string[] {
126
132
  return out.slice(-MAX_TRANSCRIPT_LINES);
127
133
  }
128
134
 
129
- function looksLikeBadSummary(output: string): boolean {
135
+ export function looksLikeBadSummary(output: string): boolean {
130
136
  const normalized = output.toLowerCase();
131
137
  return (
132
138
  normalized.includes("don't see") ||
@@ -136,14 +142,61 @@ function looksLikeBadSummary(output: string): boolean {
136
142
  );
137
143
  }
138
144
 
139
- async function waitForPaneClose(paneId: string): Promise<void> {
140
- const tmux = getTmuxAvailability();
145
+ async function waitForSentinelClose(sentinelPath: string, timeoutMs: number): Promise<void> {
146
+ const deadline = Date.now() + timeoutMs;
147
+ while (true) {
148
+ if (fs.existsSync(sentinelPath)) return;
149
+ if (Date.now() >= deadline) {
150
+ logDebug("codex-capture", `watch timeout reached waiting for sentinel ${sentinelPath}`);
151
+ return;
152
+ }
153
+
154
+ const remainingMs = deadline - Date.now();
155
+ await sleep(Math.max(0, Math.min(POLL_INTERVAL_MS, remainingMs)));
156
+ }
157
+ }
158
+
159
+ function normalizeWatchTarget(target: string | PaneWatchTarget): PaneWatchTarget {
160
+ if (typeof target === "string") {
161
+ return { backend: "tmux", paneId: target };
162
+ }
163
+ return target;
164
+ }
165
+
166
+ export async function waitForPaneClose(
167
+ target: string | PaneWatchTarget,
168
+ timeoutMs = WAIT_TIMEOUT_MS_DEFAULT,
169
+ ): Promise<void> {
170
+ const watch = normalizeWatchTarget(target);
171
+
172
+ if (watch.sentinelPath) {
173
+ await waitForSentinelClose(watch.sentinelPath, timeoutMs);
174
+ return;
175
+ }
176
+
177
+ const backend = watch.backend ?? "tmux";
178
+ const paneId = watch.paneId ?? "";
179
+
180
+ if (backend !== "tmux") {
181
+ logDebug("codex-capture", `No pane watcher for backend=${backend}; continuing without wait`);
182
+ return;
183
+ }
184
+
185
+ if (!paneId) return;
186
+
187
+ const tmux = getTmuxAvailability({ requireSessionEnv: false });
141
188
  if (!tmux.available || !tmux.tmuxPath) {
142
189
  logWarn("codex-capture", `tmux unavailable while watching pane ${paneId}: ${tmux.reason ?? "unknown reason"}`);
143
190
  return;
144
191
  }
145
192
 
193
+ const deadline = Date.now() + timeoutMs;
146
194
  while (true) {
195
+ if (Date.now() >= deadline) {
196
+ logDebug("codex-capture", `watch timeout reached for pane ${paneId} after ${timeoutMs}ms`);
197
+ return;
198
+ }
199
+
147
200
  const result = await execFileAsync(tmux.tmuxPath, ["list-panes", "-a", "-F", "#{pane_id}"], {
148
201
  timeout: POLL_TIMEOUT_MS,
149
202
  });
@@ -156,11 +209,12 @@ async function waitForPaneClose(paneId: string): Promise<void> {
156
209
  const activePaneIds = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
157
210
  if (!activePaneIds.includes(paneId)) return;
158
211
 
159
- await sleep(POLL_INTERVAL_MS);
212
+ const remainingMs = deadline - Date.now();
213
+ await sleep(Math.max(0, Math.min(POLL_INTERVAL_MS, remainingMs)));
160
214
  }
161
215
  }
162
216
 
163
- function summarizeViaSessionFileSpark(sessionFile: string): string | null {
217
+ export function summarizeViaSessionFileSpark(sessionFile: string): string | null {
164
218
  const transcriptLines = collectTranscriptLines(sessionFile);
165
219
  if (transcriptLines.length === 0) return null;
166
220
 
@@ -184,7 +238,7 @@ function summarizeViaSessionFileSpark(sessionFile: string): string | null {
184
238
  return null;
185
239
  }
186
240
 
187
- async function summarizeViaResume(sessionId: string): Promise<string | null> {
241
+ export async function summarizeViaResume(sessionId: string): Promise<string | null> {
188
242
  const outputFile = path.join(os.tmpdir(), `codex-resume-summary-${Date.now()}-${process.pid}.txt`);
189
243
 
190
244
  const result = await execFileAsync(
@@ -211,47 +265,8 @@ async function summarizeViaResume(sessionId: string): Promise<string | null> {
211
265
  return null;
212
266
  }
213
267
 
214
- function summarizeFromSessionFileFallback(sessionFile: string): string | null {
268
+ export function summarizeFromSessionFileFallback(sessionFile: string): string | null {
215
269
  const lines = collectTranscriptLines(sessionFile).slice(-12);
216
270
  if (lines.length === 0) return null;
217
271
  return `Codex session completed. Transcript fallback:\n- ${lines.join("\n- ")}`;
218
272
  }
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
- });
@@ -1,22 +1,20 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Launch Codex in a tmux pane and inject a prompt into its REPL.
3
+ * Launch Codex in a visible pane (tmux/wt/window) and pass the prompt at startup.
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";
12
12
  import * as path from "node:path";
13
13
 
14
- import {
15
- getTmuxAvailability,
16
- launchDriverInTmuxOrFallback,
17
- } from "../../../lib-ts/base/tmux-driver.js";
14
+ import { launchDriverInTmuxOrFallback } from "../../../lib-ts/base/tmux-driver.js";
15
+ import { cleanupSentinelPath } from "../../../lib-ts/base/sentinel-ipc.js";
18
16
  import { getProjectRoot } from "../../../lib-ts/base/constants.js";
19
- import { resolveCodexModel, codexReplSpec, buildCliInvocation, isCodexSandbox, type CodexSandbox } from "../../../lib-ts/base/cli-args.js";
17
+ import { resolveCodexModel, codexReplSpec, buildCliInvocation, isCodexSandbox, type CodexSandbox, type CliArgSpec } from "../../../lib-ts/base/cli-args.js";
20
18
  import { CODEX_MODELS } from "../../../lib-ts/base/models.js";
21
19
  import { logDebug, logWarn } from "../../../lib-ts/base/logger.js";
22
20
  import { displayPath } from "../../../lib-ts/base/utils.js";
@@ -179,16 +177,17 @@ function findLatestPlanByMtime(projectRoot: string): string | null {
179
177
  const rawArgs = process.argv.slice(2);
180
178
 
181
179
  if (rawArgs.length === 0) {
182
- eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
180
+ eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] plan | --file <path> | <text...>");
183
181
  process.exit(1);
184
182
  }
185
183
 
186
- // Extract --model, --sandbox, and --context flags before mode dispatch
184
+ // Extract flags before mode dispatch
187
185
  let modelFlag: string | undefined;
188
186
  let sandboxFlag: CodexSandbox | undefined;
189
187
  let contextFlag: string | undefined;
188
+ let extraPrompt: string | undefined;
190
189
  let yolo = true;
191
- let capture = false;
190
+ let watch = true;
192
191
  const args: string[] = [];
193
192
 
194
193
  for (let i = 0; i < rawArgs.length; i++) {
@@ -203,19 +202,24 @@ for (let i = 0; i < rawArgs.length; i++) {
203
202
  sandboxFlag = val;
204
203
  } else if (rawArgs[i] === "--context" && i + 1 < rawArgs.length) {
205
204
  contextFlag = rawArgs[++i];
205
+ } else if (rawArgs[i] === "--prompt" && i + 1 < rawArgs.length) {
206
+ extraPrompt = rawArgs[++i];
207
+ } else if (rawArgs[i] === "--prompt") {
208
+ eprint("Error: --prompt requires a text argument.");
209
+ process.exit(1);
206
210
  } else if (rawArgs[i] === "--yolo") {
207
211
  yolo = true;
208
212
  } else if (rawArgs[i] === "--no-yolo") {
209
213
  yolo = false;
210
- } else if (rawArgs[i] === "--capture") {
211
- capture = true;
214
+ } else if (rawArgs[i] === "--no-watch") {
215
+ watch = false;
212
216
  } else {
213
217
  args.push(rawArgs[i]);
214
218
  }
215
219
  }
216
220
 
217
221
  if (args.length === 0) {
218
- eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
222
+ eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--no-watch] [--context <id>] [--prompt <text>] plan | --file <path> | <text...>");
219
223
  process.exit(1);
220
224
  }
221
225
 
@@ -303,85 +307,126 @@ if (ctx && promptPath) {
303
307
  }
304
308
  }
305
309
 
306
- // ---------------------------------------------------------------------------
307
- // Pre-flight: tmux required
308
- // ---------------------------------------------------------------------------
309
-
310
- const tmux = getTmuxAvailability();
311
- if (!tmux.available) {
312
- eprint(`Error: tmux is required for Codex REPL mode. ${tmux.reason ?? ""}`);
313
- if (tempFile) try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
314
- process.exit(1);
310
+ if (extraPrompt && promptPath) {
311
+ try {
312
+ const base = fs.readFileSync(promptPath, "utf-8");
313
+ const combined = `${base}\n\n---\n\n## Additional Instructions\n\n${extraPrompt}`;
314
+ const extraPromptPath = path.join(os.tmpdir(), `codex-extra-prompt-${Date.now()}.md`);
315
+ fs.writeFileSync(extraPromptPath, combined, "utf-8");
316
+ if (tempFile) {
317
+ try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
318
+ }
319
+ promptPath = extraPromptPath;
320
+ tempFile = extraPromptPath;
321
+ } catch {
322
+ logWarn("codex-skill", "Extra prompt append failed, continuing without it");
323
+ }
315
324
  }
316
325
 
317
326
  // ---------------------------------------------------------------------------
318
- // Launch Codex REPL in tmux pane
327
+ // Launch Codex
319
328
  // ---------------------------------------------------------------------------
320
329
 
321
- // Build args via centralized CLI builder
322
330
  const codexArgs = buildCliInvocation(codexReplSpec(resolvedModel, sandboxFlag, yolo)).args;
323
331
  if (yolo) console.log("Mode: YOLO (bypass approvals and sandbox)");
324
332
  if (sandboxFlag) console.log(`Sandbox: ${sandboxFlag}`);
325
333
  if (resolvedModel) console.log(`Model: ${resolvedModel}${modelFlag !== resolvedModel ? ` (from "${modelFlag}")` : ""}`);
326
334
 
327
- logDebug("codex-skill", `Launching: model=${resolvedModel ?? "default"}, sandbox=${sandboxFlag ?? "default"}, yolo=${yolo}, source=${args[0]}, bytes=${promptPath ? fs.statSync(promptPath).size : 0}`);
328
- const launchStartedAtMs = Date.now();
335
+ logDebug("codex-skill", `Launching: model=${resolvedModel ?? "default"}, sandbox=${sandboxFlag ?? "default"}, yolo=${yolo}, extraPrompt=${!!extraPrompt}, source=${args[0]}, bytes=${promptPath ? fs.statSync(promptPath).size : 0}`);
329
336
 
337
+ const launchStartedAtMs = Date.now();
330
338
  const result = await launchDriverInTmuxOrFallback({
331
339
  toolName: "codex",
332
340
  mode: "repl",
333
341
  args: codexArgs,
334
- promptPath,
335
- sendPromptInRepl: true,
342
+ splitFlag: "auto",
343
+ promptPath: promptPath ?? undefined,
336
344
  allowExecFallback: false,
337
345
  });
338
346
 
339
- // Cleanup temp file after injection
340
- if (tempFile) {
341
- try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
342
- }
343
-
344
347
  if (!result.launched) {
345
- logWarn("codex-skill", `Launch failed: ${result.reason}`);
346
- eprint(`Error: Failed to launch Codex. ${result.reason ?? ""}`);
347
- process.exit(1);
348
- }
348
+ // Final fallback: non-interactive codex exec in current terminal.
349
+ eprint(`Note: Pane launch unavailable (${result.reason ?? "unknown"}). Using codex exec mode (non-interactive).`);
350
+
351
+ const execSpec: CliArgSpec = {
352
+ provider: "codex",
353
+ model: resolvedModel ?? CODEX_MODELS.codex,
354
+ mode: "structured",
355
+ sandbox: sandboxFlag ?? "danger-full-access",
356
+ };
357
+ const execInv = buildCliInvocation(execSpec);
358
+ const promptContent = promptPath ? fs.readFileSync(promptPath, "utf-8") : "";
359
+
360
+ if (tempFile) {
361
+ try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
362
+ }
349
363
 
350
- // Log injection diagnostics
351
- const diag = result.sendDiagnostics;
352
- if (diag) {
353
- if (diag.success) {
354
- logDebug("codex-skill", `Injection OK: promptWait=${diag.promptWaitMs}ms, retrySent=${diag.retrySent}`);
355
- } else {
356
- logWarn("codex-skill", `Injection failed at ${diag.failedAt}: wait=${diag.promptWaitMs}ms, stderr=${diag.tmuxStderr ?? "none"}, paneTail=${diag.paneTailOnTimeout ?? "none"}`);
364
+ const { execFileAsync } = await import("../../../lib-ts/base/subprocess-utils.js");
365
+ const execResult = await execFileAsync(execInv.cliName, execInv.args, {
366
+ input: promptContent,
367
+ env: { ...process.env, ...execInv.env },
368
+ shell: process.platform === "win32",
369
+ });
370
+
371
+ if (execResult.stdout) console.log(execResult.stdout);
372
+ if (execResult.exitCode !== 0) {
373
+ eprint(`Codex exec exited with code ${execResult.exitCode}`);
374
+ if (execResult.stderr) eprint(execResult.stderr);
375
+ process.exit(1);
357
376
  }
377
+
378
+ console.log("Codex exec completed (non-interactive mode).");
379
+ process.exit(0);
358
380
  }
359
381
 
382
+ if (tempFile) {
383
+ try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
384
+ }
385
+
386
+ const backendLabel = result.backend === "tmux" ? "tmux pane" : (result.backend === "wt" ? "Windows Terminal pane" : "window");
360
387
  if (result.paneId) {
361
- console.log(`Codex launched in tmux pane: ${result.paneId}`);
388
+ console.log(`Codex launched in ${backendLabel}: ${result.paneId}`);
362
389
  } else {
363
- console.log("Codex launched in tmux pane.");
390
+ console.log(`Codex launched in ${backendLabel}.`);
364
391
  }
365
392
 
366
- if (capture && result.paneId) {
393
+ if (watch && (result.paneId || result.sentinelPath)) {
367
394
  try {
395
+ const {
396
+ SUMMARY_UNAVAILABLE_MESSAGE,
397
+ summarizeFromSessionFileFallback,
398
+ summarizeViaResume,
399
+ summarizeViaSessionFileSpark,
400
+ waitForPaneClose,
401
+ } = await import("../lib/codex-watcher.js");
402
+
368
403
  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
- }
404
+ await waitForPaneClose({
405
+ backend: result.backend,
406
+ paneId: result.paneId,
407
+ sentinelPath: result.sentinelPath,
408
+ });
409
+
410
+ const sessionFile = sessionInfo?.sessionFile ?? "";
411
+ const sessionId = sessionInfo?.sessionId ?? "";
412
+ const summary = summarizeViaSessionFileSpark(sessionFile)
413
+ ?? (sessionId ? await summarizeViaResume(sessionId) : null)
414
+ ?? summarizeFromSessionFileFallback(sessionFile)
415
+ ?? SUMMARY_UNAVAILABLE_MESSAGE;
416
+
417
+ console.log("\n--- Codex Session Summary ---");
418
+ console.log(summary);
379
419
  } catch (error) {
380
- logWarn("codex-skill", `Capture session discovery threw for ${result.paneId}: ${String(error)}`);
420
+ logWarn("codex-skill", `Watch flow failed for ${result.paneId ?? result.backend}: ${String(error)}`);
421
+ console.log("\n--- Codex Session Summary ---");
422
+ console.log("Codex session completed. Summary unavailable.");
423
+ } finally {
424
+ cleanupSentinelPath(result.sentinelPath);
381
425
  }
426
+ } else {
427
+ cleanupSentinelPath(result.sentinelPath);
382
428
  }
383
429
 
384
430
  if (result.reason) {
385
- // Partial success (e.g., launched but prompt injection failed)
386
431
  eprint(`Warning: ${result.reason}`);
387
432
  }