aiwcli 0.14.0 → 0.15.1
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/templates/_shared/.claude/skills/codex/prompt.md +25 -5
- package/dist/templates/_shared/lib-ts/agent-exec/index.ts +2 -0
- package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
- package/dist/templates/_shared/lib-ts/base/cli-args.ts +4 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
- package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
- package/dist/templates/_shared/scripts/status_line.ts +36 -19
- package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +29 -4
- package/dist/templates/_shared/skills/prompt-codex/scripts/launch-codex.ts +140 -7
- package/dist/templates/_shared/skills/prompt-codex/scripts/watch-codex.ts +257 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +118 -42
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +54 -23
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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] [--context <id>] plan
|
|
7
|
-
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--context <id>] --file <path>
|
|
8
|
-
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--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] [--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...>
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
@@ -32,6 +32,10 @@ const CODEX_ALIASES: Record<string, string> = {
|
|
|
32
32
|
gpt: CODEX_MODELS.gpt,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
const SESSION_DISCOVERY_TIMEOUT_MS = 12000;
|
|
36
|
+
const SESSION_DISCOVERY_POLL_MS = 250;
|
|
37
|
+
const SESSION_MTIME_WINDOW_MS = 120000;
|
|
38
|
+
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
// Helpers
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
@@ -40,6 +44,107 @@ function eprint(...args: unknown[]): void {
|
|
|
40
44
|
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
function sleep(ms: number): Promise<void> {
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectSessionJsonlFiles(rootDir: string): string[] {
|
|
52
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
53
|
+
const stack: string[] = [rootDir];
|
|
54
|
+
const files: string[] = [];
|
|
55
|
+
|
|
56
|
+
while (stack.length > 0) {
|
|
57
|
+
const dir = stack.pop();
|
|
58
|
+
if (!dir) continue;
|
|
59
|
+
let entries: fs.Dirent[] = [];
|
|
60
|
+
try {
|
|
61
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const fullPath = path.join(dir, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
stack.push(fullPath);
|
|
70
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
71
|
+
files.push(fullPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function samePath(a: string, b: string): boolean {
|
|
80
|
+
const left = path.resolve(a);
|
|
81
|
+
const right = path.resolve(b);
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
return left === right;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readSessionMeta(sessionFile: string): { sessionId: string; cwd: string } | null {
|
|
89
|
+
try {
|
|
90
|
+
const raw = fs.readFileSync(sessionFile, "utf-8");
|
|
91
|
+
const firstLine = raw.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
92
|
+
if (!firstLine) return null;
|
|
93
|
+
const parsed = JSON.parse(firstLine);
|
|
94
|
+
if (parsed?.type !== "session_meta") return null;
|
|
95
|
+
const sessionId = parsed?.payload?.id;
|
|
96
|
+
const cwd = parsed?.payload?.cwd;
|
|
97
|
+
if (typeof sessionId !== "string" || typeof cwd !== "string") return null;
|
|
98
|
+
return { sessionId, cwd };
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findLatestSessionCandidate(
|
|
105
|
+
projectRoot: string,
|
|
106
|
+
launchStartedAtMs: number,
|
|
107
|
+
): { sessionId: string; sessionFile: string } | null {
|
|
108
|
+
const sessionsRoot = path.join(os.homedir(), ".codex", "sessions");
|
|
109
|
+
const files = collectSessionJsonlFiles(sessionsRoot);
|
|
110
|
+
if (files.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
const candidates: Array<{ sessionId: string; sessionFile: string; mtimeMs: number }> = [];
|
|
113
|
+
for (const sessionFile of files) {
|
|
114
|
+
let mtimeMs = 0;
|
|
115
|
+
try {
|
|
116
|
+
mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (mtimeMs < launchStartedAtMs - SESSION_MTIME_WINDOW_MS) continue;
|
|
121
|
+
|
|
122
|
+
const meta = readSessionMeta(sessionFile);
|
|
123
|
+
if (!meta) continue;
|
|
124
|
+
if (!samePath(meta.cwd, projectRoot)) continue;
|
|
125
|
+
|
|
126
|
+
candidates.push({ sessionId: meta.sessionId, sessionFile, mtimeMs });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (candidates.length === 0) return null;
|
|
130
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
131
|
+
const best = candidates[0];
|
|
132
|
+
return best ? { sessionId: best.sessionId, sessionFile: best.sessionFile } : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function waitForCaptureSession(
|
|
136
|
+
projectRoot: string,
|
|
137
|
+
launchStartedAtMs: number,
|
|
138
|
+
): Promise<{ sessionId: string; sessionFile: string } | null> {
|
|
139
|
+
const deadline = Date.now() + SESSION_DISCOVERY_TIMEOUT_MS;
|
|
140
|
+
while (Date.now() < deadline) {
|
|
141
|
+
const candidate = findLatestSessionCandidate(projectRoot, launchStartedAtMs);
|
|
142
|
+
if (candidate) return candidate;
|
|
143
|
+
await sleep(SESSION_DISCOVERY_POLL_MS);
|
|
144
|
+
}
|
|
145
|
+
return findLatestSessionCandidate(projectRoot, launchStartedAtMs);
|
|
146
|
+
}
|
|
147
|
+
|
|
43
148
|
/** Fallback plan discovery: scan all context plan dirs by mtime. */
|
|
44
149
|
function findLatestPlanByMtime(projectRoot: string): string | null {
|
|
45
150
|
const contextsDir = path.join(projectRoot, "_output", "contexts");
|
|
@@ -74,7 +179,7 @@ function findLatestPlanByMtime(projectRoot: string): string | null {
|
|
|
74
179
|
const rawArgs = process.argv.slice(2);
|
|
75
180
|
|
|
76
181
|
if (rawArgs.length === 0) {
|
|
77
|
-
eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--context <id>] plan | --file <path> | <text...>");
|
|
182
|
+
eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
|
|
78
183
|
process.exit(1);
|
|
79
184
|
}
|
|
80
185
|
|
|
@@ -82,6 +187,8 @@ if (rawArgs.length === 0) {
|
|
|
82
187
|
let modelFlag: string | undefined;
|
|
83
188
|
let sandboxFlag: CodexSandbox | undefined;
|
|
84
189
|
let contextFlag: string | undefined;
|
|
190
|
+
let yolo = true;
|
|
191
|
+
let capture = false;
|
|
85
192
|
const args: string[] = [];
|
|
86
193
|
|
|
87
194
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
@@ -96,13 +203,19 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
96
203
|
sandboxFlag = val;
|
|
97
204
|
} else if (rawArgs[i] === "--context" && i + 1 < rawArgs.length) {
|
|
98
205
|
contextFlag = rawArgs[++i];
|
|
206
|
+
} else if (rawArgs[i] === "--yolo") {
|
|
207
|
+
yolo = true;
|
|
208
|
+
} else if (rawArgs[i] === "--no-yolo") {
|
|
209
|
+
yolo = false;
|
|
210
|
+
} else if (rawArgs[i] === "--capture") {
|
|
211
|
+
capture = true;
|
|
99
212
|
} else {
|
|
100
213
|
args.push(rawArgs[i]);
|
|
101
214
|
}
|
|
102
215
|
}
|
|
103
216
|
|
|
104
217
|
if (args.length === 0) {
|
|
105
|
-
eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--context <id>] plan | --file <path> | <text...>");
|
|
218
|
+
eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
|
|
106
219
|
process.exit(1);
|
|
107
220
|
}
|
|
108
221
|
|
|
@@ -206,11 +319,13 @@ if (!tmux.available) {
|
|
|
206
319
|
// ---------------------------------------------------------------------------
|
|
207
320
|
|
|
208
321
|
// Build args via centralized CLI builder
|
|
209
|
-
const codexArgs = buildCliInvocation(codexReplSpec(resolvedModel, sandboxFlag)).args;
|
|
322
|
+
const codexArgs = buildCliInvocation(codexReplSpec(resolvedModel, sandboxFlag, yolo)).args;
|
|
323
|
+
if (yolo) console.log("Mode: YOLO (bypass approvals and sandbox)");
|
|
210
324
|
if (sandboxFlag) console.log(`Sandbox: ${sandboxFlag}`);
|
|
211
325
|
if (resolvedModel) console.log(`Model: ${resolvedModel}${modelFlag !== resolvedModel ? ` (from "${modelFlag}")` : ""}`);
|
|
212
326
|
|
|
213
|
-
logDebug("codex-skill", `Launching: model=${resolvedModel ?? "default"}, sandbox=${sandboxFlag ?? "default"}, source=${args[0]}, bytes=${promptPath ? fs.statSync(promptPath).size : 0}`);
|
|
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();
|
|
214
329
|
|
|
215
330
|
const result = await launchDriverInTmuxOrFallback({
|
|
216
331
|
toolName: "codex",
|
|
@@ -248,6 +363,24 @@ if (result.paneId) {
|
|
|
248
363
|
console.log("Codex launched in tmux pane.");
|
|
249
364
|
}
|
|
250
365
|
|
|
366
|
+
if (capture && result.paneId) {
|
|
367
|
+
try {
|
|
368
|
+
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
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
logWarn("codex-skill", `Capture session discovery threw for ${result.paneId}: ${String(error)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
251
384
|
if (result.reason) {
|
|
252
385
|
// Partial success (e.g., launched but prompt injection failed)
|
|
253
386
|
eprint(`Warning: ${result.reason}`);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { inference } from "../../../lib-ts/base/inference.js";
|
|
8
|
+
import { logDebug, logWarn } from "../../../lib-ts/base/logger.js";
|
|
9
|
+
import { CODEX_MODELS } from "../../../lib-ts/base/models.js";
|
|
10
|
+
import { execFileAsync } from "../../../lib-ts/base/subprocess-utils.js";
|
|
11
|
+
import { getTmuxAvailability } from "../../../lib-ts/base/tmux-driver.js";
|
|
12
|
+
|
|
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.";
|
|
20
|
+
|
|
21
|
+
const TRANSCRIPT_SUMMARY_PROMPT = `Summarize this Codex session transcript excerpt.
|
|
22
|
+
Return 3-5 concise bullet points.
|
|
23
|
+
Focus on:
|
|
24
|
+
- what was accomplished
|
|
25
|
+
- files changed
|
|
26
|
+
- errors or blockers
|
|
27
|
+
Do not ask follow-up questions.
|
|
28
|
+
Do not request additional input.
|
|
29
|
+
If information is partial, provide best-effort summary from available text.`;
|
|
30
|
+
|
|
31
|
+
const RESUME_SUMMARY_PROMPT = `Summarize the previous Codex session in 3-5 concise bullet points.
|
|
32
|
+
Focus on:
|
|
33
|
+
- what was accomplished
|
|
34
|
+
- files changed
|
|
35
|
+
- errors or blockers
|
|
36
|
+
Do not ask follow-up questions.
|
|
37
|
+
Do not request additional input.
|
|
38
|
+
If the prior session was brief, still provide a best-effort summary.`;
|
|
39
|
+
|
|
40
|
+
function sleep(ms: number): Promise<void> {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeCleanup(filePath: string): void {
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
47
|
+
} catch {
|
|
48
|
+
// Best-effort cleanup only.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readTextIfExists(filePath: string): string {
|
|
53
|
+
try {
|
|
54
|
+
if (!filePath || !fs.existsSync(filePath)) return "";
|
|
55
|
+
return fs.readFileSync(filePath, "utf-8").trim();
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeText(text: string): string {
|
|
62
|
+
return text
|
|
63
|
+
.replace(/\r/g, "")
|
|
64
|
+
.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "")
|
|
65
|
+
.replace(/\s+/g, " ")
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getMessageContentText(content: unknown): string {
|
|
70
|
+
if (!Array.isArray(content)) return "";
|
|
71
|
+
return content
|
|
72
|
+
.map((entry: any) => {
|
|
73
|
+
if (!entry || typeof entry !== "object") return "";
|
|
74
|
+
if (typeof entry.text !== "string") return "";
|
|
75
|
+
return entry.text;
|
|
76
|
+
})
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function collectTranscriptLines(sessionFile: string): string[] {
|
|
81
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) return [];
|
|
82
|
+
|
|
83
|
+
const out: string[] = [];
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(sessionFile, "utf-8");
|
|
88
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
89
|
+
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
let parsed: any;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(line);
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let role = "";
|
|
99
|
+
let text = "";
|
|
100
|
+
|
|
101
|
+
if (parsed?.type === "response_item" && parsed?.payload?.type === "message") {
|
|
102
|
+
role = parsed?.payload?.role === "assistant" ? "assistant" : (parsed?.payload?.role === "user" ? "user" : "");
|
|
103
|
+
text = getMessageContentText(parsed?.payload?.content);
|
|
104
|
+
} else if (parsed?.type === "item.completed" && parsed?.item?.type === "agent_message" && typeof parsed?.item?.text === "string") {
|
|
105
|
+
role = "assistant";
|
|
106
|
+
text = parsed.item.text;
|
|
107
|
+
} else if (parsed?.type === "event_msg" && parsed?.payload?.type === "agent_message" && typeof parsed?.payload?.message === "string") {
|
|
108
|
+
role = "assistant";
|
|
109
|
+
text = parsed.payload.message;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalized = normalizeText(text);
|
|
113
|
+
if (!role || !normalized) continue;
|
|
114
|
+
|
|
115
|
+
const truncated = normalized.length > MAX_LINE_LENGTH ? `${normalized.slice(0, MAX_LINE_LENGTH)}...` : normalized;
|
|
116
|
+
const tagged = `${role}: ${truncated}`;
|
|
117
|
+
if (seen.has(tagged)) continue;
|
|
118
|
+
|
|
119
|
+
seen.add(tagged);
|
|
120
|
+
out.push(tagged);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logWarn("codex-capture", `Session transcript parse failed: ${String(error)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return out.slice(-MAX_TRANSCRIPT_LINES);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function looksLikeBadSummary(output: string): boolean {
|
|
130
|
+
const normalized = output.toLowerCase();
|
|
131
|
+
return (
|
|
132
|
+
normalized.includes("don't see") ||
|
|
133
|
+
normalized.includes("no output") ||
|
|
134
|
+
normalized.includes("could you provide") ||
|
|
135
|
+
normalized.includes("paste")
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function waitForPaneClose(paneId: string): Promise<void> {
|
|
140
|
+
const tmux = getTmuxAvailability();
|
|
141
|
+
if (!tmux.available || !tmux.tmuxPath) {
|
|
142
|
+
logWarn("codex-capture", `tmux unavailable while watching pane ${paneId}: ${tmux.reason ?? "unknown reason"}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
while (true) {
|
|
147
|
+
const result = await execFileAsync(tmux.tmuxPath, ["list-panes", "-a", "-F", "#{pane_id}"], {
|
|
148
|
+
timeout: POLL_TIMEOUT_MS,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (result.exitCode !== 0) {
|
|
152
|
+
logDebug("codex-capture", `list-panes failed; assuming pane closed (${result.stderr.trim() || "no stderr"})`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const activePaneIds = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
157
|
+
if (!activePaneIds.includes(paneId)) return;
|
|
158
|
+
|
|
159
|
+
await sleep(POLL_INTERVAL_MS);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function summarizeViaSessionFileSpark(sessionFile: string): string | null {
|
|
164
|
+
const transcriptLines = collectTranscriptLines(sessionFile);
|
|
165
|
+
if (transcriptLines.length === 0) return null;
|
|
166
|
+
|
|
167
|
+
const transcript = transcriptLines.join("\n");
|
|
168
|
+
const result = inference(
|
|
169
|
+
TRANSCRIPT_SUMMARY_PROMPT,
|
|
170
|
+
`Session transcript excerpt:\n\n${transcript}`,
|
|
171
|
+
"fast",
|
|
172
|
+
SUMMARY_TIMEOUT_SEC,
|
|
173
|
+
{ model: CODEX_MODELS.spark },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (result.success && result.output && result.output.trim() && !looksLikeBadSummary(result.output)) {
|
|
177
|
+
return result.output.trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logWarn(
|
|
181
|
+
"codex-capture",
|
|
182
|
+
`Session-file Spark summary failed: ${result.error ?? "empty or low-signal output"}`,
|
|
183
|
+
);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function summarizeViaResume(sessionId: string): Promise<string | null> {
|
|
188
|
+
const outputFile = path.join(os.tmpdir(), `codex-resume-summary-${Date.now()}-${process.pid}.txt`);
|
|
189
|
+
|
|
190
|
+
const result = await execFileAsync(
|
|
191
|
+
"codex",
|
|
192
|
+
[
|
|
193
|
+
"exec",
|
|
194
|
+
"resume",
|
|
195
|
+
sessionId,
|
|
196
|
+
RESUME_SUMMARY_PROMPT,
|
|
197
|
+
"--json",
|
|
198
|
+
"--model",
|
|
199
|
+
CODEX_MODELS.spark,
|
|
200
|
+
"--output-last-message",
|
|
201
|
+
outputFile,
|
|
202
|
+
],
|
|
203
|
+
{ timeout: RESUME_TIMEOUT_MS },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const summary = readTextIfExists(outputFile);
|
|
207
|
+
safeCleanup(outputFile);
|
|
208
|
+
|
|
209
|
+
if (summary && !looksLikeBadSummary(summary)) return summary;
|
|
210
|
+
logWarn("codex-capture", `codex exec resume failed for ${sessionId}: exit=${result.exitCode}, stderr=${result.stderr.trim() || "none"}`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function summarizeFromSessionFileFallback(sessionFile: string): string | null {
|
|
215
|
+
const lines = collectTranscriptLines(sessionFile).slice(-12);
|
|
216
|
+
if (lines.length === 0) return null;
|
|
217
|
+
return `Codex session completed. Transcript fallback:\n- ${lines.join("\n- ")}`;
|
|
218
|
+
}
|
|
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,144 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* See cc-native-plan-review-spec.md §4.6
|
|
2
|
+
* CC-native wrapper around shared structured-output parsing.
|
|
3
|
+
* Keeps existing import surface stable for provider implementations.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import {
|
|
8
|
-
import { logDebug, logError, logWarn } from "../../_shared/lib-ts/base/logger.js";
|
|
6
|
+
import { parseStructuredOutput } from "../../_shared/lib-ts/agent-exec/structured-output.js";
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
|
-
* Parse
|
|
12
|
-
*
|
|
13
|
-
* Claude CLI can output in several formats:
|
|
14
|
-
* - Direct structured_output dict
|
|
15
|
-
* - Assistant message with StructuredOutput tool use
|
|
16
|
-
* - List of events with assistant messages
|
|
17
|
-
* - Raw text with embedded JSON (heuristic fallback)
|
|
18
|
-
*
|
|
19
|
-
* @param raw - Raw stdout from Claude CLI
|
|
20
|
-
* @param requireFields - Optional fields to validate in heuristic fallback
|
|
21
|
-
* @returns Parsed JSON dict or null if parsing failed
|
|
9
|
+
* Parse CLI JSON output into a structured object.
|
|
10
|
+
* Delegates to shared parser with cc-native logging tag.
|
|
22
11
|
*/
|
|
23
12
|
export function parseCliOutput(
|
|
24
13
|
raw: string,
|
|
25
14
|
requireFields?: string[],
|
|
26
15
|
): null | Record<string, unknown> {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const dict = result as Record<string, unknown>;
|
|
32
|
-
|
|
33
|
-
// Strategy 1: Direct structured_output key
|
|
34
|
-
if ("structured_output" in dict) {
|
|
35
|
-
logDebug("cli_parser", "Found structured_output in root dict");
|
|
36
|
-
return dict.structured_output as Record<string, unknown>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Strategy 1.5: Session result envelope (type: "result")
|
|
40
|
-
// When the model fails to call StructuredOutput, the CLI returns a
|
|
41
|
-
// session metadata object with type/subtype/duration_ms/usage/etc.
|
|
42
|
-
// but no structured_output key. Check for the `result` text field
|
|
43
|
-
// (model may have written review as text) and for error states.
|
|
44
|
-
if (dict.type === "result" || ("duration_ms" in dict && "session_id" in dict)) {
|
|
45
|
-
if (dict.is_error === true || (Array.isArray(dict.errors) && (dict.errors as unknown[]).length > 0)) {
|
|
46
|
-
logWarn("cli_parser", `CLI returned error result: ${JSON.stringify(dict.errors ?? "is_error=true")}`);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
if (typeof dict.result === "string" && dict.result) {
|
|
50
|
-
logDebug("cli_parser", "Found result text in session envelope, attempting JSON extraction");
|
|
51
|
-
const extracted = parseJsonMaybe(dict.result as string, requireFields);
|
|
52
|
-
if (extracted) return extracted;
|
|
53
|
-
logWarn("cli_parser", "Session envelope result text contained no extractable JSON");
|
|
54
|
-
}
|
|
55
|
-
logDebug("cli_parser", "Session result envelope with no structured_output or extractable result");
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Strategy 2: Assistant message with StructuredOutput tool use
|
|
60
|
-
if (dict.type === "assistant") {
|
|
61
|
-
const message = dict.message as Record<string, unknown> | undefined;
|
|
62
|
-
const content = message?.content;
|
|
63
|
-
if (Array.isArray(content)) {
|
|
64
|
-
for (const item of content) {
|
|
65
|
-
if (
|
|
66
|
-
item !== null &&
|
|
67
|
-
typeof item === "object" &&
|
|
68
|
-
(item as Record<string, unknown>).name === "StructuredOutput"
|
|
69
|
-
) {
|
|
70
|
-
logDebug(
|
|
71
|
-
"cli_parser",
|
|
72
|
-
"Found StructuredOutput in assistant message content",
|
|
73
|
-
);
|
|
74
|
-
return (item as Record<string, unknown>).input as Record<
|
|
75
|
-
string,
|
|
76
|
-
unknown
|
|
77
|
-
>;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
logDebug(
|
|
83
|
-
"cli_parser",
|
|
84
|
-
"Assistant message found but no StructuredOutput tool use in content",
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
} else if (Array.isArray(result)) {
|
|
88
|
-
// Strategy 3: List of events with assistant messages
|
|
89
|
-
logDebug(
|
|
90
|
-
"cli_parser",
|
|
91
|
-
`Received list of ${(result as unknown[]).length} events, searching for assistant message`,
|
|
92
|
-
);
|
|
93
|
-
for (let i = 0; i < (result as unknown[]).length; i++) {
|
|
94
|
-
const event = (result as unknown[])[i];
|
|
95
|
-
if (event === null || typeof event !== "object") continue;
|
|
96
|
-
|
|
97
|
-
const dict = event as Record<string, unknown>;
|
|
98
|
-
if (dict.type === "assistant") {
|
|
99
|
-
const message = dict.message as Record<string, unknown> | undefined;
|
|
100
|
-
const content = message?.content;
|
|
101
|
-
if (Array.isArray(content)) {
|
|
102
|
-
for (const item of content) {
|
|
103
|
-
if (
|
|
104
|
-
item !== null &&
|
|
105
|
-
typeof item === "object" &&
|
|
106
|
-
(item as Record<string, unknown>).name === "StructuredOutput"
|
|
107
|
-
) {
|
|
108
|
-
logDebug(
|
|
109
|
-
"cli_parser",
|
|
110
|
-
`Found StructuredOutput in event[${i}] assistant message`,
|
|
111
|
-
);
|
|
112
|
-
return (item as Record<string, unknown>).input as Record<
|
|
113
|
-
string,
|
|
114
|
-
unknown
|
|
115
|
-
>;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
logDebug(
|
|
123
|
-
"cli_parser",
|
|
124
|
-
"No StructuredOutput found in any assistant message in event list",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
} catch (error: unknown) {
|
|
128
|
-
if (error instanceof SyntaxError) {
|
|
129
|
-
logWarn("cli_parser", `JSON decode error: ${error.message}`);
|
|
130
|
-
} else {
|
|
131
|
-
logError(
|
|
132
|
-
"cli_parser",
|
|
133
|
-
`Unexpected error during structured parsing: ${error}`,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Strategy 4: Heuristic {…} extraction fallback
|
|
139
|
-
logDebug(
|
|
140
|
-
"cli_parser",
|
|
141
|
-
"No structured output found, falling back to heuristic JSON extraction",
|
|
142
|
-
);
|
|
143
|
-
return parseJsonMaybe(raw, requireFields);
|
|
16
|
+
return parseStructuredOutput(raw, {
|
|
17
|
+
requireFields,
|
|
18
|
+
loggerTag: "cli_parser",
|
|
19
|
+
});
|
|
144
20
|
}
|