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.
- package/dist/commands/launch.d.ts +1 -0
- package/dist/commands/launch.js +24 -8
- package/dist/templates/_shared/.claude/skills/codex/SKILL.md +19 -26
- package/dist/templates/_shared/.codex/workflows/codex.md +11 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/tmux.ts +23 -49
- package/dist/templates/_shared/lib-ts/base/launchers/tmux-launcher.ts +173 -0
- package/dist/templates/_shared/lib-ts/base/launchers/window-launcher.ts +93 -0
- package/dist/templates/_shared/lib-ts/base/launchers/wt-launcher.ts +64 -0
- package/dist/templates/_shared/lib-ts/base/pane-launcher.ts +55 -0
- package/dist/templates/_shared/lib-ts/base/sentinel-ipc.ts +87 -0
- package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +160 -200
- package/dist/templates/_shared/lib-ts/base/tmux-pane-placement.ts +78 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +0 -3
- package/dist/templates/_shared/scripts/resolve-run.ts +1 -1
- package/dist/templates/_shared/skills/codex/CLAUDE.md +70 -0
- package/dist/templates/_shared/skills/codex/SKILL.md +71 -0
- package/dist/templates/_shared/skills/{prompt-codex/scripts/watch-codex.ts → codex/lib/codex-watcher.ts} +78 -63
- package/dist/templates/_shared/skills/{prompt-codex → codex}/scripts/launch-codex.ts +106 -61
- package/dist/templates/_shared/skills/codex/scripts/watch-codex.ts +42 -0
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
- package/dist/templates/_shared/.claude/skills/codex/prompt.md +0 -30
- package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +0 -71
|
@@ -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
|
|
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
|
|
140
|
-
const
|
|
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
|
-
|
|
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
|
|
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] [--
|
|
7
|
-
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--
|
|
8
|
-
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--
|
|
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
|
-
|
|
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] [--
|
|
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
|
|
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
|
|
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] === "--
|
|
211
|
-
|
|
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] [--
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
346
|
-
eprint(`
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
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
|
|
388
|
+
console.log(`Codex launched in ${backendLabel}: ${result.paneId}`);
|
|
362
389
|
} else {
|
|
363
|
-
console.log(
|
|
390
|
+
console.log(`Codex launched in ${backendLabel}.`);
|
|
364
391
|
}
|
|
365
392
|
|
|
366
|
-
if (
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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", `
|
|
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
|
}
|