@jjlabsio/claude-crew 0.1.31 → 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 +65 -15
- 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,863 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Derived from @openai/codex-plugin-cc and modified for claude-crew.
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
import { parseArgs, splitRawArgumentString } from "./crew-codex/lib/args.mjs";
|
|
12
|
+
import {
|
|
13
|
+
buildPersistentTaskThreadName,
|
|
14
|
+
DEFAULT_CONTINUE_PROMPT,
|
|
15
|
+
findLatestTaskThread,
|
|
16
|
+
getCodexAvailability,
|
|
17
|
+
interruptAppServerTurn,
|
|
18
|
+
runAppServerTurn
|
|
19
|
+
} from "./crew-codex/lib/codex.mjs";
|
|
20
|
+
import { readStdinIfPiped } from "./crew-codex/lib/fs.mjs";
|
|
21
|
+
import { terminateProcessTree } from "./crew-codex/lib/process.mjs";
|
|
22
|
+
import {
|
|
23
|
+
generateJobId,
|
|
24
|
+
listJobs,
|
|
25
|
+
updateJobStateAndFile,
|
|
26
|
+
updateState,
|
|
27
|
+
upsertJob,
|
|
28
|
+
writeJobFile
|
|
29
|
+
} from "./crew-codex/lib/state.mjs";
|
|
30
|
+
import {
|
|
31
|
+
buildSingleJobSnapshot,
|
|
32
|
+
buildStatusSnapshot,
|
|
33
|
+
readStoredJob,
|
|
34
|
+
resolveCancelableJob,
|
|
35
|
+
resolveResultJob,
|
|
36
|
+
sortJobsNewestFirst
|
|
37
|
+
} from "./crew-codex/lib/job-control.mjs";
|
|
38
|
+
import {
|
|
39
|
+
appendLogLine,
|
|
40
|
+
createJobLogFile,
|
|
41
|
+
createJobProgressUpdater,
|
|
42
|
+
createJobRecord,
|
|
43
|
+
createProgressReporter,
|
|
44
|
+
nowIso,
|
|
45
|
+
runTrackedJob,
|
|
46
|
+
SESSION_ID_ENV
|
|
47
|
+
} from "./crew-codex/lib/tracked-jobs.mjs";
|
|
48
|
+
import { resolveWorkspaceRoot } from "./crew-codex/lib/workspace.mjs";
|
|
49
|
+
import {
|
|
50
|
+
renderStoredJobResult,
|
|
51
|
+
renderCancelReport,
|
|
52
|
+
renderJobStatusReport,
|
|
53
|
+
renderStatusReport,
|
|
54
|
+
renderTaskResult
|
|
55
|
+
} from "./crew-codex/lib/render.mjs";
|
|
56
|
+
|
|
57
|
+
const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
58
|
+
const DEFAULT_STATUS_WAIT_TIMEOUT_MS = 240000;
|
|
59
|
+
const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
|
|
60
|
+
const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
|
|
61
|
+
const MODEL_ALIASES = new Map([["spark", "gpt-5.3-codex-spark"]]);
|
|
62
|
+
const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn.";
|
|
63
|
+
const CREW_AGENT_RESULT_PATTERN = /<crew-agent-result>\s*([\s\S]*?)\s*<\/crew-agent-result>/g;
|
|
64
|
+
const CREW_AGENT_RESULT_STATUSES = new Set([
|
|
65
|
+
"complete",
|
|
66
|
+
"blocked_on_user",
|
|
67
|
+
"needs_agent",
|
|
68
|
+
"needs_tool",
|
|
69
|
+
"failed"
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
function printUsage() {
|
|
73
|
+
console.log(
|
|
74
|
+
[
|
|
75
|
+
"Usage:",
|
|
76
|
+
" node scripts/crew-codex-companion.mjs task [--background] [--write] [--expect-crew-result] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
|
|
77
|
+
" node scripts/crew-codex-companion.mjs status [job-id] [--all] [--json]",
|
|
78
|
+
" node scripts/crew-codex-companion.mjs result [job-id] [--json]",
|
|
79
|
+
" node scripts/crew-codex-companion.mjs cancel [job-id] [--json]"
|
|
80
|
+
].join("\n")
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function outputResult(value, asJson) {
|
|
85
|
+
if (asJson) {
|
|
86
|
+
console.log(JSON.stringify(value, null, 2));
|
|
87
|
+
} else {
|
|
88
|
+
process.stdout.write(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function outputCommandResult(payload, rendered, asJson) {
|
|
93
|
+
outputResult(asJson ? payload : rendered, asJson);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeRequestedModel(model) {
|
|
97
|
+
if (model == null) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const normalized = String(model).trim();
|
|
101
|
+
if (!normalized) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return MODEL_ALIASES.get(normalized.toLowerCase()) ?? normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeReasoningEffort(effort) {
|
|
108
|
+
if (effort == null) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const normalized = String(effort).trim().toLowerCase();
|
|
112
|
+
if (!normalized) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (!VALID_REASONING_EFFORTS.has(normalized)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Unsupported reasoning effort "${effort}". Use one of: none, minimal, low, medium, high, xhigh.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeArgv(argv) {
|
|
124
|
+
if (argv.length === 1) {
|
|
125
|
+
const [raw] = argv;
|
|
126
|
+
if (!raw || !raw.trim()) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
return splitRawArgumentString(raw);
|
|
130
|
+
}
|
|
131
|
+
return argv;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseCommandInput(argv, config = {}) {
|
|
135
|
+
return parseArgs(normalizeArgv(argv), {
|
|
136
|
+
...config,
|
|
137
|
+
aliasMap: {
|
|
138
|
+
C: "cwd",
|
|
139
|
+
...(config.aliasMap ?? {})
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveCommandCwd(options = {}) {
|
|
145
|
+
return options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveCommandWorkspace(options = {}) {
|
|
149
|
+
return resolveWorkspaceRoot(resolveCommandCwd(options));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sleep(ms) {
|
|
153
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shorten(text, limit = 96) {
|
|
157
|
+
const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
|
|
158
|
+
if (!normalized) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
if (normalized.length <= limit) {
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
|
164
|
+
return `${normalized.slice(0, limit - 3)}...`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function firstMeaningfulLine(text, fallback) {
|
|
168
|
+
const line = String(text ?? "")
|
|
169
|
+
.split(/\r?\n/)
|
|
170
|
+
.map((value) => value.trim())
|
|
171
|
+
.find(Boolean);
|
|
172
|
+
return line ?? fallback;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateCrewAgentResult(result) {
|
|
176
|
+
if (result == null || typeof result !== "object" || Array.isArray(result)) {
|
|
177
|
+
return "AgentResult must be a JSON object.";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!CREW_AGENT_RESULT_STATUSES.has(result.status)) {
|
|
181
|
+
return `AgentResult status must be one of: ${Array.from(CREW_AGENT_RESULT_STATUSES).join(", ")}.`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if ("questions" in result && !Array.isArray(result.questions)) {
|
|
185
|
+
return "AgentResult questions must be an array when provided.";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if ("requests" in result && !Array.isArray(result.requests)) {
|
|
189
|
+
return "AgentResult requests must be an array when provided.";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (result.status === "blocked_on_user" && (!Array.isArray(result.questions) || result.questions.length === 0)) {
|
|
193
|
+
return "AgentResult blocked_on_user requires a non-empty questions array.";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
(result.status === "needs_agent" || result.status === "needs_tool") &&
|
|
198
|
+
(!Array.isArray(result.requests) || result.requests.length === 0)
|
|
199
|
+
) {
|
|
200
|
+
return `AgentResult ${result.status} requires a non-empty requests array.`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseCrewAgentResult(rawOutput, required = false) {
|
|
207
|
+
const matches = [...String(rawOutput ?? "").matchAll(CREW_AGENT_RESULT_PATTERN)];
|
|
208
|
+
const latestMatch = matches.at(-1);
|
|
209
|
+
if (!latestMatch) {
|
|
210
|
+
return {
|
|
211
|
+
result: null,
|
|
212
|
+
error: required ? "Missing <crew-agent-result> block." : null
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const result = JSON.parse(latestMatch[1]);
|
|
218
|
+
const validationError = validateCrewAgentResult(result);
|
|
219
|
+
if (validationError) {
|
|
220
|
+
return {
|
|
221
|
+
result,
|
|
222
|
+
error: validationError
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
result,
|
|
228
|
+
error: null
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
result: null,
|
|
233
|
+
error: `Invalid <crew-agent-result> JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ensureCodexAvailable(cwd) {
|
|
239
|
+
const availability = getCodexAvailability(cwd);
|
|
240
|
+
if (!availability.available) {
|
|
241
|
+
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`.");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isActiveJobStatus(status) {
|
|
246
|
+
return status === "queued" || status === "running";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getCurrentClaudeSessionId() {
|
|
250
|
+
return process.env[SESSION_ID_ENV] ?? null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function filterJobsForCurrentClaudeSession(jobs) {
|
|
254
|
+
const sessionId = getCurrentClaudeSessionId();
|
|
255
|
+
if (!sessionId) {
|
|
256
|
+
return jobs;
|
|
257
|
+
}
|
|
258
|
+
return jobs.filter((job) => job.sessionId === sessionId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function findLatestResumableTaskJob(jobs) {
|
|
262
|
+
return (
|
|
263
|
+
jobs.find(
|
|
264
|
+
(job) =>
|
|
265
|
+
job.jobClass === "task" &&
|
|
266
|
+
job.threadId &&
|
|
267
|
+
job.status !== "queued" &&
|
|
268
|
+
job.status !== "running"
|
|
269
|
+
) ?? null
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
|
|
274
|
+
const timeoutMs = Math.max(0, Number(options.timeoutMs) || DEFAULT_STATUS_WAIT_TIMEOUT_MS);
|
|
275
|
+
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || DEFAULT_STATUS_POLL_INTERVAL_MS);
|
|
276
|
+
const deadline = Date.now() + timeoutMs;
|
|
277
|
+
let snapshot = buildSingleJobSnapshot(cwd, reference);
|
|
278
|
+
|
|
279
|
+
while (isActiveJobStatus(snapshot.job.status) && Date.now() < deadline) {
|
|
280
|
+
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
|
|
281
|
+
snapshot = buildSingleJobSnapshot(cwd, reference);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
...snapshot,
|
|
286
|
+
waitTimedOut: isActiveJobStatus(snapshot.job.status),
|
|
287
|
+
timeoutMs
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function resolveLatestTrackedTaskThread(cwd, options = {}) {
|
|
292
|
+
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
293
|
+
const sessionId = getCurrentClaudeSessionId();
|
|
294
|
+
const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)).filter((job) => job.id !== options.excludeJobId);
|
|
295
|
+
const visibleJobs = filterJobsForCurrentClaudeSession(jobs);
|
|
296
|
+
const activeTask = visibleJobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running"));
|
|
297
|
+
if (activeTask) {
|
|
298
|
+
throw new Error(`Task ${activeTask.id} is still running. Use crew-codex status before continuing it.`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const trackedTask = findLatestResumableTaskJob(visibleJobs);
|
|
302
|
+
if (trackedTask) {
|
|
303
|
+
return { id: trackedTask.threadId };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (sessionId) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return findLatestTaskThread(workspaceRoot);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function executeTaskRun(request) {
|
|
314
|
+
const workspaceRoot = resolveWorkspaceRoot(request.cwd);
|
|
315
|
+
ensureCodexAvailable(request.cwd);
|
|
316
|
+
|
|
317
|
+
const taskMetadata = buildTaskRunMetadata({
|
|
318
|
+
prompt: request.prompt,
|
|
319
|
+
resumeLast: request.resumeLast
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
let resumeThreadId = null;
|
|
323
|
+
if (request.resumeLast) {
|
|
324
|
+
const latestThread = await resolveLatestTrackedTaskThread(workspaceRoot, {
|
|
325
|
+
excludeJobId: request.jobId
|
|
326
|
+
});
|
|
327
|
+
if (!latestThread) {
|
|
328
|
+
throw new Error("No previous Codex task thread was found for this repository.");
|
|
329
|
+
}
|
|
330
|
+
resumeThreadId = latestThread.id;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!request.prompt && !resumeThreadId) {
|
|
334
|
+
throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last.");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = await runAppServerTurn(workspaceRoot, {
|
|
338
|
+
resumeThreadId,
|
|
339
|
+
prompt: request.prompt,
|
|
340
|
+
defaultPrompt: resumeThreadId ? DEFAULT_CONTINUE_PROMPT : "",
|
|
341
|
+
model: request.model,
|
|
342
|
+
effort: request.effort,
|
|
343
|
+
sandbox: request.write ? "workspace-write" : "read-only",
|
|
344
|
+
onProgress: request.onProgress,
|
|
345
|
+
persistThread: true,
|
|
346
|
+
threadName: resumeThreadId ? null : buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT)
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
|
|
350
|
+
const failureMessage = result.error?.message ?? result.stderr ?? "";
|
|
351
|
+
const rendered = renderTaskResult(
|
|
352
|
+
{
|
|
353
|
+
rawOutput,
|
|
354
|
+
failureMessage,
|
|
355
|
+
reasoningSummary: result.reasoningSummary
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
title: taskMetadata.title,
|
|
359
|
+
jobId: request.jobId ?? null,
|
|
360
|
+
write: Boolean(request.write)
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
const payload = {
|
|
364
|
+
status: result.status,
|
|
365
|
+
threadId: result.threadId,
|
|
366
|
+
rawOutput,
|
|
367
|
+
touchedFiles: result.touchedFiles,
|
|
368
|
+
reasoningSummary: result.reasoningSummary
|
|
369
|
+
};
|
|
370
|
+
if (request.expectCrewResult) {
|
|
371
|
+
const parsedCrewResult = parseCrewAgentResult(rawOutput, true);
|
|
372
|
+
payload.crewAgentResult = parsedCrewResult.result;
|
|
373
|
+
payload.crewAgentResultError = parsedCrewResult.error;
|
|
374
|
+
if (parsedCrewResult.error && result.status === 0) {
|
|
375
|
+
payload.status = 1;
|
|
376
|
+
} else if (parsedCrewResult.result?.status === "failed" && result.status === 0) {
|
|
377
|
+
payload.status = 1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
exitStatus: payload.status,
|
|
383
|
+
threadId: result.threadId,
|
|
384
|
+
turnId: result.turnId,
|
|
385
|
+
payload,
|
|
386
|
+
rendered,
|
|
387
|
+
summary: firstMeaningfulLine(rawOutput, firstMeaningfulLine(failureMessage, `${taskMetadata.title} finished.`)),
|
|
388
|
+
jobTitle: taskMetadata.title,
|
|
389
|
+
jobClass: "task",
|
|
390
|
+
write: Boolean(request.write)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function buildTaskRunMetadata({ prompt, resumeLast = false }) {
|
|
395
|
+
if (!resumeLast && String(prompt ?? "").includes(STOP_REVIEW_TASK_MARKER)) {
|
|
396
|
+
return {
|
|
397
|
+
title: "Codex Stop Gate Review",
|
|
398
|
+
summary: "Stop-gate review of previous Claude turn"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const title = resumeLast ? "Codex Resume" : "Codex Task";
|
|
403
|
+
const fallbackSummary = resumeLast ? DEFAULT_CONTINUE_PROMPT : "Task";
|
|
404
|
+
return {
|
|
405
|
+
title,
|
|
406
|
+
summary: shorten(prompt || fallbackSummary)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function renderQueuedTaskLaunch(payload) {
|
|
411
|
+
return `${payload.title} started in the background as ${payload.jobId}. Check crew-codex status ${payload.jobId} for progress.\n`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getJobKindLabel(kind, jobClass) {
|
|
415
|
+
if (kind === "adversarial-review") {
|
|
416
|
+
return "adversarial-review";
|
|
417
|
+
}
|
|
418
|
+
return jobClass === "review" ? "review" : "task";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function createCompanionJob({ prefix, kind, title, workspaceRoot, jobClass, summary, write = false }) {
|
|
422
|
+
return createJobRecord({
|
|
423
|
+
id: generateJobId(prefix),
|
|
424
|
+
kind,
|
|
425
|
+
kindLabel: getJobKindLabel(kind, jobClass),
|
|
426
|
+
title,
|
|
427
|
+
workspaceRoot,
|
|
428
|
+
jobClass,
|
|
429
|
+
summary,
|
|
430
|
+
write
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function createTrackedProgress(job, options = {}) {
|
|
435
|
+
const logFile = options.logFile ?? createJobLogFile(job.workspaceRoot, job.id, job.title);
|
|
436
|
+
return {
|
|
437
|
+
logFile,
|
|
438
|
+
progress: createProgressReporter({
|
|
439
|
+
stderr: Boolean(options.stderr),
|
|
440
|
+
logFile,
|
|
441
|
+
onEvent: createJobProgressUpdater(job.workspaceRoot, job.id)
|
|
442
|
+
})
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function buildTaskJob(workspaceRoot, taskMetadata, write) {
|
|
447
|
+
return createCompanionJob({
|
|
448
|
+
prefix: "task",
|
|
449
|
+
kind: "task",
|
|
450
|
+
title: taskMetadata.title,
|
|
451
|
+
workspaceRoot,
|
|
452
|
+
jobClass: "task",
|
|
453
|
+
summary: taskMetadata.summary,
|
|
454
|
+
write
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, expectCrewResult, jobId }) {
|
|
459
|
+
return {
|
|
460
|
+
cwd,
|
|
461
|
+
model,
|
|
462
|
+
effort,
|
|
463
|
+
prompt,
|
|
464
|
+
write,
|
|
465
|
+
resumeLast,
|
|
466
|
+
expectCrewResult,
|
|
467
|
+
jobId
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function readTaskPrompt(cwd, options, positionals) {
|
|
472
|
+
if (options["prompt-file"]) {
|
|
473
|
+
return fs.readFileSync(path.resolve(cwd, options["prompt-file"]), "utf8");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const positionalPrompt = positionals.join(" ");
|
|
477
|
+
return positionalPrompt || readStdinIfPiped();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function requireTaskRequest(prompt, resumeLast) {
|
|
481
|
+
if (!prompt && !resumeLast) {
|
|
482
|
+
throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last.");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function runForegroundCommand(job, runner, options = {}) {
|
|
487
|
+
const { logFile, progress } = createTrackedProgress(job, {
|
|
488
|
+
logFile: options.logFile,
|
|
489
|
+
stderr: !options.json
|
|
490
|
+
});
|
|
491
|
+
const execution = await runTrackedJob(job, () => runner(progress), { logFile });
|
|
492
|
+
outputResult(options.json ? execution.payload : execution.rendered, options.json);
|
|
493
|
+
if (execution.exitStatus !== 0) {
|
|
494
|
+
process.exitCode = execution.exitStatus;
|
|
495
|
+
}
|
|
496
|
+
return execution;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function spawnDetachedTaskWorker(cwd, jobId) {
|
|
500
|
+
const scriptPath = path.join(ROOT_DIR, "scripts", "crew-codex-companion.mjs");
|
|
501
|
+
const child = spawn(process.execPath, [scriptPath, "task-worker", "--cwd", cwd, "--job-id", jobId], {
|
|
502
|
+
cwd,
|
|
503
|
+
env: process.env,
|
|
504
|
+
detached: true,
|
|
505
|
+
stdio: "ignore",
|
|
506
|
+
windowsHide: true
|
|
507
|
+
});
|
|
508
|
+
child.unref();
|
|
509
|
+
return child;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function attachQueuedWorkerPid(workspaceRoot, jobId, pid) {
|
|
513
|
+
if (!Number.isFinite(pid)) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
updateState(workspaceRoot, (state) => {
|
|
518
|
+
const existingIndex = state.jobs.findIndex((candidate) => candidate.id === jobId);
|
|
519
|
+
if (existingIndex === -1 || state.jobs[existingIndex].status !== "queued") {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
state.jobs[existingIndex] = {
|
|
524
|
+
...state.jobs[existingIndex],
|
|
525
|
+
pid
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function markActiveJobCancelled(workspaceRoot, jobId, completedAt, existingFileJob = {}) {
|
|
531
|
+
const result = updateJobStateAndFile(workspaceRoot, jobId, (existing) => {
|
|
532
|
+
if (!existing) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
if (existing.status !== "queued" && existing.status !== "running") {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const nextJob = {
|
|
540
|
+
...existing,
|
|
541
|
+
status: "cancelled",
|
|
542
|
+
phase: "cancelled",
|
|
543
|
+
pid: null,
|
|
544
|
+
completedAt,
|
|
545
|
+
updatedAt: completedAt,
|
|
546
|
+
errorMessage: "Cancelled by user."
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
stateJob: nextJob,
|
|
551
|
+
fileJob: {
|
|
552
|
+
...existingFileJob,
|
|
553
|
+
...nextJob,
|
|
554
|
+
cancelledAt: completedAt
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
});
|
|
558
|
+
return result?.job ?? null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function enqueueBackgroundTask(cwd, job, request) {
|
|
562
|
+
const { logFile } = createTrackedProgress(job);
|
|
563
|
+
appendLogLine(logFile, "Queued for background execution.");
|
|
564
|
+
|
|
565
|
+
const queuedRecord = {
|
|
566
|
+
...job,
|
|
567
|
+
status: "queued",
|
|
568
|
+
phase: "queued",
|
|
569
|
+
pid: null,
|
|
570
|
+
logFile,
|
|
571
|
+
request
|
|
572
|
+
};
|
|
573
|
+
writeJobFile(job.workspaceRoot, job.id, queuedRecord);
|
|
574
|
+
upsertJob(job.workspaceRoot, queuedRecord);
|
|
575
|
+
|
|
576
|
+
const child = spawnDetachedTaskWorker(cwd, job.id);
|
|
577
|
+
attachQueuedWorkerPid(job.workspaceRoot, job.id, child.pid ?? null);
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
payload: {
|
|
581
|
+
jobId: job.id,
|
|
582
|
+
status: "queued",
|
|
583
|
+
title: job.title,
|
|
584
|
+
summary: job.summary,
|
|
585
|
+
logFile
|
|
586
|
+
},
|
|
587
|
+
logFile
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function handleTask(argv) {
|
|
592
|
+
const { options, positionals } = parseCommandInput(argv, {
|
|
593
|
+
valueOptions: ["model", "effort", "cwd", "prompt-file"],
|
|
594
|
+
booleanOptions: ["json", "write", "expect-crew-result", "resume-last", "resume", "fresh", "background"],
|
|
595
|
+
aliasMap: {
|
|
596
|
+
m: "model"
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const cwd = resolveCommandCwd(options);
|
|
601
|
+
const workspaceRoot = resolveCommandWorkspace(options);
|
|
602
|
+
const model = normalizeRequestedModel(options.model);
|
|
603
|
+
const effort = normalizeReasoningEffort(options.effort);
|
|
604
|
+
const prompt = readTaskPrompt(cwd, options, positionals);
|
|
605
|
+
|
|
606
|
+
const resumeLast = Boolean(options["resume-last"] || options.resume);
|
|
607
|
+
const fresh = Boolean(options.fresh);
|
|
608
|
+
if (resumeLast && fresh) {
|
|
609
|
+
throw new Error("Choose either --resume/--resume-last or --fresh.");
|
|
610
|
+
}
|
|
611
|
+
const write = Boolean(options.write);
|
|
612
|
+
const expectCrewResult = Boolean(options["expect-crew-result"]);
|
|
613
|
+
const taskMetadata = buildTaskRunMetadata({
|
|
614
|
+
prompt,
|
|
615
|
+
resumeLast
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
if (options.background) {
|
|
619
|
+
ensureCodexAvailable(cwd);
|
|
620
|
+
requireTaskRequest(prompt, resumeLast);
|
|
621
|
+
|
|
622
|
+
const job = buildTaskJob(workspaceRoot, taskMetadata, write);
|
|
623
|
+
const request = buildTaskRequest({
|
|
624
|
+
cwd,
|
|
625
|
+
model,
|
|
626
|
+
effort,
|
|
627
|
+
prompt,
|
|
628
|
+
write,
|
|
629
|
+
resumeLast,
|
|
630
|
+
expectCrewResult,
|
|
631
|
+
jobId: job.id
|
|
632
|
+
});
|
|
633
|
+
const { payload } = enqueueBackgroundTask(cwd, job, request);
|
|
634
|
+
outputCommandResult(payload, renderQueuedTaskLaunch(payload), options.json);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const job = buildTaskJob(workspaceRoot, taskMetadata, write);
|
|
639
|
+
await runForegroundCommand(
|
|
640
|
+
job,
|
|
641
|
+
(progress) =>
|
|
642
|
+
executeTaskRun({
|
|
643
|
+
cwd,
|
|
644
|
+
model,
|
|
645
|
+
effort,
|
|
646
|
+
prompt,
|
|
647
|
+
write,
|
|
648
|
+
resumeLast,
|
|
649
|
+
expectCrewResult,
|
|
650
|
+
jobId: job.id,
|
|
651
|
+
onProgress: progress
|
|
652
|
+
}),
|
|
653
|
+
{ json: options.json }
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function handleTaskWorker(argv) {
|
|
658
|
+
const { options } = parseCommandInput(argv, {
|
|
659
|
+
valueOptions: ["cwd", "job-id"]
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
if (!options["job-id"]) {
|
|
663
|
+
throw new Error("Missing required --job-id for task-worker.");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const cwd = resolveCommandCwd(options);
|
|
667
|
+
const workspaceRoot = resolveCommandWorkspace(options);
|
|
668
|
+
const storedJob = readStoredJob(workspaceRoot, options["job-id"]);
|
|
669
|
+
if (!storedJob) {
|
|
670
|
+
throw new Error(`No stored job found for ${options["job-id"]}.`);
|
|
671
|
+
}
|
|
672
|
+
if (storedJob.status !== "queued") {
|
|
673
|
+
appendLogLine(storedJob.logFile, `Skipping background worker because job is ${storedJob.status}.`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const request = storedJob.request;
|
|
678
|
+
if (!request || typeof request !== "object") {
|
|
679
|
+
throw new Error(`Stored job ${options["job-id"]} is missing its task request payload.`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const { logFile, progress } = createTrackedProgress(
|
|
683
|
+
{
|
|
684
|
+
...storedJob,
|
|
685
|
+
workspaceRoot
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
logFile: storedJob.logFile ?? null
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
await runTrackedJob(
|
|
692
|
+
{
|
|
693
|
+
...storedJob,
|
|
694
|
+
workspaceRoot,
|
|
695
|
+
logFile
|
|
696
|
+
},
|
|
697
|
+
() =>
|
|
698
|
+
executeTaskRun({
|
|
699
|
+
...request,
|
|
700
|
+
onProgress: progress
|
|
701
|
+
}),
|
|
702
|
+
{ logFile }
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function handleStatus(argv) {
|
|
707
|
+
const { options, positionals } = parseCommandInput(argv, {
|
|
708
|
+
valueOptions: ["cwd", "timeout-ms", "poll-interval-ms"],
|
|
709
|
+
booleanOptions: ["json", "all", "wait"]
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const cwd = resolveCommandCwd(options);
|
|
713
|
+
const reference = positionals[0] ?? "";
|
|
714
|
+
if (reference) {
|
|
715
|
+
const snapshot = options.wait
|
|
716
|
+
? await waitForSingleJobSnapshot(cwd, reference, {
|
|
717
|
+
timeoutMs: options["timeout-ms"],
|
|
718
|
+
pollIntervalMs: options["poll-interval-ms"]
|
|
719
|
+
})
|
|
720
|
+
: buildSingleJobSnapshot(cwd, reference);
|
|
721
|
+
outputCommandResult(snapshot, renderJobStatusReport(snapshot.job), options.json);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (options.wait) {
|
|
726
|
+
throw new Error("`status --wait` requires a job id.");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const report = buildStatusSnapshot(cwd, { all: options.all });
|
|
730
|
+
outputResult(options.json ? report : renderStatusReport(report), options.json);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function handleResult(argv) {
|
|
734
|
+
const { options, positionals } = parseCommandInput(argv, {
|
|
735
|
+
valueOptions: ["cwd"],
|
|
736
|
+
booleanOptions: ["json"]
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const cwd = resolveCommandCwd(options);
|
|
740
|
+
const reference = positionals[0] ?? "";
|
|
741
|
+
const { workspaceRoot, job } = resolveResultJob(cwd, reference);
|
|
742
|
+
const storedJob = readStoredJob(workspaceRoot, job.id);
|
|
743
|
+
const payload = {
|
|
744
|
+
job,
|
|
745
|
+
storedJob
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
outputCommandResult(payload, renderStoredJobResult(job, storedJob), options.json);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function handleTaskResumeCandidate(argv) {
|
|
752
|
+
const { options } = parseCommandInput(argv, {
|
|
753
|
+
valueOptions: ["cwd"],
|
|
754
|
+
booleanOptions: ["json"]
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const cwd = resolveCommandCwd(options);
|
|
758
|
+
const workspaceRoot = resolveCommandWorkspace(options);
|
|
759
|
+
const sessionId = getCurrentClaudeSessionId();
|
|
760
|
+
const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot)));
|
|
761
|
+
const candidate = findLatestResumableTaskJob(jobs);
|
|
762
|
+
|
|
763
|
+
const payload = {
|
|
764
|
+
available: Boolean(candidate),
|
|
765
|
+
sessionId,
|
|
766
|
+
candidate:
|
|
767
|
+
candidate == null
|
|
768
|
+
? null
|
|
769
|
+
: {
|
|
770
|
+
id: candidate.id,
|
|
771
|
+
status: candidate.status,
|
|
772
|
+
title: candidate.title ?? null,
|
|
773
|
+
summary: candidate.summary ?? null,
|
|
774
|
+
threadId: candidate.threadId,
|
|
775
|
+
completedAt: candidate.completedAt ?? null,
|
|
776
|
+
updatedAt: candidate.updatedAt ?? null
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const rendered = candidate
|
|
781
|
+
? `Resumable task found: ${candidate.id} (${candidate.status}).\n`
|
|
782
|
+
: "No resumable task found for this session.\n";
|
|
783
|
+
outputCommandResult(payload, rendered, options.json);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function handleCancel(argv) {
|
|
787
|
+
const { options, positionals } = parseCommandInput(argv, {
|
|
788
|
+
valueOptions: ["cwd"],
|
|
789
|
+
booleanOptions: ["json"]
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const cwd = resolveCommandCwd(options);
|
|
793
|
+
const reference = positionals[0] ?? "";
|
|
794
|
+
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
|
|
795
|
+
const existing = readStoredJob(workspaceRoot, job.id) ?? {};
|
|
796
|
+
const threadId = existing.threadId ?? job.threadId ?? null;
|
|
797
|
+
const turnId = existing.turnId ?? job.turnId ?? null;
|
|
798
|
+
const completedAt = nowIso();
|
|
799
|
+
const nextJob = markActiveJobCancelled(workspaceRoot, job.id, completedAt, existing);
|
|
800
|
+
if (!nextJob) {
|
|
801
|
+
throw new Error(`Job ${job.id} is no longer active.`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const interrupt = await interruptAppServerTurn(cwd, { threadId, turnId });
|
|
805
|
+
if (interrupt.attempted) {
|
|
806
|
+
appendLogLine(
|
|
807
|
+
job.logFile,
|
|
808
|
+
interrupt.interrupted
|
|
809
|
+
? `Requested Codex turn interrupt for ${turnId} on ${threadId}.`
|
|
810
|
+
: `Codex turn interrupt failed${interrupt.detail ? `: ${interrupt.detail}` : "."}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
terminateProcessTree(job.pid ?? Number.NaN);
|
|
815
|
+
appendLogLine(job.logFile, "Cancelled by user.");
|
|
816
|
+
|
|
817
|
+
const payload = {
|
|
818
|
+
jobId: job.id,
|
|
819
|
+
status: "cancelled",
|
|
820
|
+
title: job.title,
|
|
821
|
+
turnInterruptAttempted: interrupt.attempted,
|
|
822
|
+
turnInterrupted: interrupt.interrupted
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
outputCommandResult(payload, renderCancelReport(nextJob), options.json);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function main() {
|
|
829
|
+
const [subcommand, ...argv] = process.argv.slice(2);
|
|
830
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
831
|
+
printUsage();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
switch (subcommand) {
|
|
836
|
+
case "task":
|
|
837
|
+
await handleTask(argv);
|
|
838
|
+
break;
|
|
839
|
+
case "task-worker":
|
|
840
|
+
await handleTaskWorker(argv);
|
|
841
|
+
break;
|
|
842
|
+
case "status":
|
|
843
|
+
await handleStatus(argv);
|
|
844
|
+
break;
|
|
845
|
+
case "result":
|
|
846
|
+
handleResult(argv);
|
|
847
|
+
break;
|
|
848
|
+
case "task-resume-candidate":
|
|
849
|
+
handleTaskResumeCandidate(argv);
|
|
850
|
+
break;
|
|
851
|
+
case "cancel":
|
|
852
|
+
await handleCancel(argv);
|
|
853
|
+
break;
|
|
854
|
+
default:
|
|
855
|
+
throw new Error(`Unknown subcommand: ${subcommand}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
main().catch((error) => {
|
|
860
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
861
|
+
process.stderr.write(`${message}\n`);
|
|
862
|
+
process.exitCode = 1;
|
|
863
|
+
});
|