@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,137 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Derived from @openai/codex-plugin-cc and modified for claude-crew.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
export function runCommand(command, args = [], options = {}) {
|
|
7
|
+
const result = spawnSync(command, args, {
|
|
8
|
+
cwd: options.cwd,
|
|
9
|
+
env: options.env,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
input: options.input,
|
|
12
|
+
maxBuffer: options.maxBuffer,
|
|
13
|
+
stdio: options.stdio ?? "pipe",
|
|
14
|
+
shell: process.platform === "win32",
|
|
15
|
+
windowsHide: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
command,
|
|
20
|
+
args,
|
|
21
|
+
status: result.status ?? 0,
|
|
22
|
+
signal: result.signal ?? null,
|
|
23
|
+
stdout: result.stdout ?? "",
|
|
24
|
+
stderr: result.stderr ?? "",
|
|
25
|
+
error: result.error ?? null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function runCommandChecked(command, args = [], options = {}) {
|
|
30
|
+
const result = runCommand(command, args, options);
|
|
31
|
+
if (result.error) {
|
|
32
|
+
throw result.error;
|
|
33
|
+
}
|
|
34
|
+
if (result.status !== 0) {
|
|
35
|
+
throw new Error(formatCommandFailure(result));
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function binaryAvailable(command, versionArgs = ["--version"], options = {}) {
|
|
41
|
+
const result = runCommand(command, versionArgs, options);
|
|
42
|
+
if (result.error && /** @type {NodeJS.ErrnoException} */ (result.error).code === "ENOENT") {
|
|
43
|
+
return { available: false, detail: "not found" };
|
|
44
|
+
}
|
|
45
|
+
if (result.error) {
|
|
46
|
+
return { available: false, detail: result.error.message };
|
|
47
|
+
}
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
|
|
50
|
+
return { available: false, detail };
|
|
51
|
+
}
|
|
52
|
+
return { available: true, detail: result.stdout.trim() || result.stderr.trim() || "ok" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function looksLikeMissingProcessMessage(text) {
|
|
56
|
+
return /not found|no running instance|cannot find|does not exist|no such process/i.test(text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function terminateProcessTree(pid, options = {}) {
|
|
60
|
+
if (!Number.isFinite(pid)) {
|
|
61
|
+
return { attempted: false, delivered: false, method: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const platform = options.platform ?? process.platform;
|
|
65
|
+
const runCommandImpl = options.runCommandImpl ?? runCommand;
|
|
66
|
+
const killImpl = options.killImpl ?? process.kill.bind(process);
|
|
67
|
+
|
|
68
|
+
if (platform === "win32") {
|
|
69
|
+
const result = runCommandImpl("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
env: options.env
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!result.error && result.status === 0) {
|
|
75
|
+
return { attempted: true, delivered: true, method: "taskkill", result };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const combinedOutput = `${result.stderr}\n${result.stdout}`.trim();
|
|
79
|
+
if (!result.error && looksLikeMissingProcessMessage(combinedOutput)) {
|
|
80
|
+
return { attempted: true, delivered: false, method: "taskkill", result };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (result.error?.code === "ENOENT") {
|
|
84
|
+
try {
|
|
85
|
+
killImpl(pid);
|
|
86
|
+
return { attempted: true, delivered: true, method: "kill" };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.code === "ESRCH") {
|
|
89
|
+
return { attempted: true, delivered: false, method: "kill" };
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (result.error) {
|
|
96
|
+
throw result.error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(formatCommandFailure(result));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
killImpl(-pid, "SIGTERM");
|
|
104
|
+
return { attempted: true, delivered: true, method: "process-group" };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.code !== "ESRCH") {
|
|
107
|
+
try {
|
|
108
|
+
killImpl(pid, "SIGTERM");
|
|
109
|
+
return { attempted: true, delivered: true, method: "process" };
|
|
110
|
+
} catch (innerError) {
|
|
111
|
+
if (innerError?.code === "ESRCH") {
|
|
112
|
+
return { attempted: true, delivered: false, method: "process" };
|
|
113
|
+
}
|
|
114
|
+
throw innerError;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { attempted: true, delivered: false, method: "process-group" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatCommandFailure(result) {
|
|
123
|
+
const parts = [`${result.command} ${result.args.join(" ")}`.trim()];
|
|
124
|
+
if (result.signal) {
|
|
125
|
+
parts.push(`signal=${result.signal}`);
|
|
126
|
+
} else {
|
|
127
|
+
parts.push(`exit=${result.status}`);
|
|
128
|
+
}
|
|
129
|
+
const stderr = (result.stderr || "").trim();
|
|
130
|
+
const stdout = (result.stdout || "").trim();
|
|
131
|
+
if (stderr) {
|
|
132
|
+
parts.push(stderr);
|
|
133
|
+
} else if (stdout) {
|
|
134
|
+
parts.push(stdout);
|
|
135
|
+
}
|
|
136
|
+
return parts.join(": ");
|
|
137
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Derived from @openai/codex-plugin-cc and modified for claude-crew.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export function loadPromptTemplate(rootDir, name) {
|
|
7
|
+
const promptPath = path.join(rootDir, "prompts", `${name}.md`);
|
|
8
|
+
return fs.readFileSync(promptPath, "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function interpolateTemplate(template, variables) {
|
|
12
|
+
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => {
|
|
13
|
+
return Object.prototype.hasOwnProperty.call(variables, key) ? variables[key] : "";
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Derived from @openai/codex-plugin-cc and modified for claude-crew.
|
|
3
|
+
function severityRank(severity) {
|
|
4
|
+
switch (severity) {
|
|
5
|
+
case "critical":
|
|
6
|
+
return 0;
|
|
7
|
+
case "high":
|
|
8
|
+
return 1;
|
|
9
|
+
case "medium":
|
|
10
|
+
return 2;
|
|
11
|
+
default:
|
|
12
|
+
return 3;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatLineRange(finding) {
|
|
17
|
+
if (!finding.line_start) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
if (!finding.line_end || finding.line_end === finding.line_start) {
|
|
21
|
+
return `:${finding.line_start}`;
|
|
22
|
+
}
|
|
23
|
+
return `:${finding.line_start}-${finding.line_end}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateReviewResultShape(data) {
|
|
27
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
28
|
+
return "Expected a top-level JSON object.";
|
|
29
|
+
}
|
|
30
|
+
if (typeof data.verdict !== "string" || !data.verdict.trim()) {
|
|
31
|
+
return "Missing string `verdict`.";
|
|
32
|
+
}
|
|
33
|
+
if (typeof data.summary !== "string" || !data.summary.trim()) {
|
|
34
|
+
return "Missing string `summary`.";
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(data.findings)) {
|
|
37
|
+
return "Missing array `findings`.";
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(data.next_steps)) {
|
|
40
|
+
return "Missing array `next_steps`.";
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeReviewFinding(finding, index) {
|
|
46
|
+
const source = finding && typeof finding === "object" && !Array.isArray(finding) ? finding : {};
|
|
47
|
+
const lineStart = Number.isInteger(source.line_start) && source.line_start > 0 ? source.line_start : null;
|
|
48
|
+
const lineEnd =
|
|
49
|
+
Number.isInteger(source.line_end) && source.line_end > 0 && (!lineStart || source.line_end >= lineStart)
|
|
50
|
+
? source.line_end
|
|
51
|
+
: lineStart;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
severity: typeof source.severity === "string" && source.severity.trim() ? source.severity.trim() : "low",
|
|
55
|
+
title: typeof source.title === "string" && source.title.trim() ? source.title.trim() : `Finding ${index + 1}`,
|
|
56
|
+
body: typeof source.body === "string" && source.body.trim() ? source.body.trim() : "No details provided.",
|
|
57
|
+
file: typeof source.file === "string" && source.file.trim() ? source.file.trim() : "unknown",
|
|
58
|
+
line_start: lineStart,
|
|
59
|
+
line_end: lineEnd,
|
|
60
|
+
recommendation: typeof source.recommendation === "string" ? source.recommendation.trim() : ""
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeReviewResultData(data) {
|
|
65
|
+
return {
|
|
66
|
+
verdict: data.verdict.trim(),
|
|
67
|
+
summary: data.summary.trim(),
|
|
68
|
+
findings: data.findings.map((finding, index) => normalizeReviewFinding(finding, index)),
|
|
69
|
+
next_steps: data.next_steps
|
|
70
|
+
.filter((step) => typeof step === "string" && step.trim())
|
|
71
|
+
.map((step) => step.trim())
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isStructuredReviewStoredResult(storedJob) {
|
|
76
|
+
const result = storedJob?.result;
|
|
77
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return (
|
|
81
|
+
Object.prototype.hasOwnProperty.call(result, "result") ||
|
|
82
|
+
Object.prototype.hasOwnProperty.call(result, "parseError")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatJobLine(job) {
|
|
87
|
+
const parts = [job.id, `${job.status || "unknown"}`];
|
|
88
|
+
if (job.kindLabel) {
|
|
89
|
+
parts.push(job.kindLabel);
|
|
90
|
+
}
|
|
91
|
+
if (job.title) {
|
|
92
|
+
parts.push(job.title);
|
|
93
|
+
}
|
|
94
|
+
return parts.join(" | ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function escapeMarkdownCell(value) {
|
|
98
|
+
return String(value ?? "")
|
|
99
|
+
.replace(/\|/g, "\\|")
|
|
100
|
+
.replace(/\r?\n/g, " ")
|
|
101
|
+
.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatCodexResumeCommand(job) {
|
|
105
|
+
if (!job?.threadId) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return `codex resume ${job.threadId}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function appendActiveJobsTable(lines, jobs) {
|
|
112
|
+
lines.push("Active jobs:");
|
|
113
|
+
lines.push("| Job | Kind | Status | Phase | Elapsed | Codex Session ID | Summary | Actions |");
|
|
114
|
+
lines.push("| --- | --- | --- | --- | --- | --- | --- | --- |");
|
|
115
|
+
for (const job of jobs) {
|
|
116
|
+
const actions = [`crew-codex status ${job.id}`];
|
|
117
|
+
if (job.status === "queued" || job.status === "running") {
|
|
118
|
+
actions.push(`crew-codex cancel ${job.id}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(
|
|
121
|
+
`| ${escapeMarkdownCell(job.id)} | ${escapeMarkdownCell(job.kindLabel)} | ${escapeMarkdownCell(job.status)} | ${escapeMarkdownCell(job.phase ?? "")} | ${escapeMarkdownCell(job.elapsed ?? "")} | ${escapeMarkdownCell(job.threadId ?? "")} | ${escapeMarkdownCell(job.summary ?? "")} | ${actions.map((action) => `\`${action}\``).join("<br>")} |`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pushJobDetails(lines, job, options = {}) {
|
|
127
|
+
lines.push(`- ${formatJobLine(job)}`);
|
|
128
|
+
if (job.summary) {
|
|
129
|
+
lines.push(` Summary: ${job.summary}`);
|
|
130
|
+
}
|
|
131
|
+
if (job.phase) {
|
|
132
|
+
lines.push(` Phase: ${job.phase}`);
|
|
133
|
+
}
|
|
134
|
+
if (options.showElapsed && job.elapsed) {
|
|
135
|
+
lines.push(` Elapsed: ${job.elapsed}`);
|
|
136
|
+
}
|
|
137
|
+
if (options.showDuration && job.duration) {
|
|
138
|
+
lines.push(` Duration: ${job.duration}`);
|
|
139
|
+
}
|
|
140
|
+
if (job.threadId) {
|
|
141
|
+
lines.push(` Codex session ID: ${job.threadId}`);
|
|
142
|
+
}
|
|
143
|
+
const resumeCommand = formatCodexResumeCommand(job);
|
|
144
|
+
if (resumeCommand) {
|
|
145
|
+
lines.push(` Resume in Codex: ${resumeCommand}`);
|
|
146
|
+
}
|
|
147
|
+
if (job.logFile && options.showLog) {
|
|
148
|
+
lines.push(` Log: ${job.logFile}`);
|
|
149
|
+
}
|
|
150
|
+
if ((job.status === "queued" || job.status === "running") && options.showCancelHint) {
|
|
151
|
+
lines.push(` Cancel: crew-codex cancel ${job.id}`);
|
|
152
|
+
}
|
|
153
|
+
if (job.status !== "queued" && job.status !== "running" && options.showResultHint) {
|
|
154
|
+
lines.push(` Result: crew-codex result ${job.id}`);
|
|
155
|
+
}
|
|
156
|
+
if (job.status !== "queued" && job.status !== "running" && job.jobClass === "task" && job.write && options.showReviewHint) {
|
|
157
|
+
lines.push(" Review changes before shipping.");
|
|
158
|
+
}
|
|
159
|
+
if (job.progressPreview?.length) {
|
|
160
|
+
lines.push(" Progress:");
|
|
161
|
+
for (const line of job.progressPreview) {
|
|
162
|
+
lines.push(` ${line}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendReasoningSection(lines, reasoningSummary) {
|
|
168
|
+
if (!Array.isArray(reasoningSummary) || reasoningSummary.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push("", "Reasoning:");
|
|
173
|
+
for (const section of reasoningSummary) {
|
|
174
|
+
lines.push(`- ${section}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function renderSetupReport(report) {
|
|
179
|
+
const lines = [
|
|
180
|
+
"# Codex Setup",
|
|
181
|
+
"",
|
|
182
|
+
`Status: ${report.ready ? "ready" : "needs attention"}`,
|
|
183
|
+
"",
|
|
184
|
+
"Checks:",
|
|
185
|
+
`- node: ${report.node.detail}`,
|
|
186
|
+
`- npm: ${report.npm.detail}`,
|
|
187
|
+
`- codex: ${report.codex.detail}`,
|
|
188
|
+
`- auth: ${report.auth.detail}`,
|
|
189
|
+
`- session runtime: ${report.sessionRuntime.label}`,
|
|
190
|
+
`- review gate: ${report.reviewGateEnabled ? "enabled" : "disabled"}`,
|
|
191
|
+
""
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
if (report.actionsTaken.length > 0) {
|
|
195
|
+
lines.push("Actions taken:");
|
|
196
|
+
for (const action of report.actionsTaken) {
|
|
197
|
+
lines.push(`- ${action}`);
|
|
198
|
+
}
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (report.nextSteps.length > 0) {
|
|
203
|
+
lines.push("Next steps:");
|
|
204
|
+
for (const step of report.nextSteps) {
|
|
205
|
+
lines.push(`- ${step}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function renderReviewResult(parsedResult, meta) {
|
|
213
|
+
if (!parsedResult.parsed) {
|
|
214
|
+
const lines = [
|
|
215
|
+
`# Codex ${meta.reviewLabel}`,
|
|
216
|
+
"",
|
|
217
|
+
"Codex did not return valid structured JSON.",
|
|
218
|
+
"",
|
|
219
|
+
`- Parse error: ${parsedResult.parseError}`
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
if (parsedResult.rawOutput) {
|
|
223
|
+
lines.push("", "Raw final message:", "", "```text", parsedResult.rawOutput, "```");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
appendReasoningSection(lines, meta.reasoningSummary ?? parsedResult.reasoningSummary);
|
|
227
|
+
|
|
228
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const validationError = validateReviewResultShape(parsedResult.parsed);
|
|
232
|
+
if (validationError) {
|
|
233
|
+
const lines = [
|
|
234
|
+
`# Codex ${meta.reviewLabel}`,
|
|
235
|
+
"",
|
|
236
|
+
`Target: ${meta.targetLabel}`,
|
|
237
|
+
"Codex returned JSON with an unexpected review shape.",
|
|
238
|
+
"",
|
|
239
|
+
`- Validation error: ${validationError}`
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
if (parsedResult.rawOutput) {
|
|
243
|
+
lines.push("", "Raw final message:", "", "```text", parsedResult.rawOutput, "```");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
appendReasoningSection(lines, meta.reasoningSummary ?? parsedResult.reasoningSummary);
|
|
247
|
+
|
|
248
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const data = normalizeReviewResultData(parsedResult.parsed);
|
|
252
|
+
const findings = [...data.findings].sort((left, right) => severityRank(left.severity) - severityRank(right.severity));
|
|
253
|
+
const lines = [
|
|
254
|
+
`# Codex ${meta.reviewLabel}`,
|
|
255
|
+
"",
|
|
256
|
+
`Target: ${meta.targetLabel}`,
|
|
257
|
+
`Verdict: ${data.verdict}`,
|
|
258
|
+
"",
|
|
259
|
+
data.summary,
|
|
260
|
+
""
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
if (findings.length === 0) {
|
|
264
|
+
lines.push("No material findings.");
|
|
265
|
+
} else {
|
|
266
|
+
lines.push("Findings:");
|
|
267
|
+
for (const finding of findings) {
|
|
268
|
+
const lineSuffix = formatLineRange(finding);
|
|
269
|
+
lines.push(`- [${finding.severity}] ${finding.title} (${finding.file}${lineSuffix})`);
|
|
270
|
+
lines.push(` ${finding.body}`);
|
|
271
|
+
if (finding.recommendation) {
|
|
272
|
+
lines.push(` Recommendation: ${finding.recommendation}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (data.next_steps.length > 0) {
|
|
278
|
+
lines.push("", "Next steps:");
|
|
279
|
+
for (const step of data.next_steps) {
|
|
280
|
+
lines.push(`- ${step}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
appendReasoningSection(lines, meta.reasoningSummary);
|
|
285
|
+
|
|
286
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function renderNativeReviewResult(result, meta) {
|
|
290
|
+
const stdout = result.stdout.trim();
|
|
291
|
+
const stderr = result.stderr.trim();
|
|
292
|
+
const lines = [
|
|
293
|
+
`# Codex ${meta.reviewLabel}`,
|
|
294
|
+
"",
|
|
295
|
+
`Target: ${meta.targetLabel}`,
|
|
296
|
+
""
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
if (stdout) {
|
|
300
|
+
lines.push(stdout);
|
|
301
|
+
} else if (result.status === 0) {
|
|
302
|
+
lines.push("Codex review completed without any stdout output.");
|
|
303
|
+
} else {
|
|
304
|
+
lines.push("Codex review failed.");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (stderr) {
|
|
308
|
+
lines.push("", "stderr:", "", "```text", stderr, "```");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
appendReasoningSection(lines, meta.reasoningSummary);
|
|
312
|
+
|
|
313
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function renderTaskResult(parsedResult, meta) {
|
|
317
|
+
const rawOutput = typeof parsedResult?.rawOutput === "string" ? parsedResult.rawOutput : "";
|
|
318
|
+
if (rawOutput) {
|
|
319
|
+
return rawOutput.endsWith("\n") ? rawOutput : `${rawOutput}\n`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const message = String(parsedResult?.failureMessage ?? "").trim() || "Codex did not return a final message.";
|
|
323
|
+
return `${message}\n`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function renderStatusReport(report) {
|
|
327
|
+
const lines = [
|
|
328
|
+
"# Codex Status",
|
|
329
|
+
"",
|
|
330
|
+
`Session runtime: ${report.sessionRuntime.label}`,
|
|
331
|
+
`Review gate: ${report.config.stopReviewGate ? "enabled" : "disabled"}`,
|
|
332
|
+
""
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
if (report.running.length > 0) {
|
|
336
|
+
appendActiveJobsTable(lines, report.running);
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push("Live details:");
|
|
339
|
+
for (const job of report.running) {
|
|
340
|
+
pushJobDetails(lines, job, {
|
|
341
|
+
showElapsed: true,
|
|
342
|
+
showLog: true
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
lines.push("");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (report.latestFinished) {
|
|
349
|
+
lines.push("Latest finished:");
|
|
350
|
+
pushJobDetails(lines, report.latestFinished, {
|
|
351
|
+
showDuration: true,
|
|
352
|
+
showLog: report.latestFinished.status === "failed"
|
|
353
|
+
});
|
|
354
|
+
lines.push("");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (report.recent.length > 0) {
|
|
358
|
+
lines.push("Recent jobs:");
|
|
359
|
+
for (const job of report.recent) {
|
|
360
|
+
pushJobDetails(lines, job, {
|
|
361
|
+
showDuration: true,
|
|
362
|
+
showLog: job.status === "failed"
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
lines.push("");
|
|
366
|
+
} else if (report.running.length === 0 && !report.latestFinished) {
|
|
367
|
+
lines.push("No jobs recorded yet.", "");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (report.needsReview) {
|
|
371
|
+
lines.push("The stop-time review gate is enabled.");
|
|
372
|
+
lines.push("Ending the session will trigger a fresh Codex adversarial review and block if it finds issues.");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function renderJobStatusReport(job) {
|
|
379
|
+
const lines = ["# Codex Job Status", ""];
|
|
380
|
+
pushJobDetails(lines, job, {
|
|
381
|
+
showElapsed: job.status === "queued" || job.status === "running",
|
|
382
|
+
showDuration: job.status !== "queued" && job.status !== "running",
|
|
383
|
+
showLog: true,
|
|
384
|
+
showCancelHint: true,
|
|
385
|
+
showResultHint: true,
|
|
386
|
+
showReviewHint: true
|
|
387
|
+
});
|
|
388
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function renderStoredJobResult(job, storedJob) {
|
|
392
|
+
const threadId = storedJob?.threadId ?? job.threadId ?? null;
|
|
393
|
+
const resumeCommand = threadId ? `codex resume ${threadId}` : null;
|
|
394
|
+
if (isStructuredReviewStoredResult(storedJob) && storedJob?.rendered) {
|
|
395
|
+
const output = storedJob.rendered.endsWith("\n") ? storedJob.rendered : `${storedJob.rendered}\n`;
|
|
396
|
+
if (!threadId) {
|
|
397
|
+
return output;
|
|
398
|
+
}
|
|
399
|
+
return `${output}\nCodex session ID: ${threadId}\nResume in Codex: ${resumeCommand}\n`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const rawOutput =
|
|
403
|
+
(typeof storedJob?.result?.rawOutput === "string" && storedJob.result.rawOutput) ||
|
|
404
|
+
(typeof storedJob?.result?.codex?.stdout === "string" && storedJob.result.codex.stdout) ||
|
|
405
|
+
"";
|
|
406
|
+
if (rawOutput) {
|
|
407
|
+
const output = rawOutput.endsWith("\n") ? rawOutput : `${rawOutput}\n`;
|
|
408
|
+
if (!threadId) {
|
|
409
|
+
return output;
|
|
410
|
+
}
|
|
411
|
+
return `${output}\nCodex session ID: ${threadId}\nResume in Codex: ${resumeCommand}\n`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (storedJob?.rendered) {
|
|
415
|
+
const output = storedJob.rendered.endsWith("\n") ? storedJob.rendered : `${storedJob.rendered}\n`;
|
|
416
|
+
if (!threadId) {
|
|
417
|
+
return output;
|
|
418
|
+
}
|
|
419
|
+
return `${output}\nCodex session ID: ${threadId}\nResume in Codex: ${resumeCommand}\n`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const lines = [
|
|
423
|
+
`# ${job.title ?? "Codex Result"}`,
|
|
424
|
+
"",
|
|
425
|
+
`Job: ${job.id}`,
|
|
426
|
+
`Status: ${job.status}`
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
if (threadId) {
|
|
430
|
+
lines.push(`Codex session ID: ${threadId}`);
|
|
431
|
+
lines.push(`Resume in Codex: ${resumeCommand}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (job.summary) {
|
|
435
|
+
lines.push(`Summary: ${job.summary}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (job.errorMessage) {
|
|
439
|
+
lines.push("", job.errorMessage);
|
|
440
|
+
} else if (storedJob?.errorMessage) {
|
|
441
|
+
lines.push("", storedJob.errorMessage);
|
|
442
|
+
} else {
|
|
443
|
+
lines.push("", "No captured result payload was stored for this job.");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function renderCancelReport(job) {
|
|
450
|
+
const lines = [
|
|
451
|
+
"# Codex Cancel",
|
|
452
|
+
"",
|
|
453
|
+
`Cancelled ${job.id}.`,
|
|
454
|
+
""
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
if (job.title) {
|
|
458
|
+
lines.push(`- Title: ${job.title}`);
|
|
459
|
+
}
|
|
460
|
+
if (job.summary) {
|
|
461
|
+
lines.push(`- Summary: ${job.summary}`);
|
|
462
|
+
}
|
|
463
|
+
lines.push("- Check `crew-codex status` for the updated queue.");
|
|
464
|
+
|
|
465
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
466
|
+
}
|