@jjlabsio/claude-crew 0.1.30 → 0.1.32
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -1
- package/README.md +11 -1
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/data/provider-catalog.json +36 -14
- package/package.json +2 -1
- package/scripts/crew-codex/LICENSE +201 -0
- package/scripts/crew-codex/NOTICE +16 -0
- package/scripts/crew-codex/app-server-broker.mjs +254 -0
- package/scripts/crew-codex/lib/app-server.mjs +352 -0
- package/scripts/crew-codex/lib/args.mjs +130 -0
- package/scripts/crew-codex/lib/broker-endpoint.mjs +43 -0
- package/scripts/crew-codex/lib/broker-lifecycle.mjs +213 -0
- package/scripts/crew-codex/lib/codex.mjs +1090 -0
- package/scripts/crew-codex/lib/fs.mjs +42 -0
- package/scripts/crew-codex/lib/git.mjs +348 -0
- package/scripts/crew-codex/lib/job-control.mjs +310 -0
- package/scripts/crew-codex/lib/process.mjs +137 -0
- package/scripts/crew-codex/lib/prompts.mjs +15 -0
- package/scripts/crew-codex/lib/render.mjs +466 -0
- package/scripts/crew-codex/lib/state.mjs +424 -0
- package/scripts/crew-codex/lib/tracked-jobs.mjs +279 -0
- package/scripts/crew-codex/lib/workspace.mjs +11 -0
- package/scripts/crew-codex-companion.mjs +863 -0
- package/skills/crew-dev/SKILL.md +68 -18
- package/skills/crew-interview/SKILL.md +34 -6
- package/skills/crew-plan/SKILL.md +43 -15
- package/skills/crew-setup/SKILL.md +38 -34
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Derived from @openai/codex-plugin-cc and modified for claude-crew.
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("./app-server-protocol").AppServerNotification} AppServerNotification
|
|
5
|
+
* @typedef {import("./app-server-protocol").ReviewTarget} ReviewTarget
|
|
6
|
+
* @typedef {import("./app-server-protocol").ThreadItem} ThreadItem
|
|
7
|
+
* @typedef {import("./app-server-protocol").ThreadResumeParams} ThreadResumeParams
|
|
8
|
+
* @typedef {import("./app-server-protocol").ThreadStartParams} ThreadStartParams
|
|
9
|
+
* @typedef {import("./app-server-protocol").Turn} Turn
|
|
10
|
+
* @typedef {import("./app-server-protocol").UserInput} UserInput
|
|
11
|
+
* @typedef {((update: string | { message: string, phase: string | null, threadId?: string | null, turnId?: string | null, stderrMessage?: string | null, logTitle?: string | null, logBody?: string | null }) => void)} ProgressReporter
|
|
12
|
+
* @typedef {{
|
|
13
|
+
* threadId: string,
|
|
14
|
+
* rootThreadId: string,
|
|
15
|
+
* threadIds: Set<string>,
|
|
16
|
+
* threadTurnIds: Map<string, string>,
|
|
17
|
+
* threadLabels: Map<string, string>,
|
|
18
|
+
* turnId: string | null,
|
|
19
|
+
* bufferedNotifications: AppServerNotification[],
|
|
20
|
+
* completion: Promise<TurnCaptureState>,
|
|
21
|
+
* resolveCompletion: (state: TurnCaptureState) => void,
|
|
22
|
+
* rejectCompletion: (error: unknown) => void,
|
|
23
|
+
* finalTurn: Turn | null,
|
|
24
|
+
* completed: boolean,
|
|
25
|
+
* finalAnswerSeen: boolean,
|
|
26
|
+
* pendingCollaborations: Set<string>,
|
|
27
|
+
* activeSubagentTurns: Set<string>,
|
|
28
|
+
* completionTimer: ReturnType<typeof setTimeout> | null,
|
|
29
|
+
* lastAgentMessage: string,
|
|
30
|
+
* reviewText: string,
|
|
31
|
+
* reasoningSummary: string[],
|
|
32
|
+
* error: unknown,
|
|
33
|
+
* messages: Array<{ lifecycle: string, phase: string | null, text: string }>,
|
|
34
|
+
* fileChanges: ThreadItem[],
|
|
35
|
+
* commandExecutions: ThreadItem[],
|
|
36
|
+
* onProgress: ProgressReporter | null
|
|
37
|
+
* }} TurnCaptureState
|
|
38
|
+
*/
|
|
39
|
+
import { readJsonFile } from "./fs.mjs";
|
|
40
|
+
import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient } from "./app-server.mjs";
|
|
41
|
+
import { loadBrokerSession } from "./broker-lifecycle.mjs";
|
|
42
|
+
import { binaryAvailable } from "./process.mjs";
|
|
43
|
+
|
|
44
|
+
const SERVICE_NAME = "claude_code_codex_plugin";
|
|
45
|
+
const TASK_THREAD_PREFIX = "Codex Companion Task";
|
|
46
|
+
const DEFAULT_CONTINUE_PROMPT =
|
|
47
|
+
"Continue from the current thread state. Pick the next highest-value step and follow through until the task is resolved.";
|
|
48
|
+
|
|
49
|
+
function cleanCodexStderr(stderr) {
|
|
50
|
+
return stderr
|
|
51
|
+
.split(/\r?\n/)
|
|
52
|
+
.map((line) => line.trimEnd())
|
|
53
|
+
.filter((line) => line && !line.startsWith("WARNING: proceeding, even though we could not update PATH:"))
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @returns {ThreadStartParams} */
|
|
58
|
+
function buildThreadParams(cwd, options = {}) {
|
|
59
|
+
return {
|
|
60
|
+
cwd,
|
|
61
|
+
model: options.model ?? null,
|
|
62
|
+
approvalPolicy: options.approvalPolicy ?? "never",
|
|
63
|
+
sandbox: options.sandbox ?? "read-only",
|
|
64
|
+
serviceName: SERVICE_NAME,
|
|
65
|
+
ephemeral: options.ephemeral ?? true,
|
|
66
|
+
experimentalRawEvents: false
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @returns {ThreadResumeParams} */
|
|
71
|
+
function buildResumeParams(threadId, cwd, options = {}) {
|
|
72
|
+
return {
|
|
73
|
+
threadId,
|
|
74
|
+
cwd,
|
|
75
|
+
model: options.model ?? null,
|
|
76
|
+
approvalPolicy: options.approvalPolicy ?? "never",
|
|
77
|
+
sandbox: options.sandbox ?? "read-only"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @returns {UserInput[]} */
|
|
82
|
+
function buildTurnInput(prompt) {
|
|
83
|
+
return [{ type: "text", text: prompt, text_elements: [] }];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function shorten(text, limit = 72) {
|
|
87
|
+
const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
|
|
88
|
+
if (!normalized) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
if (normalized.length <= limit) {
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
return `${normalized.slice(0, limit - 3)}...`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function looksLikeVerificationCommand(command) {
|
|
98
|
+
return /\b(test|tests|lint|build|typecheck|type-check|check|verify|validate|pytest|jest|vitest|cargo test|npm test|pnpm test|yarn test|go test|mvn test|gradle test|tsc|eslint|ruff)\b/i.test(
|
|
99
|
+
command
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildTaskThreadName(prompt) {
|
|
104
|
+
const excerpt = shorten(prompt, 56);
|
|
105
|
+
return excerpt ? `${TASK_THREAD_PREFIX}: ${excerpt}` : TASK_THREAD_PREFIX;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractThreadId(message) {
|
|
109
|
+
return message?.params?.threadId ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractTurnId(message) {
|
|
113
|
+
if (message?.params?.turnId) {
|
|
114
|
+
return message.params.turnId;
|
|
115
|
+
}
|
|
116
|
+
if (message?.params?.turn?.id) {
|
|
117
|
+
return message.params.turn.id;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectTouchedFiles(fileChanges) {
|
|
123
|
+
const paths = new Set();
|
|
124
|
+
for (const fileChange of fileChanges) {
|
|
125
|
+
for (const change of fileChange.changes ?? []) {
|
|
126
|
+
if (change.path) {
|
|
127
|
+
paths.add(change.path);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...paths];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeReasoningText(text) {
|
|
135
|
+
return String(text ?? "").replace(/\s+/g, " ").trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractReasoningSections(value) {
|
|
139
|
+
if (!value) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof value === "string") {
|
|
144
|
+
const normalized = normalizeReasoningText(value);
|
|
145
|
+
return normalized ? [normalized] : [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
return value.flatMap((entry) => extractReasoningSections(entry));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof value === "object") {
|
|
153
|
+
if (typeof value.text === "string") {
|
|
154
|
+
return extractReasoningSections(value.text);
|
|
155
|
+
}
|
|
156
|
+
if ("summary" in value) {
|
|
157
|
+
return extractReasoningSections(value.summary);
|
|
158
|
+
}
|
|
159
|
+
if ("content" in value) {
|
|
160
|
+
return extractReasoningSections(value.content);
|
|
161
|
+
}
|
|
162
|
+
if ("parts" in value) {
|
|
163
|
+
return extractReasoningSections(value.parts);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mergeReasoningSections(existingSections, nextSections) {
|
|
171
|
+
const merged = [];
|
|
172
|
+
for (const section of [...existingSections, ...nextSections]) {
|
|
173
|
+
const normalized = normalizeReasoningText(section);
|
|
174
|
+
if (!normalized || merged.includes(normalized)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
merged.push(normalized);
|
|
178
|
+
}
|
|
179
|
+
return merged;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {ProgressReporter | null | undefined} onProgress
|
|
184
|
+
* @param {string | null | undefined} message
|
|
185
|
+
* @param {string | null | undefined} [phase]
|
|
186
|
+
*/
|
|
187
|
+
function emitProgress(onProgress, message, phase = null, extra = {}) {
|
|
188
|
+
if (!onProgress || !message) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!phase && Object.keys(extra).length === 0) {
|
|
192
|
+
onProgress(message);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
onProgress({ message, phase, ...extra });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function emitLogEvent(onProgress, options = {}) {
|
|
199
|
+
if (!onProgress) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
onProgress({
|
|
204
|
+
message: options.message ?? "",
|
|
205
|
+
phase: options.phase ?? null,
|
|
206
|
+
stderrMessage: options.stderrMessage ?? null,
|
|
207
|
+
logTitle: options.logTitle ?? null,
|
|
208
|
+
logBody: options.logBody ?? null
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function labelForThread(state, threadId) {
|
|
213
|
+
if (!threadId || threadId === state.rootThreadId || threadId === state.threadId) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return state.threadLabels.get(threadId) ?? threadId;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function registerThread(state, threadId, options = {}) {
|
|
220
|
+
if (!threadId) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
state.threadIds.add(threadId);
|
|
225
|
+
const label =
|
|
226
|
+
options.threadName ??
|
|
227
|
+
options.name ??
|
|
228
|
+
options.agentNickname ??
|
|
229
|
+
options.agentRole ??
|
|
230
|
+
state.threadLabels.get(threadId) ??
|
|
231
|
+
null;
|
|
232
|
+
if (label) {
|
|
233
|
+
state.threadLabels.set(threadId, label);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function describeStartedItem(state, item) {
|
|
238
|
+
switch (item.type) {
|
|
239
|
+
case "enteredReviewMode":
|
|
240
|
+
return { message: `Reviewer started: ${item.review}`, phase: "reviewing" };
|
|
241
|
+
case "commandExecution":
|
|
242
|
+
return {
|
|
243
|
+
message: `Running command: ${shorten(item.command, 96)}`,
|
|
244
|
+
phase: looksLikeVerificationCommand(item.command) ? "verifying" : "running"
|
|
245
|
+
};
|
|
246
|
+
case "fileChange":
|
|
247
|
+
return { message: `Applying ${item.changes.length} file change(s).`, phase: "editing" };
|
|
248
|
+
case "mcpToolCall":
|
|
249
|
+
return { message: `Calling ${item.server}/${item.tool}.`, phase: "investigating" };
|
|
250
|
+
case "dynamicToolCall":
|
|
251
|
+
return { message: `Running tool: ${item.tool}.`, phase: "investigating" };
|
|
252
|
+
case "collabAgentToolCall": {
|
|
253
|
+
const subagents = (item.receiverThreadIds ?? []).map((threadId) => labelForThread(state, threadId) ?? threadId);
|
|
254
|
+
const summary =
|
|
255
|
+
subagents.length > 0
|
|
256
|
+
? `Starting subagent ${subagents.join(", ")} via collaboration tool: ${item.tool}.`
|
|
257
|
+
: `Starting collaboration tool: ${item.tool}.`;
|
|
258
|
+
return { message: summary, phase: "investigating" };
|
|
259
|
+
}
|
|
260
|
+
case "webSearch":
|
|
261
|
+
return { message: `Searching: ${shorten(item.query, 96)}`, phase: "investigating" };
|
|
262
|
+
default:
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function describeCompletedItem(state, item) {
|
|
268
|
+
switch (item.type) {
|
|
269
|
+
case "commandExecution": {
|
|
270
|
+
const exitCode = item.exitCode ?? "?";
|
|
271
|
+
const statusLabel = item.status === "completed" ? "completed" : item.status;
|
|
272
|
+
return {
|
|
273
|
+
message: `Command ${statusLabel}: ${shorten(item.command, 96)} (exit ${exitCode})`,
|
|
274
|
+
phase: looksLikeVerificationCommand(item.command) ? "verifying" : "running"
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
case "fileChange":
|
|
278
|
+
return { message: `File changes ${item.status}.`, phase: "editing" };
|
|
279
|
+
case "mcpToolCall":
|
|
280
|
+
return { message: `Tool ${item.server}/${item.tool} ${item.status}.`, phase: "investigating" };
|
|
281
|
+
case "dynamicToolCall":
|
|
282
|
+
return { message: `Tool ${item.tool} ${item.status}.`, phase: "investigating" };
|
|
283
|
+
case "collabAgentToolCall": {
|
|
284
|
+
const subagents = (item.receiverThreadIds ?? []).map((threadId) => labelForThread(state, threadId) ?? threadId);
|
|
285
|
+
const summary =
|
|
286
|
+
subagents.length > 0
|
|
287
|
+
? `Subagent ${subagents.join(", ")} ${item.status}.`
|
|
288
|
+
: `Collaboration tool ${item.tool} ${item.status}.`;
|
|
289
|
+
return { message: summary, phase: "investigating" };
|
|
290
|
+
}
|
|
291
|
+
case "exitedReviewMode":
|
|
292
|
+
return { message: "Reviewer finished.", phase: "finalizing" };
|
|
293
|
+
default:
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @returns {TurnCaptureState} */
|
|
299
|
+
function createTurnCaptureState(threadId, options = {}) {
|
|
300
|
+
let resolveCompletion;
|
|
301
|
+
let rejectCompletion;
|
|
302
|
+
const completion = new Promise((resolve, reject) => {
|
|
303
|
+
resolveCompletion = resolve;
|
|
304
|
+
rejectCompletion = reject;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
threadId,
|
|
309
|
+
rootThreadId: threadId,
|
|
310
|
+
threadIds: new Set([threadId]),
|
|
311
|
+
threadTurnIds: new Map(),
|
|
312
|
+
threadLabels: new Map(),
|
|
313
|
+
turnId: null,
|
|
314
|
+
bufferedNotifications: [],
|
|
315
|
+
completion,
|
|
316
|
+
resolveCompletion,
|
|
317
|
+
rejectCompletion,
|
|
318
|
+
finalTurn: null,
|
|
319
|
+
completed: false,
|
|
320
|
+
finalAnswerSeen: false,
|
|
321
|
+
pendingCollaborations: new Set(),
|
|
322
|
+
activeSubagentTurns: new Set(),
|
|
323
|
+
completionTimer: null,
|
|
324
|
+
lastAgentMessage: "",
|
|
325
|
+
reviewText: "",
|
|
326
|
+
reasoningSummary: [],
|
|
327
|
+
error: null,
|
|
328
|
+
messages: [],
|
|
329
|
+
fileChanges: [],
|
|
330
|
+
commandExecutions: [],
|
|
331
|
+
onProgress: options.onProgress ?? null
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function clearCompletionTimer(state) {
|
|
336
|
+
if (state.completionTimer) {
|
|
337
|
+
clearTimeout(state.completionTimer);
|
|
338
|
+
state.completionTimer = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function completeTurn(state, turn = null, options = {}) {
|
|
343
|
+
if (state.completed) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
clearCompletionTimer(state);
|
|
348
|
+
state.completed = true;
|
|
349
|
+
|
|
350
|
+
if (turn) {
|
|
351
|
+
state.finalTurn = turn;
|
|
352
|
+
if (!state.turnId) {
|
|
353
|
+
state.turnId = turn.id;
|
|
354
|
+
}
|
|
355
|
+
} else if (!state.finalTurn) {
|
|
356
|
+
state.finalTurn = {
|
|
357
|
+
id: state.turnId ?? "inferred-turn",
|
|
358
|
+
status: "completed"
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (options.inferred) {
|
|
363
|
+
emitProgress(state.onProgress, "Turn completion inferred after the main thread finished and subagent work drained.", "finalizing");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
state.resolveCompletion(state);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function scheduleInferredCompletion(state) {
|
|
370
|
+
if (state.completed || state.finalTurn || !state.finalAnswerSeen) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (state.pendingCollaborations.size > 0 || state.activeSubagentTurns.size > 0) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
clearCompletionTimer(state);
|
|
379
|
+
state.completionTimer = setTimeout(() => {
|
|
380
|
+
state.completionTimer = null;
|
|
381
|
+
if (state.completed || state.finalTurn || !state.finalAnswerSeen) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (state.pendingCollaborations.size > 0 || state.activeSubagentTurns.size > 0) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
completeTurn(state, null, { inferred: true });
|
|
388
|
+
}, 250);
|
|
389
|
+
state.completionTimer.unref?.();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function belongsToTurn(state, message) {
|
|
393
|
+
const messageThreadId = extractThreadId(message);
|
|
394
|
+
if (!messageThreadId || !state.threadIds.has(messageThreadId)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const trackedTurnId = state.threadTurnIds.get(messageThreadId) ?? null;
|
|
398
|
+
const messageTurnId = extractTurnId(message);
|
|
399
|
+
return trackedTurnId === null || messageTurnId === null || messageTurnId === trackedTurnId;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function recordItem(state, item, lifecycle, threadId = null) {
|
|
403
|
+
if (item.type === "collabAgentToolCall") {
|
|
404
|
+
if (!threadId || threadId === state.threadId) {
|
|
405
|
+
if (lifecycle === "started" || item.status === "inProgress") {
|
|
406
|
+
state.pendingCollaborations.add(item.id);
|
|
407
|
+
} else if (lifecycle === "completed") {
|
|
408
|
+
state.pendingCollaborations.delete(item.id);
|
|
409
|
+
scheduleInferredCompletion(state);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const receiverThreadId of item.receiverThreadIds ?? []) {
|
|
413
|
+
registerThread(state, receiverThreadId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (item.type === "agentMessage") {
|
|
418
|
+
state.messages.push({
|
|
419
|
+
lifecycle,
|
|
420
|
+
phase: item.phase ?? null,
|
|
421
|
+
text: item.text ?? ""
|
|
422
|
+
});
|
|
423
|
+
if (item.text) {
|
|
424
|
+
if (!threadId || threadId === state.threadId) {
|
|
425
|
+
state.lastAgentMessage = item.text;
|
|
426
|
+
if (lifecycle === "completed" && item.phase === "final_answer") {
|
|
427
|
+
state.finalAnswerSeen = true;
|
|
428
|
+
scheduleInferredCompletion(state);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (lifecycle === "completed") {
|
|
432
|
+
const sourceLabel = labelForThread(state, threadId);
|
|
433
|
+
emitLogEvent(state.onProgress, {
|
|
434
|
+
message: sourceLabel ? `Subagent ${sourceLabel}: ${shorten(item.text, 96)}` : `Assistant message captured: ${shorten(item.text, 96)}`,
|
|
435
|
+
stderrMessage: null,
|
|
436
|
+
phase: item.phase === "final_answer" ? "finalizing" : null,
|
|
437
|
+
logTitle: sourceLabel ? `Subagent ${sourceLabel} message` : "Assistant message",
|
|
438
|
+
logBody: item.text
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (item.type === "exitedReviewMode") {
|
|
446
|
+
state.reviewText = item.review ?? "";
|
|
447
|
+
if (lifecycle === "completed" && item.review) {
|
|
448
|
+
emitLogEvent(state.onProgress, {
|
|
449
|
+
message: "Review output captured.",
|
|
450
|
+
stderrMessage: null,
|
|
451
|
+
phase: "finalizing",
|
|
452
|
+
logTitle: "Review output",
|
|
453
|
+
logBody: item.review
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (item.type === "reasoning" && lifecycle === "completed") {
|
|
460
|
+
const nextSections = extractReasoningSections(item.summary);
|
|
461
|
+
state.reasoningSummary = mergeReasoningSections(state.reasoningSummary, nextSections);
|
|
462
|
+
if (nextSections.length > 0) {
|
|
463
|
+
const sourceLabel = labelForThread(state, threadId);
|
|
464
|
+
emitLogEvent(state.onProgress, {
|
|
465
|
+
message: sourceLabel
|
|
466
|
+
? `Subagent ${sourceLabel} reasoning: ${shorten(nextSections[0], 96)}`
|
|
467
|
+
: `Reasoning summary captured: ${shorten(nextSections[0], 96)}`,
|
|
468
|
+
stderrMessage: null,
|
|
469
|
+
logTitle: sourceLabel ? `Subagent ${sourceLabel} reasoning summary` : "Reasoning summary",
|
|
470
|
+
logBody: nextSections.map((section) => `- ${section}`).join("\n")
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (item.type === "fileChange" && lifecycle === "completed") {
|
|
477
|
+
state.fileChanges.push(item);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (item.type === "commandExecution" && lifecycle === "completed") {
|
|
482
|
+
state.commandExecutions.push(item);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function applyTurnNotification(state, message) {
|
|
487
|
+
switch (message.method) {
|
|
488
|
+
case "thread/started":
|
|
489
|
+
registerThread(state, message.params.thread.id, {
|
|
490
|
+
threadName: message.params.thread.name,
|
|
491
|
+
name: message.params.thread.name,
|
|
492
|
+
agentNickname: message.params.thread.agentNickname,
|
|
493
|
+
agentRole: message.params.thread.agentRole
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
case "thread/name/updated":
|
|
497
|
+
registerThread(state, message.params.threadId, {
|
|
498
|
+
threadName: message.params.threadName ?? null
|
|
499
|
+
});
|
|
500
|
+
break;
|
|
501
|
+
case "turn/started":
|
|
502
|
+
registerThread(state, message.params.threadId);
|
|
503
|
+
state.threadTurnIds.set(message.params.threadId, message.params.turn.id);
|
|
504
|
+
if ((message.params.threadId ?? null) !== state.threadId) {
|
|
505
|
+
state.activeSubagentTurns.add(message.params.threadId);
|
|
506
|
+
}
|
|
507
|
+
emitProgress(
|
|
508
|
+
state.onProgress,
|
|
509
|
+
`Turn started (${message.params.turn.id}).`,
|
|
510
|
+
"starting",
|
|
511
|
+
(message.params.threadId ?? null) === state.threadId
|
|
512
|
+
? {
|
|
513
|
+
threadId: message.params.threadId ?? null,
|
|
514
|
+
turnId: message.params.turn.id ?? null
|
|
515
|
+
}
|
|
516
|
+
: {}
|
|
517
|
+
);
|
|
518
|
+
break;
|
|
519
|
+
case "item/started":
|
|
520
|
+
recordItem(state, message.params.item, "started", message.params.threadId ?? null);
|
|
521
|
+
{
|
|
522
|
+
const update = describeStartedItem(state, message.params.item);
|
|
523
|
+
emitProgress(state.onProgress, update?.message, update?.phase ?? null);
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
case "item/completed":
|
|
527
|
+
recordItem(state, message.params.item, "completed", message.params.threadId ?? null);
|
|
528
|
+
{
|
|
529
|
+
const update = describeCompletedItem(state, message.params.item);
|
|
530
|
+
emitProgress(state.onProgress, update?.message, update?.phase ?? null);
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
case "error":
|
|
534
|
+
state.error = message.params.error;
|
|
535
|
+
emitProgress(state.onProgress, `Codex error: ${message.params.error.message}`, "failed");
|
|
536
|
+
break;
|
|
537
|
+
case "turn/completed":
|
|
538
|
+
if ((message.params.threadId ?? null) !== state.threadId) {
|
|
539
|
+
state.activeSubagentTurns.delete(message.params.threadId);
|
|
540
|
+
scheduleInferredCompletion(state);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
emitProgress(
|
|
544
|
+
state.onProgress,
|
|
545
|
+
`Turn ${message.params.turn.status === "completed" ? "completed" : message.params.turn.status}.`,
|
|
546
|
+
"finalizing"
|
|
547
|
+
);
|
|
548
|
+
completeTurn(state, message.params.turn);
|
|
549
|
+
break;
|
|
550
|
+
default:
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function captureTurn(client, threadId, startRequest, options = {}) {
|
|
556
|
+
const state = createTurnCaptureState(threadId, options);
|
|
557
|
+
const previousHandler = client.notificationHandler;
|
|
558
|
+
|
|
559
|
+
client.setNotificationHandler((message) => {
|
|
560
|
+
if (!state.turnId) {
|
|
561
|
+
state.bufferedNotifications.push(message);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (message.method === "thread/started" || message.method === "thread/name/updated") {
|
|
566
|
+
applyTurnNotification(state, message);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!belongsToTurn(state, message)) {
|
|
571
|
+
if (previousHandler) {
|
|
572
|
+
previousHandler(message);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
applyTurnNotification(state, message);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const response = await startRequest();
|
|
582
|
+
options.onResponse?.(response, state);
|
|
583
|
+
state.turnId = response.turn?.id ?? null;
|
|
584
|
+
if (state.turnId) {
|
|
585
|
+
state.threadTurnIds.set(state.threadId, state.turnId);
|
|
586
|
+
}
|
|
587
|
+
for (const message of state.bufferedNotifications) {
|
|
588
|
+
if (belongsToTurn(state, message)) {
|
|
589
|
+
applyTurnNotification(state, message);
|
|
590
|
+
} else {
|
|
591
|
+
if (previousHandler) {
|
|
592
|
+
previousHandler(message);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
state.bufferedNotifications.length = 0;
|
|
597
|
+
|
|
598
|
+
if (response.turn?.status && response.turn.status !== "inProgress") {
|
|
599
|
+
completeTurn(state, response.turn);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return await state.completion;
|
|
603
|
+
} finally {
|
|
604
|
+
clearCompletionTimer(state);
|
|
605
|
+
client.setNotificationHandler(previousHandler ?? null);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function withAppServer(cwd, fn) {
|
|
610
|
+
let client = null;
|
|
611
|
+
try {
|
|
612
|
+
client = await CodexAppServerClient.connect(cwd);
|
|
613
|
+
const result = await fn(client);
|
|
614
|
+
await client.close();
|
|
615
|
+
return result;
|
|
616
|
+
} catch (error) {
|
|
617
|
+
const brokerRequested = client?.transport === "broker" || Boolean(process.env[BROKER_ENDPOINT_ENV]);
|
|
618
|
+
const shouldRetryDirect =
|
|
619
|
+
(client?.transport === "broker" && error?.rpcCode === BROKER_BUSY_RPC_CODE) ||
|
|
620
|
+
(brokerRequested && (error?.code === "ENOENT" || error?.code === "ECONNREFUSED"));
|
|
621
|
+
|
|
622
|
+
if (client) {
|
|
623
|
+
await client.close().catch(() => {});
|
|
624
|
+
client = null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!shouldRetryDirect) {
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const directClient = await CodexAppServerClient.connect(cwd, { disableBroker: true });
|
|
632
|
+
try {
|
|
633
|
+
return await fn(directClient);
|
|
634
|
+
} finally {
|
|
635
|
+
await directClient.close();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function startThread(client, cwd, options = {}) {
|
|
641
|
+
const response = await client.request("thread/start", buildThreadParams(cwd, options));
|
|
642
|
+
const threadId = response.thread.id;
|
|
643
|
+
if (options.threadName) {
|
|
644
|
+
try {
|
|
645
|
+
await client.request("thread/name/set", { threadId, name: options.threadName });
|
|
646
|
+
} catch (err) {
|
|
647
|
+
// Only suppress "unknown variant/method" errors from older CLI versions
|
|
648
|
+
// that don't support thread/name/set. Rethrow auth, network, or server errors.
|
|
649
|
+
const msg = String(err?.message ?? err ?? "");
|
|
650
|
+
if (!msg.includes("unknown variant") && !msg.includes("unknown method")) {
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return response;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function resumeThread(client, threadId, cwd, options = {}) {
|
|
659
|
+
return client.request("thread/resume", buildResumeParams(threadId, cwd, options));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function buildResultStatus(turnState) {
|
|
663
|
+
return turnState.finalTurn?.status === "completed" ? 0 : 1;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const BUILTIN_PROVIDER_LABELS = new Map([
|
|
667
|
+
["openai", "OpenAI"],
|
|
668
|
+
["ollama", "Ollama"],
|
|
669
|
+
["lmstudio", "LM Studio"]
|
|
670
|
+
]);
|
|
671
|
+
|
|
672
|
+
function normalizeProviderId(value) {
|
|
673
|
+
const providerId = typeof value === "string" ? value.trim() : "";
|
|
674
|
+
return providerId || null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function formatProviderLabel(providerId, providerConfig = null) {
|
|
678
|
+
const configuredName = typeof providerConfig?.name === "string" ? providerConfig.name.trim() : "";
|
|
679
|
+
if (configuredName) {
|
|
680
|
+
return configuredName;
|
|
681
|
+
}
|
|
682
|
+
if (!providerId) {
|
|
683
|
+
return "The active provider";
|
|
684
|
+
}
|
|
685
|
+
return BUILTIN_PROVIDER_LABELS.get(providerId) ?? providerId;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function buildAuthStatus(fields = {}) {
|
|
689
|
+
return {
|
|
690
|
+
available: true,
|
|
691
|
+
loggedIn: false,
|
|
692
|
+
detail: "not authenticated",
|
|
693
|
+
source: "unknown",
|
|
694
|
+
authMethod: null,
|
|
695
|
+
verified: null,
|
|
696
|
+
requiresOpenaiAuth: null,
|
|
697
|
+
provider: null,
|
|
698
|
+
...fields
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function resolveProviderConfig(configResponse) {
|
|
703
|
+
const config = configResponse?.config;
|
|
704
|
+
if (!config || typeof config !== "object") {
|
|
705
|
+
return {
|
|
706
|
+
providerId: null,
|
|
707
|
+
providerConfig: null
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const providerId = normalizeProviderId(config.model_provider);
|
|
712
|
+
const providers =
|
|
713
|
+
config.model_providers && typeof config.model_providers === "object" && !Array.isArray(config.model_providers)
|
|
714
|
+
? config.model_providers
|
|
715
|
+
: null;
|
|
716
|
+
const providerConfig =
|
|
717
|
+
providerId && providers?.[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null;
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
providerId,
|
|
721
|
+
providerConfig
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function buildAppServerAuthStatus(accountResponse, configResponse) {
|
|
726
|
+
const account = accountResponse?.account ?? null;
|
|
727
|
+
const requiresOpenaiAuth =
|
|
728
|
+
typeof accountResponse?.requiresOpenaiAuth === "boolean" ? accountResponse.requiresOpenaiAuth : null;
|
|
729
|
+
const { providerId, providerConfig } = resolveProviderConfig(configResponse);
|
|
730
|
+
const providerLabel = formatProviderLabel(providerId, providerConfig);
|
|
731
|
+
|
|
732
|
+
if (account?.type === "chatgpt") {
|
|
733
|
+
const email = typeof account.email === "string" && account.email.trim() ? account.email.trim() : null;
|
|
734
|
+
return buildAuthStatus({
|
|
735
|
+
loggedIn: true,
|
|
736
|
+
detail: email ? `ChatGPT login active for ${email}` : "ChatGPT login active",
|
|
737
|
+
source: "app-server",
|
|
738
|
+
authMethod: "chatgpt",
|
|
739
|
+
verified: true,
|
|
740
|
+
requiresOpenaiAuth,
|
|
741
|
+
provider: providerId
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (account?.type === "apiKey") {
|
|
746
|
+
return buildAuthStatus({
|
|
747
|
+
loggedIn: true,
|
|
748
|
+
detail: "API key configured (unverified)",
|
|
749
|
+
source: "app-server",
|
|
750
|
+
authMethod: "apiKey",
|
|
751
|
+
verified: false,
|
|
752
|
+
requiresOpenaiAuth,
|
|
753
|
+
provider: providerId
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (requiresOpenaiAuth === false) {
|
|
758
|
+
return buildAuthStatus({
|
|
759
|
+
loggedIn: true,
|
|
760
|
+
detail: `${providerLabel} is configured and does not require OpenAI authentication`,
|
|
761
|
+
source: "app-server",
|
|
762
|
+
requiresOpenaiAuth,
|
|
763
|
+
provider: providerId
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return buildAuthStatus({
|
|
768
|
+
loggedIn: false,
|
|
769
|
+
detail: `${providerLabel} requires OpenAI authentication`,
|
|
770
|
+
source: "app-server",
|
|
771
|
+
requiresOpenaiAuth,
|
|
772
|
+
provider: providerId
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function getCodexAuthStatusFromClient(client, cwd) {
|
|
777
|
+
try {
|
|
778
|
+
const accountResponse = await client.request("account/read", { refreshToken: false });
|
|
779
|
+
const configResponse = await client.request("config/read", {
|
|
780
|
+
includeLayers: false,
|
|
781
|
+
cwd
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
return buildAppServerAuthStatus(accountResponse, configResponse);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
return buildAuthStatus({
|
|
787
|
+
loggedIn: false,
|
|
788
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
789
|
+
source: "app-server"
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function getCodexAvailability(cwd) {
|
|
795
|
+
const versionStatus = binaryAvailable("codex", ["--version"], { cwd });
|
|
796
|
+
if (!versionStatus.available) {
|
|
797
|
+
return versionStatus;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const appServerStatus = binaryAvailable("codex", ["app-server", "--help"], { cwd });
|
|
801
|
+
if (!appServerStatus.available) {
|
|
802
|
+
return {
|
|
803
|
+
available: false,
|
|
804
|
+
detail: `${versionStatus.detail}; advanced runtime unavailable: ${appServerStatus.detail}`
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
available: true,
|
|
810
|
+
detail: `${versionStatus.detail}; advanced runtime available`
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function getSessionRuntimeStatus(env = process.env, cwd = process.cwd()) {
|
|
815
|
+
const endpoint = env?.[BROKER_ENDPOINT_ENV] ?? loadBrokerSession(cwd)?.endpoint ?? null;
|
|
816
|
+
if (endpoint) {
|
|
817
|
+
return {
|
|
818
|
+
mode: "shared",
|
|
819
|
+
label: "shared session",
|
|
820
|
+
detail: "This Claude session is configured to reuse one shared Codex runtime.",
|
|
821
|
+
endpoint
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
mode: "direct",
|
|
827
|
+
label: "direct startup",
|
|
828
|
+
detail: "No shared Codex runtime is active yet. The first review or task command will start one on demand.",
|
|
829
|
+
endpoint: null
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export async function getCodexAuthStatus(cwd, options = {}) {
|
|
834
|
+
const availability = getCodexAvailability(cwd);
|
|
835
|
+
if (!availability.available) {
|
|
836
|
+
return {
|
|
837
|
+
available: false,
|
|
838
|
+
loggedIn: false,
|
|
839
|
+
detail: availability.detail,
|
|
840
|
+
source: "availability",
|
|
841
|
+
authMethod: null,
|
|
842
|
+
verified: null,
|
|
843
|
+
requiresOpenaiAuth: null,
|
|
844
|
+
provider: null
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
let client = null;
|
|
849
|
+
try {
|
|
850
|
+
client = await CodexAppServerClient.connect(cwd, {
|
|
851
|
+
env: options.env,
|
|
852
|
+
reuseExistingBroker: true
|
|
853
|
+
});
|
|
854
|
+
return await getCodexAuthStatusFromClient(client, cwd);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
return buildAuthStatus({
|
|
857
|
+
loggedIn: false,
|
|
858
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
859
|
+
source: "app-server"
|
|
860
|
+
});
|
|
861
|
+
} finally {
|
|
862
|
+
if (client) {
|
|
863
|
+
await client.close().catch(() => {});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
|
|
869
|
+
if (!threadId || !turnId) {
|
|
870
|
+
return {
|
|
871
|
+
attempted: false,
|
|
872
|
+
interrupted: false,
|
|
873
|
+
transport: null,
|
|
874
|
+
detail: "missing threadId or turnId"
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const availability = getCodexAvailability(cwd);
|
|
879
|
+
if (!availability.available) {
|
|
880
|
+
return {
|
|
881
|
+
attempted: false,
|
|
882
|
+
interrupted: false,
|
|
883
|
+
transport: null,
|
|
884
|
+
detail: availability.detail
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let client = null;
|
|
889
|
+
try {
|
|
890
|
+
client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true });
|
|
891
|
+
await client.request("turn/interrupt", { threadId, turnId });
|
|
892
|
+
return {
|
|
893
|
+
attempted: true,
|
|
894
|
+
interrupted: true,
|
|
895
|
+
transport: client.transport,
|
|
896
|
+
detail: `Interrupted ${turnId} on ${threadId}.`
|
|
897
|
+
};
|
|
898
|
+
} catch (error) {
|
|
899
|
+
return {
|
|
900
|
+
attempted: true,
|
|
901
|
+
interrupted: false,
|
|
902
|
+
transport: client?.transport ?? null,
|
|
903
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
904
|
+
};
|
|
905
|
+
} finally {
|
|
906
|
+
await client?.close().catch(() => {});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export async function runAppServerReview(cwd, options = {}) {
|
|
911
|
+
const availability = getCodexAvailability(cwd);
|
|
912
|
+
if (!availability.available) {
|
|
913
|
+
throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/crew-setup`.");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return withAppServer(cwd, async (client) => {
|
|
917
|
+
emitProgress(options.onProgress, "Starting Codex review thread.", "starting");
|
|
918
|
+
const thread = await startThread(client, cwd, {
|
|
919
|
+
model: options.model,
|
|
920
|
+
sandbox: "read-only",
|
|
921
|
+
ephemeral: true,
|
|
922
|
+
threadName: options.threadName
|
|
923
|
+
});
|
|
924
|
+
const sourceThreadId = thread.thread.id;
|
|
925
|
+
emitProgress(options.onProgress, `Thread ready (${sourceThreadId}).`, "starting", {
|
|
926
|
+
threadId: sourceThreadId
|
|
927
|
+
});
|
|
928
|
+
const delivery = options.delivery ?? "inline";
|
|
929
|
+
|
|
930
|
+
const turnState = await captureTurn(
|
|
931
|
+
client,
|
|
932
|
+
sourceThreadId,
|
|
933
|
+
() =>
|
|
934
|
+
client.request("review/start", {
|
|
935
|
+
threadId: sourceThreadId,
|
|
936
|
+
delivery,
|
|
937
|
+
target: options.target
|
|
938
|
+
}),
|
|
939
|
+
{
|
|
940
|
+
onProgress: options.onProgress,
|
|
941
|
+
onResponse(response, state) {
|
|
942
|
+
if (response.reviewThreadId) {
|
|
943
|
+
state.threadIds.add(response.reviewThreadId);
|
|
944
|
+
if (delivery === "detached") {
|
|
945
|
+
state.threadId = response.reviewThreadId;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
status: buildResultStatus(turnState),
|
|
954
|
+
threadId: turnState.threadId,
|
|
955
|
+
sourceThreadId,
|
|
956
|
+
turnId: turnState.turnId,
|
|
957
|
+
reviewText: turnState.reviewText,
|
|
958
|
+
reasoningSummary: turnState.reasoningSummary,
|
|
959
|
+
turn: turnState.finalTurn,
|
|
960
|
+
error: turnState.error,
|
|
961
|
+
stderr: cleanCodexStderr(client.stderr)
|
|
962
|
+
};
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export async function runAppServerTurn(cwd, options = {}) {
|
|
967
|
+
const availability = getCodexAvailability(cwd);
|
|
968
|
+
if (!availability.available) {
|
|
969
|
+
throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/crew-setup`.");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return withAppServer(cwd, async (client) => {
|
|
973
|
+
let threadId;
|
|
974
|
+
|
|
975
|
+
if (options.resumeThreadId) {
|
|
976
|
+
emitProgress(options.onProgress, `Resuming thread ${options.resumeThreadId}.`, "starting");
|
|
977
|
+
const response = await resumeThread(client, options.resumeThreadId, cwd, {
|
|
978
|
+
model: options.model,
|
|
979
|
+
sandbox: options.sandbox,
|
|
980
|
+
ephemeral: false
|
|
981
|
+
});
|
|
982
|
+
threadId = response.thread.id;
|
|
983
|
+
} else {
|
|
984
|
+
emitProgress(options.onProgress, "Starting Codex task thread.", "starting");
|
|
985
|
+
const response = await startThread(client, cwd, {
|
|
986
|
+
model: options.model,
|
|
987
|
+
sandbox: options.sandbox,
|
|
988
|
+
ephemeral: options.persistThread ? false : true,
|
|
989
|
+
threadName: options.persistThread ? options.threadName : options.threadName ?? null
|
|
990
|
+
});
|
|
991
|
+
threadId = response.thread.id;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
emitProgress(options.onProgress, `Thread ready (${threadId}).`, "starting", {
|
|
995
|
+
threadId
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const prompt = options.prompt?.trim() || options.defaultPrompt || "";
|
|
999
|
+
if (!prompt) {
|
|
1000
|
+
throw new Error("A prompt is required for this Codex run.");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const turnState = await captureTurn(
|
|
1004
|
+
client,
|
|
1005
|
+
threadId,
|
|
1006
|
+
() =>
|
|
1007
|
+
client.request("turn/start", {
|
|
1008
|
+
threadId,
|
|
1009
|
+
input: buildTurnInput(prompt),
|
|
1010
|
+
model: options.model ?? null,
|
|
1011
|
+
effort: options.effort ?? null,
|
|
1012
|
+
outputSchema: options.outputSchema ?? null
|
|
1013
|
+
}),
|
|
1014
|
+
{ onProgress: options.onProgress }
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
return {
|
|
1018
|
+
status: buildResultStatus(turnState),
|
|
1019
|
+
threadId,
|
|
1020
|
+
turnId: turnState.turnId,
|
|
1021
|
+
finalMessage: turnState.lastAgentMessage,
|
|
1022
|
+
reasoningSummary: turnState.reasoningSummary,
|
|
1023
|
+
turn: turnState.finalTurn,
|
|
1024
|
+
error: turnState.error,
|
|
1025
|
+
stderr: cleanCodexStderr(client.stderr),
|
|
1026
|
+
fileChanges: turnState.fileChanges,
|
|
1027
|
+
touchedFiles: collectTouchedFiles(turnState.fileChanges),
|
|
1028
|
+
commandExecutions: turnState.commandExecutions
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export async function findLatestTaskThread(cwd) {
|
|
1034
|
+
const availability = getCodexAvailability(cwd);
|
|
1035
|
+
if (!availability.available) {
|
|
1036
|
+
throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/crew-setup`.");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return withAppServer(cwd, async (client) => {
|
|
1040
|
+
const response = await client.request("thread/list", {
|
|
1041
|
+
cwd,
|
|
1042
|
+
limit: 20,
|
|
1043
|
+
sortKey: "updated_at",
|
|
1044
|
+
sourceKinds: ["appServer"],
|
|
1045
|
+
searchTerm: TASK_THREAD_PREFIX
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
response.data.find((thread) => typeof thread.name === "string" && thread.name.startsWith(TASK_THREAD_PREFIX)) ??
|
|
1050
|
+
null
|
|
1051
|
+
);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function buildPersistentTaskThreadName(prompt) {
|
|
1056
|
+
return buildTaskThreadName(prompt);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function parseStructuredOutput(rawOutput, fallback = {}) {
|
|
1060
|
+
if (!rawOutput) {
|
|
1061
|
+
return {
|
|
1062
|
+
parsed: null,
|
|
1063
|
+
parseError: fallback.failureMessage ?? "Codex did not return a final structured message.",
|
|
1064
|
+
rawOutput: rawOutput ?? "",
|
|
1065
|
+
...fallback
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
try {
|
|
1070
|
+
return {
|
|
1071
|
+
parsed: JSON.parse(rawOutput),
|
|
1072
|
+
parseError: null,
|
|
1073
|
+
rawOutput,
|
|
1074
|
+
...fallback
|
|
1075
|
+
};
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
return {
|
|
1078
|
+
parsed: null,
|
|
1079
|
+
parseError: error.message,
|
|
1080
|
+
rawOutput,
|
|
1081
|
+
...fallback
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export function readOutputSchema(schemaPath) {
|
|
1087
|
+
return readJsonFile(schemaPath);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export { DEFAULT_CONTINUE_PROMPT, TASK_THREAD_PREFIX };
|