@mulep/cli 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +4790 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts-N6ZYOVSU.js +10 -0
- package/dist/prompts-N6ZYOVSU.js.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4790 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5
|
+
import { CLEANUP_TIMEOUT_SEC, VERSION as VERSION2 } from "@mulep/core";
|
|
6
|
+
|
|
7
|
+
// src/commands/build.ts
|
|
8
|
+
import { BuildStore, DebateStore, REVIEW_DIFF_MAX_CHARS, REVIEW_TEXT_MAX_CHARS, SessionManager, buildHandoffEnvelope, generateId, loadConfig, openDatabase as openDatabase2 } from "@mulep/core";
|
|
9
|
+
import chalk3 from "chalk";
|
|
10
|
+
import { execFileSync, execSync } from "child_process";
|
|
11
|
+
import { unlinkSync } from "fs";
|
|
12
|
+
import { join as join2 } from "path";
|
|
13
|
+
|
|
14
|
+
// src/progress.ts
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
var THROTTLE_MS = 3e4;
|
|
17
|
+
var MAX_CARRY_OVER = 64 * 1024;
|
|
18
|
+
function createProgressCallbacks(label = "codex") {
|
|
19
|
+
let lastActivityAt = 0;
|
|
20
|
+
let lastMessage = "";
|
|
21
|
+
let carryOver = "";
|
|
22
|
+
let droppedEvents = 0;
|
|
23
|
+
function printActivity(msg) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
if (msg === lastMessage && now - lastActivityAt < THROTTLE_MS) return;
|
|
26
|
+
lastActivityAt = now;
|
|
27
|
+
lastMessage = msg;
|
|
28
|
+
console.error(chalk.dim(` [${label}] ${msg}`));
|
|
29
|
+
}
|
|
30
|
+
function parseLine(line) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed) return;
|
|
33
|
+
try {
|
|
34
|
+
const event = JSON.parse(trimmed);
|
|
35
|
+
formatEvent(event, printActivity);
|
|
36
|
+
} catch {
|
|
37
|
+
droppedEvents++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
onSpawn(pid, command) {
|
|
42
|
+
const exe = command.replace(/^"([^"]+)".*/, "$1").split(/[\s/\\]+/).pop() ?? command;
|
|
43
|
+
console.error(chalk.dim(` [${label}] Started (PID: ${pid}, cmd: ${exe})`));
|
|
44
|
+
},
|
|
45
|
+
onStderr(_chunk) {
|
|
46
|
+
},
|
|
47
|
+
onProgress(chunk) {
|
|
48
|
+
const data = carryOver + chunk;
|
|
49
|
+
const lines = data.split("\n");
|
|
50
|
+
carryOver = lines.pop() ?? "";
|
|
51
|
+
if (carryOver.length > MAX_CARRY_OVER) {
|
|
52
|
+
carryOver = "";
|
|
53
|
+
droppedEvents++;
|
|
54
|
+
}
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
parseLine(line);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
onClose() {
|
|
60
|
+
if (carryOver.trim()) {
|
|
61
|
+
parseLine(carryOver);
|
|
62
|
+
carryOver = "";
|
|
63
|
+
}
|
|
64
|
+
if (droppedEvents > 0) {
|
|
65
|
+
console.error(chalk.dim(` [${label}] ${droppedEvents} event(s) dropped (parse errors or buffer overflow)`));
|
|
66
|
+
droppedEvents = 0;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
onHeartbeat(elapsedSec) {
|
|
70
|
+
if (elapsedSec % 60 === 0) {
|
|
71
|
+
printActivity(`${elapsedSec}s elapsed...`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function formatEvent(event, print) {
|
|
77
|
+
const type = event.type;
|
|
78
|
+
if (type === "thread.started") {
|
|
79
|
+
const tid = event.thread_id ?? "";
|
|
80
|
+
print(`Thread: ${tid.slice(0, 12)}...`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (type === "item.completed") {
|
|
84
|
+
const item = event.item;
|
|
85
|
+
if (!item) return;
|
|
86
|
+
if (item.type === "tool_call" || item.type === "function_call") {
|
|
87
|
+
const name = item.name ?? item.function ?? "tool";
|
|
88
|
+
const rawArgs = item.arguments ?? item.input ?? "";
|
|
89
|
+
const args = (typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs)).slice(0, 80);
|
|
90
|
+
const pathMatch = args.match(/["']([^"']*\.[a-z]{1,4})["']/i);
|
|
91
|
+
if (pathMatch) {
|
|
92
|
+
print(`${name}: ${pathMatch[1]}`);
|
|
93
|
+
} else {
|
|
94
|
+
print(`${name}${args ? `: ${args.slice(0, 60)}` : ""}`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (type === "turn.completed") {
|
|
100
|
+
const usage = event.usage;
|
|
101
|
+
if (usage) {
|
|
102
|
+
const input = (usage.input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
103
|
+
const output = usage.output_tokens ?? 0;
|
|
104
|
+
print(`Turn done (${input} in / ${output} out tokens)`);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/utils.ts
|
|
111
|
+
import { openDatabase } from "@mulep/core";
|
|
112
|
+
import chalk2 from "chalk";
|
|
113
|
+
import { mkdirSync } from "fs";
|
|
114
|
+
import { join } from "path";
|
|
115
|
+
function getDbPath(projectDir) {
|
|
116
|
+
const base = projectDir ?? process.cwd();
|
|
117
|
+
const dbDir = join(base, ".mulep", "db");
|
|
118
|
+
mkdirSync(dbDir, { recursive: true });
|
|
119
|
+
return join(dbDir, "mulep.db");
|
|
120
|
+
}
|
|
121
|
+
async function withDatabase(fn) {
|
|
122
|
+
const db = openDatabase(getDbPath());
|
|
123
|
+
const originalExit = process.exit;
|
|
124
|
+
let requestedExitCode;
|
|
125
|
+
process.exit = ((code) => {
|
|
126
|
+
requestedExitCode = typeof code === "number" ? code : 1;
|
|
127
|
+
throw new Error("__WITH_DATABASE_EXIT__");
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
return await fn(db);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (requestedExitCode === void 0) {
|
|
133
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
} finally {
|
|
137
|
+
process.exit = originalExit;
|
|
138
|
+
db.close();
|
|
139
|
+
if (requestedExitCode !== void 0) {
|
|
140
|
+
originalExit(requestedExitCode);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/commands/build.ts
|
|
146
|
+
async function buildStartCommand(task, options) {
|
|
147
|
+
let db;
|
|
148
|
+
try {
|
|
149
|
+
const buildId = generateId();
|
|
150
|
+
const dbPath = getDbPath();
|
|
151
|
+
db = openDatabase2(dbPath);
|
|
152
|
+
const buildStore = new BuildStore(db);
|
|
153
|
+
const debateStore = new DebateStore(db);
|
|
154
|
+
const projectDir = process.cwd();
|
|
155
|
+
let baselineRef = null;
|
|
156
|
+
try {
|
|
157
|
+
const dirty = execSync("git status --porcelain", { cwd: projectDir, encoding: "utf-8" }).trim();
|
|
158
|
+
if (dirty && !options.allowDirty) {
|
|
159
|
+
db.close();
|
|
160
|
+
console.error(chalk3.red("Working tree is dirty. Commit or stash your changes first."));
|
|
161
|
+
console.error(chalk3.yellow("Use --allow-dirty to auto-stash."));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (dirty && options.allowDirty) {
|
|
165
|
+
execSync('git stash push -u -m "mulep-build-baseline"', { cwd: projectDir, encoding: "utf-8" });
|
|
166
|
+
console.error(chalk3.yellow('Auto-stashed dirty changes with marker "mulep-build-baseline"'));
|
|
167
|
+
}
|
|
168
|
+
baselineRef = execSync("git rev-parse HEAD", { cwd: projectDir, encoding: "utf-8" }).trim();
|
|
169
|
+
} catch {
|
|
170
|
+
console.error(chalk3.yellow("Warning: Not a git repository. No baseline tracking."));
|
|
171
|
+
}
|
|
172
|
+
const debateId = generateId();
|
|
173
|
+
debateStore.upsert({ debateId, role: "proposer", status: "active" });
|
|
174
|
+
debateStore.upsert({ debateId, role: "critic", status: "active" });
|
|
175
|
+
debateStore.saveState(debateId, "proposer", {
|
|
176
|
+
debateId,
|
|
177
|
+
question: task,
|
|
178
|
+
models: ["codex-architect", "codex-reviewer"],
|
|
179
|
+
round: 0,
|
|
180
|
+
turn: 0,
|
|
181
|
+
thread: [],
|
|
182
|
+
runningSummary: "",
|
|
183
|
+
stanceHistory: [],
|
|
184
|
+
usage: { totalPromptTokens: 0, totalCompletionTokens: 0, totalCalls: 0, startedAt: Date.now() },
|
|
185
|
+
status: "running",
|
|
186
|
+
sessionIds: {},
|
|
187
|
+
resumeStats: { attempted: 0, succeeded: 0, fallbacks: 0 }
|
|
188
|
+
});
|
|
189
|
+
const sessionMgr = new SessionManager(db);
|
|
190
|
+
const session2 = sessionMgr.resolveActive("build");
|
|
191
|
+
sessionMgr.recordEvent({
|
|
192
|
+
sessionId: session2.id,
|
|
193
|
+
command: "build",
|
|
194
|
+
subcommand: "start",
|
|
195
|
+
promptPreview: `Build started: ${task}`
|
|
196
|
+
});
|
|
197
|
+
buildStore.create({ buildId, task, debateId, baselineRef: baselineRef ?? void 0 });
|
|
198
|
+
buildStore.updateWithEvent(
|
|
199
|
+
buildId,
|
|
200
|
+
{ debateId },
|
|
201
|
+
{ eventType: "debate_started", actor: "system", phase: "debate", payload: { task, debateId, baselineRef } }
|
|
202
|
+
);
|
|
203
|
+
const output = {
|
|
204
|
+
buildId,
|
|
205
|
+
debateId,
|
|
206
|
+
task,
|
|
207
|
+
baselineRef,
|
|
208
|
+
sessionId: session2.id,
|
|
209
|
+
maxRounds: options.maxRounds ?? 5,
|
|
210
|
+
status: "planning",
|
|
211
|
+
phase: "debate"
|
|
212
|
+
};
|
|
213
|
+
console.log(JSON.stringify(output, null, 2));
|
|
214
|
+
db.close();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
db?.close();
|
|
217
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function buildStatusCommand(buildId) {
|
|
222
|
+
let db;
|
|
223
|
+
try {
|
|
224
|
+
const dbPath = getDbPath();
|
|
225
|
+
db = openDatabase2(dbPath);
|
|
226
|
+
const store = new BuildStore(db);
|
|
227
|
+
const run = store.get(buildId);
|
|
228
|
+
if (!run) {
|
|
229
|
+
db.close();
|
|
230
|
+
console.error(chalk3.red(`No build found with ID: ${buildId}`));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const events = store.getEvents(buildId);
|
|
234
|
+
const bugsFound = store.countEventsByType(buildId, "bug_found");
|
|
235
|
+
const fixesApplied = store.countEventsByType(buildId, "fix_completed");
|
|
236
|
+
const output = {
|
|
237
|
+
buildId: run.buildId,
|
|
238
|
+
task: run.task,
|
|
239
|
+
status: run.status,
|
|
240
|
+
phase: run.currentPhase,
|
|
241
|
+
loop: run.currentLoop,
|
|
242
|
+
debateId: run.debateId,
|
|
243
|
+
baselineRef: run.baselineRef,
|
|
244
|
+
planCodexSession: run.planCodexSession,
|
|
245
|
+
reviewCodexSession: run.reviewCodexSession,
|
|
246
|
+
planVersion: run.planVersion,
|
|
247
|
+
reviewCycles: run.reviewCycles,
|
|
248
|
+
bugsFound,
|
|
249
|
+
bugsFixed: fixesApplied,
|
|
250
|
+
totalEvents: events.length,
|
|
251
|
+
createdAt: new Date(run.createdAt).toISOString(),
|
|
252
|
+
updatedAt: new Date(run.updatedAt).toISOString(),
|
|
253
|
+
completedAt: run.completedAt ? new Date(run.completedAt).toISOString() : null,
|
|
254
|
+
recentEvents: events.slice(-10).map((e) => ({
|
|
255
|
+
seq: e.seq,
|
|
256
|
+
type: e.eventType,
|
|
257
|
+
actor: e.actor,
|
|
258
|
+
phase: e.phase,
|
|
259
|
+
loop: e.loopIndex,
|
|
260
|
+
tokens: e.tokensUsed,
|
|
261
|
+
time: new Date(e.createdAt).toISOString()
|
|
262
|
+
}))
|
|
263
|
+
};
|
|
264
|
+
console.log(JSON.stringify(output, null, 2));
|
|
265
|
+
db.close();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
db?.close();
|
|
268
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function buildListCommand(options) {
|
|
273
|
+
let db;
|
|
274
|
+
try {
|
|
275
|
+
const dbPath = getDbPath();
|
|
276
|
+
db = openDatabase2(dbPath);
|
|
277
|
+
const store = new BuildStore(db);
|
|
278
|
+
const builds = store.list({
|
|
279
|
+
status: options.status,
|
|
280
|
+
limit: options.limit ?? 20
|
|
281
|
+
});
|
|
282
|
+
const output = builds.map((b) => ({
|
|
283
|
+
buildId: b.buildId,
|
|
284
|
+
task: b.task,
|
|
285
|
+
status: b.status,
|
|
286
|
+
phase: b.phase,
|
|
287
|
+
loop: b.loop,
|
|
288
|
+
reviewCycles: b.reviewCycles,
|
|
289
|
+
createdAt: new Date(b.createdAt).toISOString(),
|
|
290
|
+
updatedAt: new Date(b.updatedAt).toISOString()
|
|
291
|
+
}));
|
|
292
|
+
console.log(JSON.stringify(output, null, 2));
|
|
293
|
+
db.close();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
db?.close();
|
|
296
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function buildEventCommand(buildId, eventType, options) {
|
|
301
|
+
let db;
|
|
302
|
+
try {
|
|
303
|
+
const dbPath = getDbPath();
|
|
304
|
+
db = openDatabase2(dbPath);
|
|
305
|
+
const store = new BuildStore(db);
|
|
306
|
+
const run = store.get(buildId);
|
|
307
|
+
if (!run) {
|
|
308
|
+
db.close();
|
|
309
|
+
console.error(chalk3.red(`No build found with ID: ${buildId}`));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
let payload;
|
|
313
|
+
if (!process.stdin.isTTY) {
|
|
314
|
+
const chunks = [];
|
|
315
|
+
for await (const chunk of process.stdin) {
|
|
316
|
+
chunks.push(chunk);
|
|
317
|
+
}
|
|
318
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
319
|
+
if (input) {
|
|
320
|
+
try {
|
|
321
|
+
payload = JSON.parse(input);
|
|
322
|
+
} catch {
|
|
323
|
+
payload = { text: input };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const updates = {};
|
|
328
|
+
if (eventType === "plan_approved") {
|
|
329
|
+
updates.currentPhase = "plan_approved";
|
|
330
|
+
updates.status = "implementing";
|
|
331
|
+
updates.planVersion = run.planVersion + 1;
|
|
332
|
+
} else if (eventType === "impl_completed") {
|
|
333
|
+
updates.currentPhase = "review";
|
|
334
|
+
updates.status = "reviewing";
|
|
335
|
+
} else if (eventType === "review_verdict") {
|
|
336
|
+
const verdict = payload?.verdict;
|
|
337
|
+
if (verdict === "approved") {
|
|
338
|
+
updates.currentPhase = "done";
|
|
339
|
+
updates.status = "completed";
|
|
340
|
+
updates.completedAt = Date.now();
|
|
341
|
+
} else {
|
|
342
|
+
updates.currentPhase = "fix";
|
|
343
|
+
updates.status = "fixing";
|
|
344
|
+
updates.reviewCycles = run.reviewCycles + 1;
|
|
345
|
+
}
|
|
346
|
+
} else if (eventType === "fix_completed") {
|
|
347
|
+
updates.currentPhase = "review";
|
|
348
|
+
updates.status = "reviewing";
|
|
349
|
+
updates.currentLoop = run.currentLoop + 1;
|
|
350
|
+
}
|
|
351
|
+
store.updateWithEvent(
|
|
352
|
+
buildId,
|
|
353
|
+
updates,
|
|
354
|
+
{
|
|
355
|
+
eventType,
|
|
356
|
+
actor: "system",
|
|
357
|
+
phase: updates.currentPhase ?? run.currentPhase,
|
|
358
|
+
loopIndex: options.loop ?? run.currentLoop,
|
|
359
|
+
payload,
|
|
360
|
+
tokensUsed: options.tokens ?? 0
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
const updated = store.get(buildId);
|
|
364
|
+
console.log(JSON.stringify({
|
|
365
|
+
buildId,
|
|
366
|
+
eventType,
|
|
367
|
+
newStatus: updated?.status,
|
|
368
|
+
newPhase: updated?.currentPhase,
|
|
369
|
+
seq: updated?.lastEventSeq
|
|
370
|
+
}));
|
|
371
|
+
db.close();
|
|
372
|
+
} catch (error) {
|
|
373
|
+
db?.close();
|
|
374
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function buildReviewCommand(buildId) {
|
|
379
|
+
let db;
|
|
380
|
+
try {
|
|
381
|
+
const dbPath = getDbPath();
|
|
382
|
+
db = openDatabase2(dbPath);
|
|
383
|
+
const buildStore = new BuildStore(db);
|
|
384
|
+
const config = loadConfig();
|
|
385
|
+
const projectDir = process.cwd();
|
|
386
|
+
const run = buildStore.get(buildId);
|
|
387
|
+
if (!run) {
|
|
388
|
+
db.close();
|
|
389
|
+
console.error(chalk3.red(`No build found with ID: ${buildId}`));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
let diff = "";
|
|
393
|
+
if (run.baselineRef) {
|
|
394
|
+
try {
|
|
395
|
+
const tmpIndex = join2(projectDir, ".git", "mulep-review-index");
|
|
396
|
+
try {
|
|
397
|
+
execFileSync("git", ["read-tree", "HEAD"], { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
398
|
+
execFileSync("git", ["add", "-A"], { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
399
|
+
diff = execFileSync("git", ["diff", "--cached", run.baselineRef, "--"], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
400
|
+
} finally {
|
|
401
|
+
try {
|
|
402
|
+
unlinkSync(tmpIndex);
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.error(chalk3.red(`Failed to generate diff: ${err instanceof Error ? err.message : String(err)}`));
|
|
408
|
+
db.close();
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!diff.trim()) {
|
|
413
|
+
db.close();
|
|
414
|
+
console.error(chalk3.yellow("No changes detected against baseline."));
|
|
415
|
+
process.exit(0);
|
|
416
|
+
}
|
|
417
|
+
const { ModelRegistry: ModelRegistry8, CliAdapter: CliAdapterClass } = await import("@mulep/core");
|
|
418
|
+
const registry = ModelRegistry8.fromConfig(config, projectDir);
|
|
419
|
+
const adapter = registry.getAdapter("codex-reviewer") ?? registry.getAdapter("codex-architect");
|
|
420
|
+
if (!adapter) {
|
|
421
|
+
db.close();
|
|
422
|
+
console.error(chalk3.red("No codex adapter found in config"));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const sessionMgr = new SessionManager(db);
|
|
426
|
+
const session2 = sessionMgr.resolveActive("build-review");
|
|
427
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
428
|
+
const existingSession = currentSession?.codexThreadId ?? run.reviewCodexSession ?? void 0;
|
|
429
|
+
const prompt = buildHandoffEnvelope({
|
|
430
|
+
command: "build-review",
|
|
431
|
+
task: `Review code changes for the task: "${run.task}"
|
|
432
|
+
|
|
433
|
+
GIT DIFF (against baseline ${run.baselineRef}):
|
|
434
|
+
${diff.slice(0, REVIEW_DIFF_MAX_CHARS)}
|
|
435
|
+
|
|
436
|
+
Review for:
|
|
437
|
+
1. Correctness \u2014 does the code work as intended?
|
|
438
|
+
2. Bugs \u2014 any logic errors, edge cases, or crashes?
|
|
439
|
+
3. Security \u2014 any vulnerabilities introduced?
|
|
440
|
+
4. Code quality \u2014 naming, structure, patterns
|
|
441
|
+
5. Completeness \u2014 does it fully implement the task?`,
|
|
442
|
+
resumed: Boolean(existingSession),
|
|
443
|
+
constraints: run.reviewCycles > 0 ? [`This is review cycle ${run.reviewCycles + 1}. Focus on whether prior issues were addressed.`] : void 0
|
|
444
|
+
});
|
|
445
|
+
const progress = createProgressCallbacks("build-review");
|
|
446
|
+
let result;
|
|
447
|
+
try {
|
|
448
|
+
result = await adapter.callWithResume(prompt, {
|
|
449
|
+
sessionId: existingSession,
|
|
450
|
+
timeout: 6e5,
|
|
451
|
+
...progress
|
|
452
|
+
});
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (existingSession) {
|
|
455
|
+
console.error(chalk3.yellow(" Clearing stale codex thread ID after failure."));
|
|
456
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
457
|
+
}
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
if (existingSession && result.sessionId !== existingSession) {
|
|
461
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
462
|
+
}
|
|
463
|
+
if (result.sessionId) {
|
|
464
|
+
sessionMgr.updateThreadId(session2.id, result.sessionId);
|
|
465
|
+
}
|
|
466
|
+
sessionMgr.addUsageFromResult(session2.id, result.usage, prompt, result.text);
|
|
467
|
+
sessionMgr.recordEvent({
|
|
468
|
+
sessionId: session2.id,
|
|
469
|
+
command: "build",
|
|
470
|
+
subcommand: "review",
|
|
471
|
+
promptPreview: `Build review for ${buildId}: ${run.task}`,
|
|
472
|
+
responsePreview: result.text.slice(0, 500),
|
|
473
|
+
promptFull: prompt,
|
|
474
|
+
responseFull: result.text,
|
|
475
|
+
usageJson: JSON.stringify(result.usage),
|
|
476
|
+
durationMs: result.durationMs,
|
|
477
|
+
codexThreadId: result.sessionId
|
|
478
|
+
});
|
|
479
|
+
const tail = result.text.slice(-500);
|
|
480
|
+
const verdictMatch = tail.match(/^(?:-\s*)?VERDICT:\s*(APPROVED|NEEDS_REVISION)/m);
|
|
481
|
+
const approved = verdictMatch?.[1] === "APPROVED";
|
|
482
|
+
buildStore.updateWithEvent(
|
|
483
|
+
buildId,
|
|
484
|
+
{
|
|
485
|
+
reviewCodexSession: result.sessionId ?? void 0,
|
|
486
|
+
reviewCycles: run.reviewCycles + 1
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
eventType: "review_verdict",
|
|
490
|
+
actor: "codex",
|
|
491
|
+
phase: "review",
|
|
492
|
+
loopIndex: run.currentLoop,
|
|
493
|
+
payload: {
|
|
494
|
+
verdict: approved ? "approved" : "needs_revision",
|
|
495
|
+
response: result.text.slice(0, REVIEW_TEXT_MAX_CHARS),
|
|
496
|
+
sessionId: result.sessionId,
|
|
497
|
+
resumed: existingSession ? result.sessionId === existingSession : false
|
|
498
|
+
},
|
|
499
|
+
codexThreadId: result.sessionId,
|
|
500
|
+
tokensUsed: result.usage.totalTokens
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
if (approved) {
|
|
504
|
+
buildStore.updateWithEvent(
|
|
505
|
+
buildId,
|
|
506
|
+
{ currentPhase: "done", status: "completed", completedAt: Date.now() },
|
|
507
|
+
{ eventType: "phase_transition", actor: "system", phase: "done", payload: { reason: "review_approved" } }
|
|
508
|
+
);
|
|
509
|
+
} else {
|
|
510
|
+
buildStore.updateWithEvent(
|
|
511
|
+
buildId,
|
|
512
|
+
{ currentPhase: "fix", status: "fixing" },
|
|
513
|
+
{ eventType: "phase_transition", actor: "system", phase: "fix", payload: { reason: "review_needs_revision" } }
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const output = {
|
|
517
|
+
buildId,
|
|
518
|
+
review: result.text.slice(0, 2e3),
|
|
519
|
+
verdict: approved ? "approved" : "needs_revision",
|
|
520
|
+
sessionId: result.sessionId,
|
|
521
|
+
resumed: existingSession ? result.sessionId === existingSession : false,
|
|
522
|
+
tokens: result.usage,
|
|
523
|
+
durationMs: result.durationMs
|
|
524
|
+
};
|
|
525
|
+
console.log(JSON.stringify(output, null, 2));
|
|
526
|
+
db.close();
|
|
527
|
+
} catch (error) {
|
|
528
|
+
db?.close();
|
|
529
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/commands/debate.ts
|
|
535
|
+
import {
|
|
536
|
+
DebateStore as DebateStore2,
|
|
537
|
+
MessageStore,
|
|
538
|
+
ModelRegistry,
|
|
539
|
+
SessionManager as SessionManager2,
|
|
540
|
+
buildReconstructionPrompt,
|
|
541
|
+
generateId as generateId2,
|
|
542
|
+
getTokenBudgetStatus,
|
|
543
|
+
loadConfig as loadConfig2,
|
|
544
|
+
openDatabase as openDatabase3,
|
|
545
|
+
parseDebateVerdict,
|
|
546
|
+
preflightTokenCheck
|
|
547
|
+
} from "@mulep/core";
|
|
548
|
+
import chalk4 from "chalk";
|
|
549
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
550
|
+
import { join as join3 } from "path";
|
|
551
|
+
async function debateStartCommand(topic, options) {
|
|
552
|
+
let db;
|
|
553
|
+
try {
|
|
554
|
+
const debateId = generateId2();
|
|
555
|
+
const dbPath = getDbPath();
|
|
556
|
+
db = openDatabase3(dbPath);
|
|
557
|
+
const store = new DebateStore2(db);
|
|
558
|
+
store.upsert({ debateId, role: "proposer", status: "active" });
|
|
559
|
+
store.upsert({ debateId, role: "critic", status: "active" });
|
|
560
|
+
store.saveState(debateId, "proposer", {
|
|
561
|
+
debateId,
|
|
562
|
+
question: topic,
|
|
563
|
+
models: ["codex-architect", "codex-reviewer"],
|
|
564
|
+
round: 0,
|
|
565
|
+
turn: 0,
|
|
566
|
+
thread: [],
|
|
567
|
+
runningSummary: "",
|
|
568
|
+
stanceHistory: [],
|
|
569
|
+
usage: { totalPromptTokens: 0, totalCompletionTokens: 0, totalCalls: 0, startedAt: Date.now() },
|
|
570
|
+
status: "running",
|
|
571
|
+
sessionIds: {},
|
|
572
|
+
resumeStats: { attempted: 0, succeeded: 0, fallbacks: 0 },
|
|
573
|
+
maxRounds: options.maxRounds ?? 5,
|
|
574
|
+
defaultTimeout: options.timeout
|
|
575
|
+
});
|
|
576
|
+
const output = {
|
|
577
|
+
debateId,
|
|
578
|
+
topic,
|
|
579
|
+
maxRounds: options.maxRounds ?? 5,
|
|
580
|
+
status: "started"
|
|
581
|
+
};
|
|
582
|
+
console.log(JSON.stringify(output, null, 2));
|
|
583
|
+
db.close();
|
|
584
|
+
} catch (error) {
|
|
585
|
+
db?.close();
|
|
586
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
var DEFAULT_RESPONSE_CAP = 16384;
|
|
591
|
+
var MAX_RESPONSE_CAP = 24576;
|
|
592
|
+
function buildResponseOutput(debateId, round, responseText, cap, explicitOutput) {
|
|
593
|
+
const text = responseText ?? "";
|
|
594
|
+
const byteLen = Buffer.byteLength(text, "utf-8");
|
|
595
|
+
const truncated = byteLen > cap;
|
|
596
|
+
let responseFile;
|
|
597
|
+
if (truncated && !explicitOutput) {
|
|
598
|
+
const dir = join3(process.cwd(), ".mulep", "debates");
|
|
599
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
600
|
+
responseFile = join3(dir, `${debateId}-r${round}.txt`);
|
|
601
|
+
writeFileSync(responseFile, text, { encoding: "utf-8", mode: 384 });
|
|
602
|
+
} else if (explicitOutput && text.length > 0) {
|
|
603
|
+
responseFile = explicitOutput;
|
|
604
|
+
}
|
|
605
|
+
let sliced = text;
|
|
606
|
+
if (truncated) {
|
|
607
|
+
let lo = 0;
|
|
608
|
+
let hi = text.length;
|
|
609
|
+
while (lo < hi) {
|
|
610
|
+
const mid = lo + hi + 1 >>> 1;
|
|
611
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf-8") <= cap) lo = mid;
|
|
612
|
+
else hi = mid - 1;
|
|
613
|
+
}
|
|
614
|
+
sliced = text.slice(0, lo);
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
response: sliced,
|
|
618
|
+
responseTruncated: truncated,
|
|
619
|
+
responseBytes: byteLen,
|
|
620
|
+
responseCap: cap,
|
|
621
|
+
responseFile
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async function debateTurnCommand(debateId, prompt, options) {
|
|
625
|
+
let db;
|
|
626
|
+
try {
|
|
627
|
+
const dbPath = getDbPath();
|
|
628
|
+
db = openDatabase3(dbPath);
|
|
629
|
+
const store = new DebateStore2(db);
|
|
630
|
+
const msgStore = new MessageStore(db);
|
|
631
|
+
const config = loadConfig2();
|
|
632
|
+
const projectDir = process.cwd();
|
|
633
|
+
const registry = ModelRegistry.fromConfig(config, projectDir);
|
|
634
|
+
const criticRow = store.get(debateId, "critic");
|
|
635
|
+
if (!criticRow) {
|
|
636
|
+
db.close();
|
|
637
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
if (criticRow.status === "completed") {
|
|
641
|
+
db.close();
|
|
642
|
+
console.error(chalk4.red(`Debate ${debateId} is already completed. Start a new debate to continue discussion.`));
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
646
|
+
if (!adapter) {
|
|
647
|
+
db.close();
|
|
648
|
+
console.error(chalk4.red("No codex adapter found in config. Available: codex-reviewer or codex-architect"));
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
const rawRound = options.round ?? criticRow.round + 1;
|
|
652
|
+
const newRound = Number.isFinite(rawRound) && rawRound > 0 ? rawRound : criticRow.round + 1;
|
|
653
|
+
const responseCap = Math.min(
|
|
654
|
+
options.responseCap && options.responseCap > 0 ? options.responseCap : DEFAULT_RESPONSE_CAP,
|
|
655
|
+
MAX_RESPONSE_CAP
|
|
656
|
+
);
|
|
657
|
+
const quiet = options.quiet ?? false;
|
|
658
|
+
const proposerStateForLimit = store.loadState(debateId, "proposer");
|
|
659
|
+
const storedTimeout = proposerStateForLimit?.defaultTimeout;
|
|
660
|
+
const rawTimeout = options.timeout ?? storedTimeout ?? 600;
|
|
661
|
+
const timeout = (Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 600) * 1e3;
|
|
662
|
+
const rawMax = proposerStateForLimit?.maxRounds ?? 5;
|
|
663
|
+
const maxRounds = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 5;
|
|
664
|
+
if (newRound > maxRounds) {
|
|
665
|
+
console.error(chalk4.red(`Round ${newRound} exceeds max rounds (${maxRounds}). Complete or increase limit.`));
|
|
666
|
+
db.close();
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
const existing = msgStore.getByRound(debateId, newRound, "critic");
|
|
670
|
+
if (existing?.status === "completed") {
|
|
671
|
+
if (criticRow.round < newRound) {
|
|
672
|
+
store.upsert({
|
|
673
|
+
debateId,
|
|
674
|
+
role: "critic",
|
|
675
|
+
codexSessionId: existing.sessionId ?? criticRow.codexSessionId ?? void 0,
|
|
676
|
+
round: newRound,
|
|
677
|
+
status: "active"
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (options.output && existing.responseText) {
|
|
681
|
+
writeFileSync(options.output, existing.responseText, "utf-8");
|
|
682
|
+
if (!quiet) console.error(chalk4.dim(`Full response written to ${options.output}`));
|
|
683
|
+
}
|
|
684
|
+
const responseFields = buildResponseOutput(debateId, newRound, existing.responseText, responseCap, options.output);
|
|
685
|
+
const output = {
|
|
686
|
+
debateId,
|
|
687
|
+
round: newRound,
|
|
688
|
+
...responseFields,
|
|
689
|
+
sessionId: existing.sessionId,
|
|
690
|
+
resumed: false,
|
|
691
|
+
cached: true,
|
|
692
|
+
usage: existing.usageJson ? (() => {
|
|
693
|
+
try {
|
|
694
|
+
return JSON.parse(existing.usageJson);
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
})() : null,
|
|
699
|
+
durationMs: existing.durationMs
|
|
700
|
+
};
|
|
701
|
+
console.log(JSON.stringify(output, null, 2));
|
|
702
|
+
db.close();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const staleThreshold = timeout + 6e4;
|
|
706
|
+
const recovered = msgStore.recoverStaleForDebate(debateId, staleThreshold);
|
|
707
|
+
if (recovered > 0 && !quiet) {
|
|
708
|
+
console.error(chalk4.yellow(` Recovered ${recovered} stale message(s) from prior crash.`));
|
|
709
|
+
}
|
|
710
|
+
const current = msgStore.getByRound(debateId, newRound, "critic");
|
|
711
|
+
let msgId;
|
|
712
|
+
if (current) {
|
|
713
|
+
msgId = current.id;
|
|
714
|
+
if (current.status === "failed" || current.status === "queued") {
|
|
715
|
+
msgStore.updatePrompt(msgId, prompt);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
msgId = msgStore.insertQueued({
|
|
719
|
+
debateId,
|
|
720
|
+
round: newRound,
|
|
721
|
+
role: "critic",
|
|
722
|
+
bridge: "codex",
|
|
723
|
+
model: adapter.modelId ?? "codex",
|
|
724
|
+
promptText: prompt
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (!msgStore.markRunning(msgId)) {
|
|
728
|
+
const recheckRow = msgStore.getByRound(debateId, newRound, "critic");
|
|
729
|
+
if (recheckRow?.status === "completed") {
|
|
730
|
+
if (options.output && recheckRow.responseText) {
|
|
731
|
+
writeFileSync(options.output, recheckRow.responseText, "utf-8");
|
|
732
|
+
if (!quiet) console.error(chalk4.dim(`Full response written to ${options.output}`));
|
|
733
|
+
}
|
|
734
|
+
const recheckResponseFields = buildResponseOutput(debateId, newRound, recheckRow.responseText, responseCap, options.output);
|
|
735
|
+
const output = {
|
|
736
|
+
debateId,
|
|
737
|
+
round: newRound,
|
|
738
|
+
...recheckResponseFields,
|
|
739
|
+
sessionId: recheckRow.sessionId,
|
|
740
|
+
resumed: false,
|
|
741
|
+
cached: true,
|
|
742
|
+
usage: recheckRow.usageJson ? (() => {
|
|
743
|
+
try {
|
|
744
|
+
return JSON.parse(recheckRow.usageJson);
|
|
745
|
+
} catch {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
})() : null,
|
|
749
|
+
durationMs: recheckRow.durationMs
|
|
750
|
+
};
|
|
751
|
+
console.log(JSON.stringify(output, null, 2));
|
|
752
|
+
db.close();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
db.close();
|
|
756
|
+
console.error(chalk4.red(`Cannot transition message ${msgId} to running (current status: ${recheckRow?.status})`));
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
const sessionMgr = new SessionManager2(db);
|
|
760
|
+
const unifiedSession = sessionMgr.resolveActive("debate");
|
|
761
|
+
let existingSessionId = unifiedSession.codexThreadId ?? criticRow.codexSessionId ?? void 0;
|
|
762
|
+
const attemptedResume = existingSessionId != null;
|
|
763
|
+
const completedHistory = msgStore.getHistory(debateId).filter((m) => m.status === "completed");
|
|
764
|
+
const maxContext = adapter.capabilities.maxContextTokens;
|
|
765
|
+
const preflight = preflightTokenCheck(completedHistory, prompt, maxContext);
|
|
766
|
+
if (preflight.shouldStop && !options.force) {
|
|
767
|
+
console.error(chalk4.red(`Token budget at ${Math.round(preflight.utilizationRatio * 100)}% (${preflight.totalTokensUsed}/${maxContext}). Debate blocked \u2014 use --force to override.`));
|
|
768
|
+
db.close();
|
|
769
|
+
process.exit(1);
|
|
770
|
+
} else if (preflight.shouldStop && options.force) {
|
|
771
|
+
if (!quiet) console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}% (${preflight.totalTokensUsed}/${maxContext}) \u2014 forced past budget limit.`));
|
|
772
|
+
} else if (preflight.shouldSummarize) {
|
|
773
|
+
if (!quiet) console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}%. Older rounds will be summarized on resume failure.`));
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
const progress = createProgressCallbacks("debate");
|
|
777
|
+
let result;
|
|
778
|
+
try {
|
|
779
|
+
result = await adapter.callWithResume(prompt, {
|
|
780
|
+
sessionId: existingSessionId,
|
|
781
|
+
timeout,
|
|
782
|
+
...progress
|
|
783
|
+
});
|
|
784
|
+
} catch (err) {
|
|
785
|
+
if (existingSessionId) {
|
|
786
|
+
sessionMgr.updateThreadId(unifiedSession.id, null);
|
|
787
|
+
}
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
if (existingSessionId && result.sessionId !== existingSessionId) {
|
|
791
|
+
sessionMgr.updateThreadId(unifiedSession.id, null);
|
|
792
|
+
}
|
|
793
|
+
const resumed = attemptedResume && result.sessionId === existingSessionId;
|
|
794
|
+
const resumeFailed = attemptedResume && !resumed;
|
|
795
|
+
if (resumeFailed && result.text.length < 50) {
|
|
796
|
+
if (!quiet) console.error(chalk4.yellow(" Resume failed with minimal response. Reconstructing from ledger..."));
|
|
797
|
+
const history = msgStore.getHistory(debateId);
|
|
798
|
+
const reconstructed = buildReconstructionPrompt(history, prompt);
|
|
799
|
+
result = await adapter.callWithResume(reconstructed, {
|
|
800
|
+
timeout,
|
|
801
|
+
...progress
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (result.text.length < 200 && (result.durationMs ?? 0) > 6e4) {
|
|
805
|
+
if (!quiet) console.error(chalk4.yellow(` Warning: GPT response is only ${result.text.length} chars after ${Math.round((result.durationMs ?? 0) / 1e3)}s \u2014 possible output truncation (codex may have spent its turn on tool calls).`));
|
|
806
|
+
}
|
|
807
|
+
const verdict = parseDebateVerdict(result.text);
|
|
808
|
+
const completed = msgStore.markCompleted(msgId, {
|
|
809
|
+
responseText: result.text,
|
|
810
|
+
verdict,
|
|
811
|
+
usageJson: JSON.stringify(result.usage),
|
|
812
|
+
durationMs: result.durationMs ?? 0,
|
|
813
|
+
sessionId: result.sessionId ?? null
|
|
814
|
+
});
|
|
815
|
+
if (!completed) {
|
|
816
|
+
console.error(chalk4.red(`Message ${msgId} ledger transition to completed failed (possible concurrent invocation or state drift).`));
|
|
817
|
+
db.close();
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
if (resumeFailed) {
|
|
821
|
+
store.incrementResumeFailCount(debateId, "critic");
|
|
822
|
+
}
|
|
823
|
+
if (result.sessionId) {
|
|
824
|
+
sessionMgr.updateThreadId(unifiedSession.id, result.sessionId);
|
|
825
|
+
}
|
|
826
|
+
sessionMgr.addUsageFromResult(unifiedSession.id, result.usage, prompt, result.text);
|
|
827
|
+
sessionMgr.recordEvent({
|
|
828
|
+
sessionId: unifiedSession.id,
|
|
829
|
+
command: "debate",
|
|
830
|
+
subcommand: "turn",
|
|
831
|
+
promptPreview: prompt.slice(0, 500),
|
|
832
|
+
responsePreview: result.text.slice(0, 500),
|
|
833
|
+
promptFull: prompt,
|
|
834
|
+
responseFull: result.text,
|
|
835
|
+
usageJson: JSON.stringify(result.usage),
|
|
836
|
+
durationMs: result.durationMs,
|
|
837
|
+
codexThreadId: result.sessionId
|
|
838
|
+
});
|
|
839
|
+
store.upsert({
|
|
840
|
+
debateId,
|
|
841
|
+
role: "critic",
|
|
842
|
+
codexSessionId: result.sessionId ?? void 0,
|
|
843
|
+
round: newRound,
|
|
844
|
+
status: "active"
|
|
845
|
+
});
|
|
846
|
+
const proposerState = store.loadState(debateId, "proposer");
|
|
847
|
+
if (proposerState) {
|
|
848
|
+
const stats = proposerState.resumeStats ?? { attempted: 0, succeeded: 0, fallbacks: 0 };
|
|
849
|
+
if (attemptedResume) stats.attempted++;
|
|
850
|
+
if (resumed) stats.succeeded++;
|
|
851
|
+
if (resumeFailed) stats.fallbacks++;
|
|
852
|
+
proposerState.resumeStats = stats;
|
|
853
|
+
store.saveState(debateId, "proposer", proposerState);
|
|
854
|
+
}
|
|
855
|
+
if (options.output) {
|
|
856
|
+
writeFileSync(options.output, result.text, "utf-8");
|
|
857
|
+
if (!quiet) console.error(chalk4.dim(`Full response written to ${options.output}`));
|
|
858
|
+
}
|
|
859
|
+
const freshResponseFields = buildResponseOutput(debateId, newRound, result.text, responseCap, options.output);
|
|
860
|
+
const output = {
|
|
861
|
+
debateId,
|
|
862
|
+
round: newRound,
|
|
863
|
+
...freshResponseFields,
|
|
864
|
+
sessionId: result.sessionId,
|
|
865
|
+
resumed,
|
|
866
|
+
cached: false,
|
|
867
|
+
stance: verdict.stance,
|
|
868
|
+
usage: result.usage,
|
|
869
|
+
durationMs: result.durationMs
|
|
870
|
+
};
|
|
871
|
+
if (!quiet) {
|
|
872
|
+
const stanceColor = verdict.stance === "SUPPORT" ? chalk4.green : verdict.stance === "OPPOSE" ? chalk4.red : chalk4.yellow;
|
|
873
|
+
console.error(stanceColor(`
|
|
874
|
+
Round ${newRound} \u2014 Stance: ${verdict.stance}`));
|
|
875
|
+
const previewLines = result.text.split("\n").filter((l) => l.trim().length > 10).slice(0, 3);
|
|
876
|
+
for (const line of previewLines) {
|
|
877
|
+
console.error(chalk4.dim(` ${line.trim().slice(0, 120)}`));
|
|
878
|
+
}
|
|
879
|
+
console.error(chalk4.dim(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s | Output: ${result.usage?.outputTokens ?? "?"} tokens | Input: ${result.usage?.inputTokens ?? "?"} (includes sandbox) | Resumed: ${resumed}`));
|
|
880
|
+
}
|
|
881
|
+
console.log(JSON.stringify(output, null, 2));
|
|
882
|
+
} catch (error) {
|
|
883
|
+
msgStore.markFailed(msgId, error instanceof Error ? error.message : String(error));
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
db.close();
|
|
887
|
+
} catch (error) {
|
|
888
|
+
db?.close();
|
|
889
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function debateNextCommand(debateId, options) {
|
|
894
|
+
const prompt = "Continue your analysis. Respond to the previous round's arguments and refine your position.";
|
|
895
|
+
return debateTurnCommand(debateId, prompt, options);
|
|
896
|
+
}
|
|
897
|
+
async function debateStatusCommand(debateId) {
|
|
898
|
+
let db;
|
|
899
|
+
try {
|
|
900
|
+
const dbPath = getDbPath();
|
|
901
|
+
db = openDatabase3(dbPath);
|
|
902
|
+
const store = new DebateStore2(db);
|
|
903
|
+
const turns = store.getByDebateId(debateId);
|
|
904
|
+
if (turns.length === 0) {
|
|
905
|
+
db.close();
|
|
906
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
const state = store.loadState(debateId, "proposer");
|
|
910
|
+
const msgStore = new MessageStore(db);
|
|
911
|
+
const msgHistory = msgStore.getHistory(debateId);
|
|
912
|
+
const tokenStatus = getTokenBudgetStatus(msgHistory, 4e5);
|
|
913
|
+
const output = {
|
|
914
|
+
debateId,
|
|
915
|
+
topic: state?.question ?? "unknown",
|
|
916
|
+
status: turns.some((t) => t.status === "active") ? "active" : turns[0].status,
|
|
917
|
+
round: Math.max(...turns.map((t) => t.round)),
|
|
918
|
+
participants: turns.map((t) => ({
|
|
919
|
+
role: t.role,
|
|
920
|
+
codexSessionId: t.codexSessionId,
|
|
921
|
+
round: t.round,
|
|
922
|
+
status: t.status,
|
|
923
|
+
resumeFailCount: t.resumeFailCount,
|
|
924
|
+
lastActivity: new Date(t.lastActivityAt).toISOString()
|
|
925
|
+
})),
|
|
926
|
+
resumeStats: state?.resumeStats ?? null,
|
|
927
|
+
tokenBudget: {
|
|
928
|
+
used: tokenStatus.totalTokensUsed,
|
|
929
|
+
max: tokenStatus.maxContextTokens,
|
|
930
|
+
utilization: `${Math.round(tokenStatus.utilizationRatio * 100)}%`,
|
|
931
|
+
messages: msgHistory.length
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
console.log(JSON.stringify(output, null, 2));
|
|
935
|
+
db.close();
|
|
936
|
+
} catch (error) {
|
|
937
|
+
db?.close();
|
|
938
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async function debateListCommand(options) {
|
|
943
|
+
let db;
|
|
944
|
+
try {
|
|
945
|
+
const dbPath = getDbPath();
|
|
946
|
+
db = openDatabase3(dbPath);
|
|
947
|
+
const store = new DebateStore2(db);
|
|
948
|
+
const desiredLimit = options.limit ?? 20;
|
|
949
|
+
const rows = store.list({
|
|
950
|
+
status: options.status,
|
|
951
|
+
limit: desiredLimit * 3
|
|
952
|
+
});
|
|
953
|
+
const debates = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const row of rows) {
|
|
955
|
+
const existing = debates.get(row.debateId) ?? [];
|
|
956
|
+
existing.push(row);
|
|
957
|
+
debates.set(row.debateId, existing);
|
|
958
|
+
}
|
|
959
|
+
const output = Array.from(debates.entries()).slice(0, desiredLimit).map(([id, turns]) => {
|
|
960
|
+
const state = store.loadState(id, "proposer");
|
|
961
|
+
return {
|
|
962
|
+
debateId: id,
|
|
963
|
+
topic: state?.question ?? "unknown",
|
|
964
|
+
status: turns.some((t) => t.status === "active") ? "active" : turns[0].status,
|
|
965
|
+
round: Math.max(...turns.map((t) => t.round)),
|
|
966
|
+
lastActivity: new Date(Math.max(...turns.map((t) => t.lastActivityAt))).toISOString()
|
|
967
|
+
};
|
|
968
|
+
});
|
|
969
|
+
console.log(JSON.stringify(output, null, 2));
|
|
970
|
+
db.close();
|
|
971
|
+
} catch (error) {
|
|
972
|
+
db?.close();
|
|
973
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function debateHistoryCommand(debateId, options = {}) {
|
|
978
|
+
let db;
|
|
979
|
+
try {
|
|
980
|
+
const dbPath = getDbPath();
|
|
981
|
+
db = openDatabase3(dbPath);
|
|
982
|
+
const msgStore = new MessageStore(db);
|
|
983
|
+
const history = msgStore.getHistory(debateId);
|
|
984
|
+
if (history.length === 0) {
|
|
985
|
+
const debateStore = new DebateStore2(db);
|
|
986
|
+
const turns = debateStore.getByDebateId(debateId);
|
|
987
|
+
if (turns.length > 0) {
|
|
988
|
+
console.error(chalk4.yellow(`No messages stored for debate ${debateId} \u2014 this debate predates message persistence (schema v4). Only metadata is available via "debate status".`));
|
|
989
|
+
} else {
|
|
990
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
991
|
+
}
|
|
992
|
+
db.close();
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
const tokenStatus = getTokenBudgetStatus(history, 4e5);
|
|
996
|
+
const output = {
|
|
997
|
+
debateId,
|
|
998
|
+
messageCount: history.length,
|
|
999
|
+
tokenBudget: {
|
|
1000
|
+
used: tokenStatus.totalTokensUsed,
|
|
1001
|
+
max: tokenStatus.maxContextTokens,
|
|
1002
|
+
utilization: `${Math.round(tokenStatus.utilizationRatio * 100)}%`
|
|
1003
|
+
},
|
|
1004
|
+
messages: history.map((m) => ({
|
|
1005
|
+
round: m.round,
|
|
1006
|
+
role: m.role,
|
|
1007
|
+
bridge: m.bridge,
|
|
1008
|
+
model: m.model,
|
|
1009
|
+
status: m.status,
|
|
1010
|
+
stance: m.stance,
|
|
1011
|
+
confidence: m.confidence,
|
|
1012
|
+
durationMs: m.durationMs,
|
|
1013
|
+
sessionId: m.sessionId,
|
|
1014
|
+
promptPreview: m.promptText.slice(0, 200),
|
|
1015
|
+
responsePreview: m.responseText?.slice(0, 200) ?? null,
|
|
1016
|
+
error: m.error,
|
|
1017
|
+
createdAt: new Date(m.createdAt).toISOString(),
|
|
1018
|
+
completedAt: m.completedAt ? new Date(m.completedAt).toISOString() : null
|
|
1019
|
+
}))
|
|
1020
|
+
};
|
|
1021
|
+
if (options.output) {
|
|
1022
|
+
const fullOutput = {
|
|
1023
|
+
...output,
|
|
1024
|
+
messages: history.map((m) => ({
|
|
1025
|
+
round: m.round,
|
|
1026
|
+
role: m.role,
|
|
1027
|
+
bridge: m.bridge,
|
|
1028
|
+
model: m.model,
|
|
1029
|
+
status: m.status,
|
|
1030
|
+
stance: m.stance,
|
|
1031
|
+
confidence: m.confidence,
|
|
1032
|
+
durationMs: m.durationMs,
|
|
1033
|
+
sessionId: m.sessionId,
|
|
1034
|
+
promptText: m.promptText,
|
|
1035
|
+
responseText: m.responseText ?? null,
|
|
1036
|
+
error: m.error,
|
|
1037
|
+
createdAt: new Date(m.createdAt).toISOString(),
|
|
1038
|
+
completedAt: m.completedAt ? new Date(m.completedAt).toISOString() : null
|
|
1039
|
+
}))
|
|
1040
|
+
};
|
|
1041
|
+
writeFileSync(options.output, JSON.stringify(fullOutput, null, 2), "utf-8");
|
|
1042
|
+
console.error(chalk4.dim(`Full history written to ${options.output}`));
|
|
1043
|
+
}
|
|
1044
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1045
|
+
db.close();
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
db?.close();
|
|
1048
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async function debateCompleteCommand(debateId) {
|
|
1053
|
+
let db;
|
|
1054
|
+
try {
|
|
1055
|
+
const dbPath = getDbPath();
|
|
1056
|
+
db = openDatabase3(dbPath);
|
|
1057
|
+
const store = new DebateStore2(db);
|
|
1058
|
+
const turns = store.getByDebateId(debateId);
|
|
1059
|
+
if (turns.length === 0) {
|
|
1060
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
1061
|
+
db.close();
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
const completeTransaction = db.transaction(() => {
|
|
1065
|
+
store.updateStatus(debateId, "proposer", "completed");
|
|
1066
|
+
store.updateStatus(debateId, "critic", "completed");
|
|
1067
|
+
});
|
|
1068
|
+
completeTransaction();
|
|
1069
|
+
console.log(JSON.stringify({ debateId, status: "completed" }));
|
|
1070
|
+
db.close();
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
db?.close();
|
|
1073
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/commands/cleanup.ts
|
|
1079
|
+
import {
|
|
1080
|
+
BuildStore as BuildStore2,
|
|
1081
|
+
JobStore,
|
|
1082
|
+
ModelRegistry as ModelRegistry2,
|
|
1083
|
+
SessionManager as SessionManager3,
|
|
1084
|
+
buildHandoffEnvelope as buildHandoffEnvelope2,
|
|
1085
|
+
computeThreeWayStats,
|
|
1086
|
+
createIgnoreFilter,
|
|
1087
|
+
generateId as generateId3,
|
|
1088
|
+
hostFindingsSchema,
|
|
1089
|
+
loadConfig as loadConfig3,
|
|
1090
|
+
mergeThreeWay,
|
|
1091
|
+
openDatabase as openDatabase4,
|
|
1092
|
+
recalculateConfidenceStats,
|
|
1093
|
+
runAllScanners
|
|
1094
|
+
} from "@mulep/core";
|
|
1095
|
+
import chalk5 from "chalk";
|
|
1096
|
+
import { readFileSync } from "fs";
|
|
1097
|
+
async function cleanupCommand(path, options) {
|
|
1098
|
+
let db;
|
|
1099
|
+
try {
|
|
1100
|
+
const { resolve: resolve6 } = await import("path");
|
|
1101
|
+
const projectDir = resolve6(path);
|
|
1102
|
+
if (options.background) {
|
|
1103
|
+
const bgDb = openDatabase4(getDbPath());
|
|
1104
|
+
const jobStore = new JobStore(bgDb);
|
|
1105
|
+
const jobId = jobStore.enqueue({
|
|
1106
|
+
type: "cleanup",
|
|
1107
|
+
payload: { path: projectDir, scope: options.scope, timeout: options.timeout, maxDisputes: options.maxDisputes, hostFindings: options.hostFindings, output: options.output }
|
|
1108
|
+
});
|
|
1109
|
+
console.log(JSON.stringify({ jobId, status: "queued", message: "Cleanup enqueued. Check with: mulep jobs status " + jobId }));
|
|
1110
|
+
bgDb.close();
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const scopes = options.scope === "all" ? ["deps", "unused-exports", "hardcoded", "duplicates", "deadcode", "security", "near-duplicates", "anti-patterns"] : [options.scope];
|
|
1114
|
+
const dbPath = getDbPath();
|
|
1115
|
+
db = openDatabase4(dbPath);
|
|
1116
|
+
const buildStore = new BuildStore2(db);
|
|
1117
|
+
const buildId = generateId3();
|
|
1118
|
+
const startTime = Date.now();
|
|
1119
|
+
buildStore.create({ buildId, task: `cleanup:${options.scope}` });
|
|
1120
|
+
console.error(chalk5.cyan(`Cleanup scan started (ID: ${buildId})`));
|
|
1121
|
+
console.error(chalk5.cyan(`Scopes: ${scopes.join(", ")}`));
|
|
1122
|
+
console.error(chalk5.cyan(`Project: ${projectDir}`));
|
|
1123
|
+
let hostFindings = [];
|
|
1124
|
+
if (options.hostFindings) {
|
|
1125
|
+
console.error(chalk5.cyan(`Host findings: ${options.hostFindings}`));
|
|
1126
|
+
try {
|
|
1127
|
+
const raw = readFileSync(options.hostFindings, "utf-8");
|
|
1128
|
+
const parsed = JSON.parse(raw);
|
|
1129
|
+
const validated = hostFindingsSchema.parse(parsed);
|
|
1130
|
+
hostFindings = validated.map((f) => ({
|
|
1131
|
+
key: `${f.scope}:${f.file}:${f.symbol}`,
|
|
1132
|
+
scope: f.scope,
|
|
1133
|
+
confidence: f.confidence,
|
|
1134
|
+
file: f.file,
|
|
1135
|
+
line: f.line,
|
|
1136
|
+
description: f.description,
|
|
1137
|
+
recommendation: f.recommendation,
|
|
1138
|
+
deterministicEvidence: [],
|
|
1139
|
+
semanticEvidence: [],
|
|
1140
|
+
hostEvidence: [`Host: ${f.description}`],
|
|
1141
|
+
sources: ["host"],
|
|
1142
|
+
disputed: false
|
|
1143
|
+
}));
|
|
1144
|
+
console.error(chalk5.dim(` [host] Loaded ${hostFindings.length} findings`));
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
console.error(chalk5.red(`Failed to load host findings: ${err instanceof Error ? err.message : String(err)}`));
|
|
1147
|
+
db.close();
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const ig = createIgnoreFilter(projectDir, { skipGitignore: options.noGitignore });
|
|
1152
|
+
console.error(chalk5.yellow("\nPhase 1: Scanning (parallel)..."));
|
|
1153
|
+
let codexAdapter = null;
|
|
1154
|
+
try {
|
|
1155
|
+
const config = loadConfig3();
|
|
1156
|
+
const registry = ModelRegistry2.fromConfig(config, projectDir);
|
|
1157
|
+
try {
|
|
1158
|
+
codexAdapter = registry.getAdapter("codex-reviewer");
|
|
1159
|
+
} catch {
|
|
1160
|
+
try {
|
|
1161
|
+
codexAdapter = registry.getAdapter("codex-architect");
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
const sessionMgr = new SessionManager3(db);
|
|
1168
|
+
const session2 = sessionMgr.resolveActive("cleanup");
|
|
1169
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
1170
|
+
const sessionThreadId = currentSession?.codexThreadId ?? void 0;
|
|
1171
|
+
const [deterministicFindings, semanticFindings] = await Promise.all([
|
|
1172
|
+
Promise.resolve().then(() => {
|
|
1173
|
+
console.error(chalk5.dim(" [deterministic] Starting..."));
|
|
1174
|
+
const findings = runAllScanners(projectDir, scopes, ig);
|
|
1175
|
+
console.error(chalk5.dim(` [deterministic] Done: ${findings.length} findings`));
|
|
1176
|
+
return findings;
|
|
1177
|
+
}),
|
|
1178
|
+
runCodexScan(codexAdapter, projectDir, scopes, options.timeout, sessionMgr, session2.id, sessionThreadId)
|
|
1179
|
+
]);
|
|
1180
|
+
buildStore.updateWithEvent(
|
|
1181
|
+
buildId,
|
|
1182
|
+
{ status: "reviewing", currentPhase: "review", metadata: { cleanupPhase: "scan" } },
|
|
1183
|
+
{
|
|
1184
|
+
eventType: "scan_completed",
|
|
1185
|
+
actor: "system",
|
|
1186
|
+
phase: "review",
|
|
1187
|
+
payload: {
|
|
1188
|
+
deterministicCount: deterministicFindings.length,
|
|
1189
|
+
semanticCount: semanticFindings.length,
|
|
1190
|
+
hostCount: hostFindings.length
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
);
|
|
1194
|
+
console.error(chalk5.yellow("\nPhase 2: Merging findings (3-way)..."));
|
|
1195
|
+
const mergedFindings = mergeThreeWay(deterministicFindings, semanticFindings, hostFindings);
|
|
1196
|
+
const stats = computeThreeWayStats(deterministicFindings, semanticFindings, hostFindings, mergedFindings);
|
|
1197
|
+
console.error(chalk5.dim(` Merged: ${mergedFindings.length} total, ${stats.agreed} agreed, ${stats.disputed} disputed`));
|
|
1198
|
+
if (hostFindings.length > 0) {
|
|
1199
|
+
console.error(chalk5.dim(` Sources: deterministic=${stats.deterministic}, codex=${stats.semantic}, host=${stats.host}`));
|
|
1200
|
+
}
|
|
1201
|
+
buildStore.updateWithEvent(
|
|
1202
|
+
buildId,
|
|
1203
|
+
{ metadata: { cleanupPhase: "merge" } },
|
|
1204
|
+
{
|
|
1205
|
+
eventType: "merge_completed",
|
|
1206
|
+
actor: "system",
|
|
1207
|
+
phase: "review",
|
|
1208
|
+
payload: { totalFindings: mergedFindings.length, ...stats }
|
|
1209
|
+
}
|
|
1210
|
+
);
|
|
1211
|
+
const hasAdjudicatable = stats.disputed > 0 || mergedFindings.some((f) => f.confidence === "medium");
|
|
1212
|
+
if (codexAdapter && hasAdjudicatable && options.maxDisputes > 0) {
|
|
1213
|
+
console.error(chalk5.yellow(`
|
|
1214
|
+
Phase 2.5: Adjudicating up to ${options.maxDisputes} disputed findings...`));
|
|
1215
|
+
await adjudicateFindings(codexAdapter, mergedFindings, options.maxDisputes, stats);
|
|
1216
|
+
buildStore.updateWithEvent(
|
|
1217
|
+
buildId,
|
|
1218
|
+
{ metadata: { cleanupPhase: "adjudicate" } },
|
|
1219
|
+
{
|
|
1220
|
+
eventType: "adjudicated",
|
|
1221
|
+
actor: "codex",
|
|
1222
|
+
phase: "review",
|
|
1223
|
+
payload: { adjudicated: stats.adjudicated }
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const durationMs = Date.now() - startTime;
|
|
1228
|
+
const actionableScopes = /* @__PURE__ */ new Set(["deps", "unused-exports", "hardcoded"]);
|
|
1229
|
+
const actionableCount = mergedFindings.filter(
|
|
1230
|
+
(f) => actionableScopes.has(f.scope) && (f.confidence === "high" || f.confidence === "medium")
|
|
1231
|
+
).length;
|
|
1232
|
+
const report = {
|
|
1233
|
+
scopes,
|
|
1234
|
+
findings: mergedFindings.sort((a, b) => {
|
|
1235
|
+
const confOrder = { high: 0, medium: 1, low: 2 };
|
|
1236
|
+
const confDiff = confOrder[a.confidence] - confOrder[b.confidence];
|
|
1237
|
+
if (confDiff !== 0) return confDiff;
|
|
1238
|
+
return a.key.localeCompare(b.key);
|
|
1239
|
+
}),
|
|
1240
|
+
stats,
|
|
1241
|
+
durationMs
|
|
1242
|
+
};
|
|
1243
|
+
buildStore.updateWithEvent(
|
|
1244
|
+
buildId,
|
|
1245
|
+
{ status: "completed", currentPhase: "done", completedAt: Date.now() },
|
|
1246
|
+
{
|
|
1247
|
+
eventType: "phase_transition",
|
|
1248
|
+
actor: "system",
|
|
1249
|
+
phase: "done",
|
|
1250
|
+
payload: {
|
|
1251
|
+
totalFindings: mergedFindings.length,
|
|
1252
|
+
actionable: actionableCount,
|
|
1253
|
+
reportOnly: mergedFindings.length - actionableCount
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
);
|
|
1257
|
+
console.error(chalk5.green(`
|
|
1258
|
+
Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
|
|
1259
|
+
console.error(chalk5.green(`Build ID: ${buildId}`));
|
|
1260
|
+
console.error(` Actionable: ${chalk5.red(String(actionableCount))}`);
|
|
1261
|
+
console.error(` Report-only: ${chalk5.dim(String(mergedFindings.length - actionableCount))}`);
|
|
1262
|
+
console.error(` High: ${stats.highConfidence} | Medium: ${stats.mediumConfidence} | Low: ${stats.lowConfidence}`);
|
|
1263
|
+
if (stats.adjudicated > 0) console.error(` Adjudicated: ${stats.adjudicated}`);
|
|
1264
|
+
if (!options.quiet && mergedFindings.length > 0) {
|
|
1265
|
+
console.error(chalk5.yellow("\n\u2500\u2500 Findings Summary \u2500\u2500"));
|
|
1266
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1267
|
+
for (const f of report.findings) {
|
|
1268
|
+
const arr = byScope.get(f.scope) ?? [];
|
|
1269
|
+
arr.push(f);
|
|
1270
|
+
byScope.set(f.scope, arr);
|
|
1271
|
+
}
|
|
1272
|
+
for (const [scope, items] of byScope) {
|
|
1273
|
+
const high = items.filter((f) => f.confidence === "high").length;
|
|
1274
|
+
const med = items.filter((f) => f.confidence === "medium").length;
|
|
1275
|
+
const low = items.filter((f) => f.confidence === "low").length;
|
|
1276
|
+
console.error(chalk5.cyan(`
|
|
1277
|
+
${scope} (${items.length})`));
|
|
1278
|
+
for (const f of items.filter((i) => i.confidence !== "low").slice(0, 5)) {
|
|
1279
|
+
const conf = f.confidence === "high" ? chalk5.red("HIGH") : chalk5.yellow("MED");
|
|
1280
|
+
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
1281
|
+
console.error(` ${conf} ${loc} \u2014 ${f.description}`);
|
|
1282
|
+
}
|
|
1283
|
+
if (high + med > 5) {
|
|
1284
|
+
console.error(chalk5.dim(` ... and ${high + med - 5} more`));
|
|
1285
|
+
}
|
|
1286
|
+
if (low > 0) {
|
|
1287
|
+
console.error(chalk5.dim(` + ${low} low-confidence (report-only)`));
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
console.error("");
|
|
1291
|
+
}
|
|
1292
|
+
sessionMgr.recordEvent({
|
|
1293
|
+
sessionId: session2.id,
|
|
1294
|
+
command: "cleanup",
|
|
1295
|
+
subcommand: "report",
|
|
1296
|
+
promptPreview: `Cleanup: ${scopes.join(", ")} on ${projectDir}`,
|
|
1297
|
+
responsePreview: `${mergedFindings.length} findings (${stats.highConfidence} high, ${stats.mediumConfidence} med, ${stats.lowConfidence} low)`,
|
|
1298
|
+
responseFull: JSON.stringify(report),
|
|
1299
|
+
durationMs
|
|
1300
|
+
});
|
|
1301
|
+
if (options.output) {
|
|
1302
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
1303
|
+
writeFileSync6(options.output, JSON.stringify(report, null, 2), "utf-8");
|
|
1304
|
+
console.error(chalk5.green(` Findings written to ${options.output}`));
|
|
1305
|
+
}
|
|
1306
|
+
const reportJson = JSON.stringify(report, null, 2);
|
|
1307
|
+
console.log(reportJson.length > 2e3 ? `${reportJson.slice(0, 2e3)}
|
|
1308
|
+
... (truncated, ${reportJson.length} chars total \u2014 use --output to save full report)` : reportJson);
|
|
1309
|
+
db.close();
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
db?.close();
|
|
1312
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1313
|
+
process.exit(1);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async function runCodexScan(adapter, _projectDir, scopes, timeoutSec, sessionMgr, sessionId, sessionThreadId) {
|
|
1317
|
+
if (!adapter) {
|
|
1318
|
+
console.error(chalk5.yellow(" [codex] No adapter available \u2014 skipping semantic scan"));
|
|
1319
|
+
return [];
|
|
1320
|
+
}
|
|
1321
|
+
console.error(chalk5.dim(" [codex] Starting semantic scan..."));
|
|
1322
|
+
const scopeDescriptions = scopes.map((s) => {
|
|
1323
|
+
if (s === "deps") return "unused dependencies (check each package.json dep, including dynamic imports)";
|
|
1324
|
+
if (s === "unused-exports") return "unused exports (exported but never imported anywhere)";
|
|
1325
|
+
if (s === "hardcoded") return "hardcoded values (magic numbers, URLs, credentials)";
|
|
1326
|
+
if (s === "duplicates") return "duplicate logic (similar function bodies across files)";
|
|
1327
|
+
if (s === "deadcode") return "dead code (unreachable or unused internal code)";
|
|
1328
|
+
return s;
|
|
1329
|
+
}).join(", ");
|
|
1330
|
+
const prompt = buildHandoffEnvelope2({
|
|
1331
|
+
command: "cleanup",
|
|
1332
|
+
task: `Scan this codebase for AI slop and code quality issues.
|
|
1333
|
+
|
|
1334
|
+
SCAN FOR: ${scopeDescriptions}
|
|
1335
|
+
|
|
1336
|
+
Where scope/confidence/file/line/symbol fields are:
|
|
1337
|
+
- scope: deps, unused-exports, hardcoded, duplicates, or deadcode
|
|
1338
|
+
- confidence: high, medium, or low
|
|
1339
|
+
- file: relative path from project root (forward slashes)
|
|
1340
|
+
- line: line number (or 0 if N/A)
|
|
1341
|
+
- symbol: the specific identifier (dep name, export name, variable name, or content hash)
|
|
1342
|
+
|
|
1343
|
+
IMPORTANT KEY FORMAT: The key will be built as scope:file:symbol \u2014 use the SAME symbol that a static scanner would use:
|
|
1344
|
+
- deps: the package name (e.g. "lodash")
|
|
1345
|
+
- unused-exports: the export name (e.g. "myFunction")
|
|
1346
|
+
- hardcoded: for numbers use "num:VALUE:LLINE" (e.g. "num:42:L15"), for URLs use "url:HOSTNAME:LLINE" (e.g. "url:api.example.com:L20"), for credentials use "cred:LLINE" (e.g. "cred:L15")
|
|
1347
|
+
- duplicates: "HASH:FUNCNAME" where HASH is first 8 chars of md5 of normalized body (e.g. "a1b2c3d4:myFunction")
|
|
1348
|
+
- deadcode: the function/variable name`,
|
|
1349
|
+
constraints: [
|
|
1350
|
+
"Be thorough but precise. Only report real issues you can verify.",
|
|
1351
|
+
"Check for dynamic imports (import()) before flagging unused deps",
|
|
1352
|
+
"Check barrel re-exports and index files before flagging unused exports",
|
|
1353
|
+
"Check type-only imports (import type)",
|
|
1354
|
+
"Check framework conventions and cross-package monorepo dependencies"
|
|
1355
|
+
],
|
|
1356
|
+
resumed: Boolean(sessionThreadId)
|
|
1357
|
+
});
|
|
1358
|
+
try {
|
|
1359
|
+
const progress = createProgressCallbacks("cleanup-scan");
|
|
1360
|
+
let result;
|
|
1361
|
+
try {
|
|
1362
|
+
result = await adapter.callWithResume(prompt, { sessionId: sessionThreadId, timeout: timeoutSec * 1e3, ...progress });
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
if (sessionThreadId && sessionMgr && sessionId) {
|
|
1365
|
+
sessionMgr.updateThreadId(sessionId, null);
|
|
1366
|
+
}
|
|
1367
|
+
throw err;
|
|
1368
|
+
}
|
|
1369
|
+
if (sessionThreadId && result.sessionId !== sessionThreadId && sessionMgr && sessionId) {
|
|
1370
|
+
sessionMgr.updateThreadId(sessionId, null);
|
|
1371
|
+
}
|
|
1372
|
+
if (sessionMgr && sessionId) {
|
|
1373
|
+
if (result.sessionId) {
|
|
1374
|
+
sessionMgr.updateThreadId(sessionId, result.sessionId);
|
|
1375
|
+
}
|
|
1376
|
+
sessionMgr.addUsageFromResult(sessionId, result.usage, prompt, result.text);
|
|
1377
|
+
sessionMgr.recordEvent({
|
|
1378
|
+
sessionId,
|
|
1379
|
+
command: "cleanup",
|
|
1380
|
+
subcommand: "scan",
|
|
1381
|
+
promptPreview: `Cleanup scan: ${scopes.join(", ")}`,
|
|
1382
|
+
responsePreview: result.text.slice(0, 500),
|
|
1383
|
+
usageJson: JSON.stringify(result.usage),
|
|
1384
|
+
durationMs: result.durationMs,
|
|
1385
|
+
codexThreadId: result.sessionId
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
const findings = [];
|
|
1389
|
+
for (const line of result.text.split("\n")) {
|
|
1390
|
+
const match = line.match(/^FINDING:\s*([^|]+)\|([^|]+)\|([^|]+)\|(\d+)\|([^|]+)\|([^|]+)\|(.+)/);
|
|
1391
|
+
if (match) {
|
|
1392
|
+
const scope = match[1].trim();
|
|
1393
|
+
if (!scopes.includes(scope)) continue;
|
|
1394
|
+
const file = match[3].trim();
|
|
1395
|
+
const symbol = match[5].trim();
|
|
1396
|
+
findings.push({
|
|
1397
|
+
key: `${scope}:${file}:${symbol}`,
|
|
1398
|
+
scope,
|
|
1399
|
+
confidence: match[2].trim(),
|
|
1400
|
+
file,
|
|
1401
|
+
line: Number.parseInt(match[4], 10) || void 0,
|
|
1402
|
+
description: match[6].trim(),
|
|
1403
|
+
recommendation: match[7].trim(),
|
|
1404
|
+
deterministicEvidence: [],
|
|
1405
|
+
semanticEvidence: [`Codex: ${match[6].trim()}`],
|
|
1406
|
+
hostEvidence: [],
|
|
1407
|
+
sources: ["semantic"],
|
|
1408
|
+
disputed: false
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
console.error(chalk5.dim(` [codex] Done: ${findings.length} findings`));
|
|
1413
|
+
return findings;
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
console.error(chalk5.yellow(` [codex] Scan failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1416
|
+
return [];
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function adjudicateFindings(adapter, findings, maxDisputes, stats) {
|
|
1420
|
+
const toAdjudicate = findings.filter((f) => f.disputed || f.confidence === "medium").slice(0, maxDisputes);
|
|
1421
|
+
for (const finding of toAdjudicate) {
|
|
1422
|
+
try {
|
|
1423
|
+
const allEvidence = [...finding.deterministicEvidence, ...finding.semanticEvidence, ...finding.hostEvidence];
|
|
1424
|
+
const prompt = buildHandoffEnvelope2({
|
|
1425
|
+
command: "adjudicate",
|
|
1426
|
+
task: `Verify this finding.
|
|
1427
|
+
|
|
1428
|
+
FINDING: ${finding.description}
|
|
1429
|
+
FILE: ${finding.file}${finding.line ? `:${finding.line}` : ""}
|
|
1430
|
+
SCOPE: ${finding.scope}
|
|
1431
|
+
SOURCES: ${finding.sources.join(", ")}
|
|
1432
|
+
EVIDENCE: ${allEvidence.join("; ")}`,
|
|
1433
|
+
constraints: ["Check for dynamic imports, barrel re-exports, type-only usage, runtime/indirect usage"],
|
|
1434
|
+
resumed: false
|
|
1435
|
+
});
|
|
1436
|
+
const adjProgress = createProgressCallbacks("adjudicate");
|
|
1437
|
+
const result = await adapter.callWithResume(prompt, { timeout: 6e4, ...adjProgress });
|
|
1438
|
+
const match = result.text.match(/ADJUDICATE:\s*(CONFIRMED|DISMISSED|UNCERTAIN)\s+(.*)/);
|
|
1439
|
+
if (match) {
|
|
1440
|
+
const verdict = match[1];
|
|
1441
|
+
if (verdict === "CONFIRMED") {
|
|
1442
|
+
finding.confidence = "high";
|
|
1443
|
+
finding.semanticEvidence.push(`Adjudicated: CONFIRMED \u2014 ${match[2]}`);
|
|
1444
|
+
} else if (verdict === "DISMISSED") {
|
|
1445
|
+
finding.confidence = "low";
|
|
1446
|
+
finding.semanticEvidence.push(`Adjudicated: DISMISSED \u2014 ${match[2]}`);
|
|
1447
|
+
} else {
|
|
1448
|
+
finding.semanticEvidence.push(`Adjudicated: UNCERTAIN \u2014 ${match[2]}`);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
finding.disputed = false;
|
|
1452
|
+
stats.adjudicated++;
|
|
1453
|
+
}
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
recalculateConfidenceStats(findings, stats);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/commands/cost.ts
|
|
1461
|
+
import { openDatabase as openDatabase5, SessionManager as SessionManager4 } from "@mulep/core";
|
|
1462
|
+
import chalk6 from "chalk";
|
|
1463
|
+
function parseUsage(usageJson) {
|
|
1464
|
+
try {
|
|
1465
|
+
const u = JSON.parse(usageJson);
|
|
1466
|
+
const input = u.inputTokens ?? u.input_tokens ?? 0;
|
|
1467
|
+
const output = u.outputTokens ?? u.output_tokens ?? 0;
|
|
1468
|
+
const total = u.totalTokens ?? u.total_tokens ?? input + output;
|
|
1469
|
+
return { input, output, total };
|
|
1470
|
+
} catch {
|
|
1471
|
+
return { input: 0, output: 0, total: 0 };
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
async function costCommand(options) {
|
|
1475
|
+
const db = openDatabase5(getDbPath());
|
|
1476
|
+
try {
|
|
1477
|
+
let rows;
|
|
1478
|
+
let scopeLabel;
|
|
1479
|
+
if (options.scope === "session") {
|
|
1480
|
+
const sessionMgr = new SessionManager4(db);
|
|
1481
|
+
const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("cost");
|
|
1482
|
+
if (!session2) {
|
|
1483
|
+
console.error(chalk6.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: mulep init"));
|
|
1484
|
+
db.close();
|
|
1485
|
+
process.exit(1);
|
|
1486
|
+
}
|
|
1487
|
+
rows = db.prepare(
|
|
1488
|
+
"SELECT command, subcommand, usage_json, duration_ms, created_at FROM session_events WHERE session_id = ? ORDER BY created_at ASC"
|
|
1489
|
+
).all(session2.id);
|
|
1490
|
+
scopeLabel = `session ${session2.id.slice(0, 8)}`;
|
|
1491
|
+
} else if (options.scope === "all") {
|
|
1492
|
+
rows = db.prepare(
|
|
1493
|
+
"SELECT command, subcommand, usage_json, duration_ms, created_at FROM session_events ORDER BY created_at ASC"
|
|
1494
|
+
).all();
|
|
1495
|
+
scopeLabel = "all-time";
|
|
1496
|
+
} else {
|
|
1497
|
+
const cutoff = Date.now() - options.days * 24 * 60 * 60 * 1e3;
|
|
1498
|
+
rows = db.prepare(
|
|
1499
|
+
"SELECT command, subcommand, usage_json, duration_ms, created_at FROM session_events WHERE created_at > ? ORDER BY created_at ASC"
|
|
1500
|
+
).all(cutoff);
|
|
1501
|
+
scopeLabel = `last ${options.days} days`;
|
|
1502
|
+
}
|
|
1503
|
+
let totalInput = 0;
|
|
1504
|
+
let totalOutput = 0;
|
|
1505
|
+
let totalTokens = 0;
|
|
1506
|
+
let totalDuration = 0;
|
|
1507
|
+
const byCommand = {};
|
|
1508
|
+
const byDay = {};
|
|
1509
|
+
for (const row of rows) {
|
|
1510
|
+
const usage = parseUsage(row.usage_json);
|
|
1511
|
+
totalInput += usage.input;
|
|
1512
|
+
totalOutput += usage.output;
|
|
1513
|
+
totalTokens += usage.total;
|
|
1514
|
+
totalDuration += row.duration_ms ?? 0;
|
|
1515
|
+
const cmd = row.command ?? "unknown";
|
|
1516
|
+
if (!byCommand[cmd]) byCommand[cmd] = { calls: 0, tokens: 0, durationMs: 0 };
|
|
1517
|
+
byCommand[cmd].calls++;
|
|
1518
|
+
byCommand[cmd].tokens += usage.total;
|
|
1519
|
+
byCommand[cmd].durationMs += row.duration_ms ?? 0;
|
|
1520
|
+
const day = new Date(row.created_at).toISOString().slice(0, 10);
|
|
1521
|
+
if (!byDay[day]) byDay[day] = { calls: 0, tokens: 0 };
|
|
1522
|
+
byDay[day].calls++;
|
|
1523
|
+
byDay[day].tokens += usage.total;
|
|
1524
|
+
}
|
|
1525
|
+
const output = {
|
|
1526
|
+
scope: scopeLabel,
|
|
1527
|
+
totalCalls: rows.length,
|
|
1528
|
+
totalTokens,
|
|
1529
|
+
totalInputTokens: totalInput,
|
|
1530
|
+
totalOutputTokens: totalOutput,
|
|
1531
|
+
totalDurationMs: totalDuration,
|
|
1532
|
+
avgTokensPerCall: rows.length > 0 ? Math.round(totalTokens / rows.length) : 0,
|
|
1533
|
+
avgDurationMs: rows.length > 0 ? Math.round(totalDuration / rows.length) : 0,
|
|
1534
|
+
byCommand,
|
|
1535
|
+
byDay
|
|
1536
|
+
};
|
|
1537
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1538
|
+
db.close();
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
db.close();
|
|
1541
|
+
console.error(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/commands/doctor.ts
|
|
1547
|
+
import { existsSync, accessSync, constants } from "fs";
|
|
1548
|
+
import { execSync as execSync2 } from "child_process";
|
|
1549
|
+
import { join as join4 } from "path";
|
|
1550
|
+
import chalk7 from "chalk";
|
|
1551
|
+
import { VERSION } from "@mulep/core";
|
|
1552
|
+
async function doctorCommand() {
|
|
1553
|
+
const cwd = process.cwd();
|
|
1554
|
+
const checks = [];
|
|
1555
|
+
console.error(chalk7.cyan(`
|
|
1556
|
+
MuleP Doctor v${VERSION}
|
|
1557
|
+
`));
|
|
1558
|
+
try {
|
|
1559
|
+
const version = execSync2("codex --version", { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
1560
|
+
checks.push({ name: "codex-cli", status: "pass", message: `Codex CLI ${version}` });
|
|
1561
|
+
} catch {
|
|
1562
|
+
checks.push({
|
|
1563
|
+
name: "codex-cli",
|
|
1564
|
+
status: "fail",
|
|
1565
|
+
message: "Codex CLI not found in PATH",
|
|
1566
|
+
fix: "npm install -g @openai/codex"
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
const configPath = join4(cwd, ".mulep.yml");
|
|
1570
|
+
if (existsSync(configPath)) {
|
|
1571
|
+
checks.push({ name: "config", status: "pass", message: ".mulep.yml found" });
|
|
1572
|
+
} else {
|
|
1573
|
+
checks.push({
|
|
1574
|
+
name: "config",
|
|
1575
|
+
status: "fail",
|
|
1576
|
+
message: ".mulep.yml not found",
|
|
1577
|
+
fix: "mulep init"
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
const dbDir = join4(cwd, ".mulep", "db");
|
|
1581
|
+
const dbPath = join4(dbDir, "mulep.db");
|
|
1582
|
+
if (existsSync(dbDir)) {
|
|
1583
|
+
try {
|
|
1584
|
+
accessSync(dbDir, constants.W_OK);
|
|
1585
|
+
checks.push({
|
|
1586
|
+
name: "database",
|
|
1587
|
+
status: existsSync(dbPath) ? "pass" : "warn",
|
|
1588
|
+
message: existsSync(dbPath) ? "Database exists and writable" : "Database directory exists, DB will be created on first use"
|
|
1589
|
+
});
|
|
1590
|
+
} catch {
|
|
1591
|
+
checks.push({
|
|
1592
|
+
name: "database",
|
|
1593
|
+
status: "fail",
|
|
1594
|
+
message: ".mulep/db/ is not writable",
|
|
1595
|
+
fix: "Check file permissions on .mulep/db/"
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
} else {
|
|
1599
|
+
checks.push({
|
|
1600
|
+
name: "database",
|
|
1601
|
+
status: "warn",
|
|
1602
|
+
message: ".mulep/db/ not found \u2014 will be created by mulep init",
|
|
1603
|
+
fix: "mulep init"
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
let gitFound = false;
|
|
1607
|
+
let searchDir = cwd;
|
|
1608
|
+
while (searchDir) {
|
|
1609
|
+
if (existsSync(join4(searchDir, ".git"))) {
|
|
1610
|
+
gitFound = true;
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
const parent = join4(searchDir, "..");
|
|
1614
|
+
if (parent === searchDir) break;
|
|
1615
|
+
searchDir = parent;
|
|
1616
|
+
}
|
|
1617
|
+
if (gitFound) {
|
|
1618
|
+
checks.push({ name: "git", status: "pass", message: "Git repository detected" });
|
|
1619
|
+
} else {
|
|
1620
|
+
checks.push({
|
|
1621
|
+
name: "git",
|
|
1622
|
+
status: "warn",
|
|
1623
|
+
message: "Not a git repository \u2014 diff/shipit/watch features limited"
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
const nodeVersion = process.version;
|
|
1627
|
+
const major = Number.parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
1628
|
+
if (major >= 18) {
|
|
1629
|
+
checks.push({ name: "node", status: "pass", message: `Node.js ${nodeVersion}` });
|
|
1630
|
+
} else {
|
|
1631
|
+
checks.push({
|
|
1632
|
+
name: "node",
|
|
1633
|
+
status: "fail",
|
|
1634
|
+
message: `Node.js ${nodeVersion} \u2014 requires >= 18`,
|
|
1635
|
+
fix: "Install Node.js 18+"
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
if (existsSync(dbPath)) {
|
|
1639
|
+
try {
|
|
1640
|
+
const { openDatabase: openDatabase14 } = await import("@mulep/core");
|
|
1641
|
+
const db = openDatabase14(dbPath);
|
|
1642
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
1643
|
+
const version = row?.user_version ?? 0;
|
|
1644
|
+
if (version >= 7) {
|
|
1645
|
+
checks.push({ name: "schema", status: "pass", message: `Schema version ${version}` });
|
|
1646
|
+
} else {
|
|
1647
|
+
checks.push({
|
|
1648
|
+
name: "schema",
|
|
1649
|
+
status: "warn",
|
|
1650
|
+
message: `Schema version ${version} \u2014 will auto-migrate on next command`
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
db.close();
|
|
1654
|
+
} catch {
|
|
1655
|
+
checks.push({ name: "schema", status: "warn", message: "Could not read schema version" });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
let hasFailure = false;
|
|
1659
|
+
for (const check of checks) {
|
|
1660
|
+
const icon = check.status === "pass" ? chalk7.green("PASS") : check.status === "warn" ? chalk7.yellow("WARN") : chalk7.red("FAIL");
|
|
1661
|
+
console.error(` ${icon} ${check.name}: ${check.message}`);
|
|
1662
|
+
if (check.fix) {
|
|
1663
|
+
console.error(chalk7.dim(` \u2192 ${check.fix}`));
|
|
1664
|
+
}
|
|
1665
|
+
if (check.status === "fail") hasFailure = true;
|
|
1666
|
+
}
|
|
1667
|
+
console.error("");
|
|
1668
|
+
const output = {
|
|
1669
|
+
version: VERSION,
|
|
1670
|
+
checks,
|
|
1671
|
+
healthy: !hasFailure
|
|
1672
|
+
};
|
|
1673
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1674
|
+
if (hasFailure) {
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/commands/events.ts
|
|
1680
|
+
import { openDatabase as openDatabase6 } from "@mulep/core";
|
|
1681
|
+
import chalk8 from "chalk";
|
|
1682
|
+
async function eventsCommand(options) {
|
|
1683
|
+
const db = openDatabase6(getDbPath());
|
|
1684
|
+
const query = options.type === "all" ? db.prepare(`
|
|
1685
|
+
SELECT 'session_event' as source, id, session_id, command, subcommand, prompt_preview, response_preview, usage_json, duration_ms, created_at
|
|
1686
|
+
FROM session_events
|
|
1687
|
+
WHERE id > ?
|
|
1688
|
+
ORDER BY id ASC
|
|
1689
|
+
LIMIT ?
|
|
1690
|
+
`) : options.type === "jobs" ? db.prepare(`
|
|
1691
|
+
SELECT 'job_log' as source, id, job_id, seq, level, event_type, message, payload_json, created_at
|
|
1692
|
+
FROM job_logs
|
|
1693
|
+
WHERE id > ?
|
|
1694
|
+
ORDER BY id ASC
|
|
1695
|
+
LIMIT ?
|
|
1696
|
+
`) : db.prepare(`
|
|
1697
|
+
SELECT 'session_event' as source, id, session_id, command, subcommand, prompt_preview, response_preview, usage_json, duration_ms, created_at
|
|
1698
|
+
FROM session_events
|
|
1699
|
+
WHERE id > ?
|
|
1700
|
+
ORDER BY id ASC
|
|
1701
|
+
LIMIT ?
|
|
1702
|
+
`);
|
|
1703
|
+
let cursor = options.sinceSeq;
|
|
1704
|
+
const poll = () => {
|
|
1705
|
+
const rows = query.all(cursor, options.limit);
|
|
1706
|
+
for (const row of rows) {
|
|
1707
|
+
console.log(JSON.stringify(row));
|
|
1708
|
+
cursor = row.id;
|
|
1709
|
+
}
|
|
1710
|
+
return rows.length;
|
|
1711
|
+
};
|
|
1712
|
+
poll();
|
|
1713
|
+
if (!options.follow) {
|
|
1714
|
+
db.close();
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
console.error(chalk8.dim("Following events... (Ctrl+C to stop)"));
|
|
1718
|
+
const interval = setInterval(() => {
|
|
1719
|
+
poll();
|
|
1720
|
+
}, 1e3);
|
|
1721
|
+
const shutdown = () => {
|
|
1722
|
+
clearInterval(interval);
|
|
1723
|
+
db.close();
|
|
1724
|
+
process.exit(0);
|
|
1725
|
+
};
|
|
1726
|
+
process.on("SIGINT", shutdown);
|
|
1727
|
+
process.on("SIGTERM", shutdown);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/commands/fix.ts
|
|
1731
|
+
import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
|
|
1732
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1733
|
+
import { resolve } from "path";
|
|
1734
|
+
import {
|
|
1735
|
+
DEFAULT_RULES,
|
|
1736
|
+
ModelRegistry as ModelRegistry3,
|
|
1737
|
+
SessionManager as SessionManager5,
|
|
1738
|
+
buildHandoffEnvelope as buildHandoffEnvelope3,
|
|
1739
|
+
evaluatePolicy,
|
|
1740
|
+
loadConfig as loadConfig4,
|
|
1741
|
+
openDatabase as openDatabase7,
|
|
1742
|
+
REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS2
|
|
1743
|
+
} from "@mulep/core";
|
|
1744
|
+
import chalk9 from "chalk";
|
|
1745
|
+
function parseFixes(text) {
|
|
1746
|
+
const fixes = [];
|
|
1747
|
+
const fixPattern = /FIX:\s*(\S+?):(\d+)\s+(.+?)(?:\n```old\n([\s\S]*?)\n```\s*\n```new\n([\s\S]*?)\n```|(?:\n```\n([\s\S]*?)\n```))/g;
|
|
1748
|
+
let match;
|
|
1749
|
+
match = fixPattern.exec(text);
|
|
1750
|
+
while (match !== null) {
|
|
1751
|
+
const file = match[1];
|
|
1752
|
+
const line = Number.parseInt(match[2], 10);
|
|
1753
|
+
const description = match[3].trim();
|
|
1754
|
+
if (match[4] !== void 0 && match[5] !== void 0) {
|
|
1755
|
+
fixes.push({ file, line, description, oldCode: match[4], newCode: match[5] });
|
|
1756
|
+
} else if (match[6] !== void 0) {
|
|
1757
|
+
fixes.push({ file, line, description, oldCode: "", newCode: match[6] });
|
|
1758
|
+
}
|
|
1759
|
+
match = fixPattern.exec(text);
|
|
1760
|
+
}
|
|
1761
|
+
return fixes;
|
|
1762
|
+
}
|
|
1763
|
+
function applyFix(fix, projectDir) {
|
|
1764
|
+
const filePath = resolve(projectDir, fix.file);
|
|
1765
|
+
const normalizedProject = resolve(projectDir) + (process.platform === "win32" ? "\\" : "/");
|
|
1766
|
+
if (!resolve(filePath).startsWith(normalizedProject)) {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
let content;
|
|
1770
|
+
try {
|
|
1771
|
+
content = readFileSync2(filePath, "utf-8");
|
|
1772
|
+
} catch {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
const lines = content.split("\n");
|
|
1776
|
+
if (fix.oldCode) {
|
|
1777
|
+
const trimmedOld = fix.oldCode.trim();
|
|
1778
|
+
if (content.includes(trimmedOld)) {
|
|
1779
|
+
const updated = content.replace(trimmedOld, fix.newCode.trim());
|
|
1780
|
+
writeFileSync2(filePath, updated, "utf-8");
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
const lineIdx = fix.line - 1;
|
|
1786
|
+
if (lineIdx < 0 || lineIdx >= lines.length) return false;
|
|
1787
|
+
const newLines = fix.newCode.trim().split("\n");
|
|
1788
|
+
const oldLineCount = Math.max(1, newLines.length);
|
|
1789
|
+
lines.splice(lineIdx, oldLineCount, ...newLines);
|
|
1790
|
+
writeFileSync2(filePath, lines.join("\n"), "utf-8");
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1793
|
+
async function fixCommand(fileOrGlob, options) {
|
|
1794
|
+
const projectDir = process.cwd();
|
|
1795
|
+
const db = openDatabase7(getDbPath());
|
|
1796
|
+
const config = loadConfig4();
|
|
1797
|
+
const registry = ModelRegistry3.fromConfig(config, projectDir);
|
|
1798
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
1799
|
+
if (!adapter) {
|
|
1800
|
+
try {
|
|
1801
|
+
execSync3("codex --version", { stdio: "pipe", encoding: "utf-8" });
|
|
1802
|
+
} catch {
|
|
1803
|
+
console.error(chalk9.red("Codex CLI is not installed or not in PATH."));
|
|
1804
|
+
console.error(chalk9.yellow("Install it: npm install -g @openai/codex"));
|
|
1805
|
+
db.close();
|
|
1806
|
+
process.exit(1);
|
|
1807
|
+
}
|
|
1808
|
+
console.error(chalk9.red("No codex adapter found in config. Run: mulep init"));
|
|
1809
|
+
db.close();
|
|
1810
|
+
process.exit(1);
|
|
1811
|
+
}
|
|
1812
|
+
const sessionMgr = new SessionManager5(db);
|
|
1813
|
+
const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("fix");
|
|
1814
|
+
if (!session2) {
|
|
1815
|
+
console.error(chalk9.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: mulep init"));
|
|
1816
|
+
db.close();
|
|
1817
|
+
process.exit(1);
|
|
1818
|
+
}
|
|
1819
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
1820
|
+
let threadId = currentSession?.codexThreadId ?? void 0;
|
|
1821
|
+
const rounds = [];
|
|
1822
|
+
let converged = false;
|
|
1823
|
+
const stuckFingerprints = /* @__PURE__ */ new Set();
|
|
1824
|
+
let prevFingerprints = /* @__PURE__ */ new Set();
|
|
1825
|
+
console.error(
|
|
1826
|
+
chalk9.cyan(
|
|
1827
|
+
`Autofix loop: ${fileOrGlob} (max ${options.maxRounds} rounds, focus: ${options.focus})`
|
|
1828
|
+
)
|
|
1829
|
+
);
|
|
1830
|
+
for (let round = 1; round <= options.maxRounds; round++) {
|
|
1831
|
+
const roundStart = Date.now();
|
|
1832
|
+
console.error(chalk9.dim(`
|
|
1833
|
+
\u2500\u2500 Round ${round}/${options.maxRounds} \u2500\u2500`));
|
|
1834
|
+
let diffContent = "";
|
|
1835
|
+
if (options.diff) {
|
|
1836
|
+
try {
|
|
1837
|
+
diffContent = execFileSync2("git", ["diff", "--", ...options.diff.split(/\s+/)], {
|
|
1838
|
+
cwd: projectDir,
|
|
1839
|
+
encoding: "utf-8",
|
|
1840
|
+
maxBuffer: 1024 * 1024
|
|
1841
|
+
});
|
|
1842
|
+
} catch {
|
|
1843
|
+
diffContent = "";
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const fixOutputContract = [
|
|
1847
|
+
"You are an autofix engine. For EVERY fixable issue, you MUST output this EXACT format:",
|
|
1848
|
+
"",
|
|
1849
|
+
"FIX: path/to/file.ts:42 Description of the bug",
|
|
1850
|
+
"```old",
|
|
1851
|
+
"exact old code copied from the file",
|
|
1852
|
+
"```",
|
|
1853
|
+
"```new",
|
|
1854
|
+
"exact replacement code",
|
|
1855
|
+
"```",
|
|
1856
|
+
"",
|
|
1857
|
+
"Then end with:",
|
|
1858
|
+
"VERDICT: APPROVED or VERDICT: NEEDS_REVISION",
|
|
1859
|
+
"SCORE: X/10",
|
|
1860
|
+
"",
|
|
1861
|
+
"Rules:",
|
|
1862
|
+
"- The ```old block MUST be an exact substring copy from the file (whitespace-sensitive).",
|
|
1863
|
+
"- One FIX block per issue.",
|
|
1864
|
+
"- Issues without a FIX block are IGNORED by the autofix engine.",
|
|
1865
|
+
"- If the code is clean, output VERDICT: APPROVED with no FIX blocks."
|
|
1866
|
+
].join("\n");
|
|
1867
|
+
const reviewPrompt = buildHandoffEnvelope3({
|
|
1868
|
+
command: "custom",
|
|
1869
|
+
task: options.diff ? `Review and identify fixable issues in this diff.
|
|
1870
|
+
|
|
1871
|
+
GIT DIFF (${options.diff}):
|
|
1872
|
+
${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}
|
|
1873
|
+
|
|
1874
|
+
${fixOutputContract}` : `Review ${fileOrGlob} and identify fixable issues. Read the file(s) first, then report issues with exact line numbers and exact fixes.
|
|
1875
|
+
|
|
1876
|
+
${fixOutputContract}`,
|
|
1877
|
+
constraints: [
|
|
1878
|
+
`Focus: ${options.focus}`,
|
|
1879
|
+
round > 1 ? `This is re-review round ${round}. Previous fixes were applied by the host. Only report REMAINING unfixed issues.` : ""
|
|
1880
|
+
].filter(Boolean),
|
|
1881
|
+
resumed: Boolean(threadId)
|
|
1882
|
+
});
|
|
1883
|
+
const timeoutMs = options.timeout * 1e3;
|
|
1884
|
+
const progress = createProgressCallbacks("fix-review");
|
|
1885
|
+
console.error(chalk9.dim(" GPT reviewing..."));
|
|
1886
|
+
let reviewResult;
|
|
1887
|
+
try {
|
|
1888
|
+
reviewResult = await adapter.callWithResume(reviewPrompt, {
|
|
1889
|
+
sessionId: threadId,
|
|
1890
|
+
timeout: timeoutMs,
|
|
1891
|
+
...progress
|
|
1892
|
+
});
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
if (threadId) {
|
|
1895
|
+
console.error(chalk9.yellow(" Clearing stale codex thread ID after failure."));
|
|
1896
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
1897
|
+
threadId = void 0;
|
|
1898
|
+
}
|
|
1899
|
+
throw err;
|
|
1900
|
+
}
|
|
1901
|
+
const resumed = threadId && reviewResult.sessionId === threadId;
|
|
1902
|
+
if (threadId && !resumed) {
|
|
1903
|
+
threadId = void 0;
|
|
1904
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
1905
|
+
}
|
|
1906
|
+
if (reviewResult.sessionId) {
|
|
1907
|
+
threadId = reviewResult.sessionId;
|
|
1908
|
+
sessionMgr.updateThreadId(session2.id, reviewResult.sessionId);
|
|
1909
|
+
}
|
|
1910
|
+
sessionMgr.addUsageFromResult(session2.id, reviewResult.usage, reviewPrompt, reviewResult.text);
|
|
1911
|
+
sessionMgr.recordEvent({
|
|
1912
|
+
sessionId: session2.id,
|
|
1913
|
+
command: "fix",
|
|
1914
|
+
subcommand: `review-round-${round}`,
|
|
1915
|
+
promptPreview: `Fix review round ${round}: ${fileOrGlob}`,
|
|
1916
|
+
responsePreview: reviewResult.text.slice(0, 500),
|
|
1917
|
+
promptFull: reviewPrompt,
|
|
1918
|
+
responseFull: reviewResult.text,
|
|
1919
|
+
usageJson: JSON.stringify(reviewResult.usage),
|
|
1920
|
+
durationMs: reviewResult.durationMs,
|
|
1921
|
+
codexThreadId: reviewResult.sessionId
|
|
1922
|
+
});
|
|
1923
|
+
const tail = reviewResult.text.slice(-500);
|
|
1924
|
+
const verdictMatch = tail.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
1925
|
+
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
1926
|
+
const verdict = verdictMatch ? verdictMatch[1].toLowerCase() : "unknown";
|
|
1927
|
+
const score = scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null;
|
|
1928
|
+
const criticalCount = (reviewResult.text.match(/CRITICAL/gi) ?? []).length;
|
|
1929
|
+
const warningCount = (reviewResult.text.match(/WARNING/gi) ?? []).length;
|
|
1930
|
+
console.error(
|
|
1931
|
+
` Verdict: ${verdict}, Score: ${score ?? "?"}/10, Critical: ${criticalCount}, Warning: ${warningCount}`
|
|
1932
|
+
);
|
|
1933
|
+
if (verdict === "approved" && criticalCount === 0) {
|
|
1934
|
+
rounds.push({
|
|
1935
|
+
round,
|
|
1936
|
+
reviewVerdict: verdict,
|
|
1937
|
+
reviewScore: score,
|
|
1938
|
+
criticalCount,
|
|
1939
|
+
warningCount,
|
|
1940
|
+
fixesProposed: 0,
|
|
1941
|
+
fixesApplied: 0,
|
|
1942
|
+
fixesFailed: 0,
|
|
1943
|
+
exitReason: "all_resolved",
|
|
1944
|
+
durationMs: Date.now() - roundStart
|
|
1945
|
+
});
|
|
1946
|
+
converged = true;
|
|
1947
|
+
console.error(chalk9.green(" APPROVED \u2014 all issues resolved."));
|
|
1948
|
+
break;
|
|
1949
|
+
}
|
|
1950
|
+
const fixes = parseFixes(reviewResult.text);
|
|
1951
|
+
console.error(chalk9.dim(` Found ${fixes.length} fix proposal(s)`));
|
|
1952
|
+
if (fixes.length === 0) {
|
|
1953
|
+
rounds.push({
|
|
1954
|
+
round,
|
|
1955
|
+
reviewVerdict: verdict,
|
|
1956
|
+
reviewScore: score,
|
|
1957
|
+
criticalCount,
|
|
1958
|
+
warningCount,
|
|
1959
|
+
fixesProposed: 0,
|
|
1960
|
+
fixesApplied: 0,
|
|
1961
|
+
fixesFailed: 0,
|
|
1962
|
+
exitReason: "no_fixes_proposed",
|
|
1963
|
+
durationMs: Date.now() - roundStart
|
|
1964
|
+
});
|
|
1965
|
+
console.error(chalk9.yellow(" GPT found issues but proposed no structured fixes. Stopping."));
|
|
1966
|
+
break;
|
|
1967
|
+
}
|
|
1968
|
+
if (options.dryRun) {
|
|
1969
|
+
console.error(chalk9.yellow(" Dry-run: showing proposed fixes without applying."));
|
|
1970
|
+
for (const fix of fixes) {
|
|
1971
|
+
console.error(chalk9.dim(` ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1972
|
+
}
|
|
1973
|
+
rounds.push({
|
|
1974
|
+
round,
|
|
1975
|
+
reviewVerdict: verdict,
|
|
1976
|
+
reviewScore: score,
|
|
1977
|
+
criticalCount,
|
|
1978
|
+
warningCount,
|
|
1979
|
+
fixesProposed: fixes.length,
|
|
1980
|
+
fixesApplied: 0,
|
|
1981
|
+
fixesFailed: 0,
|
|
1982
|
+
exitReason: "dry_run",
|
|
1983
|
+
durationMs: Date.now() - roundStart
|
|
1984
|
+
});
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
let applied = 0;
|
|
1988
|
+
let failed = 0;
|
|
1989
|
+
const currentFingerprints = /* @__PURE__ */ new Set();
|
|
1990
|
+
for (const fix of fixes) {
|
|
1991
|
+
const fingerprint = `${fix.file}:${fix.description}`;
|
|
1992
|
+
currentFingerprints.add(fingerprint);
|
|
1993
|
+
if (stuckFingerprints.has(fingerprint)) {
|
|
1994
|
+
console.error(chalk9.dim(` Skip (stuck): ${fix.file}:${fix.line}`));
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
if (prevFingerprints.has(fingerprint)) {
|
|
1998
|
+
stuckFingerprints.add(fingerprint);
|
|
1999
|
+
console.error(chalk9.yellow(` Stuck (recurring): ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
2000
|
+
failed++;
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
const success = applyFix(fix, projectDir);
|
|
2004
|
+
if (success) {
|
|
2005
|
+
applied++;
|
|
2006
|
+
console.error(chalk9.green(` Fixed: ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
2007
|
+
} else {
|
|
2008
|
+
failed++;
|
|
2009
|
+
console.error(chalk9.red(` Failed: ${fix.file}:${fix.line} \u2014 could not match old code`));
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
prevFingerprints = currentFingerprints;
|
|
2013
|
+
if (applied > 0 && !options.noStage) {
|
|
2014
|
+
try {
|
|
2015
|
+
execFileSync2("git", ["add", "-A"], { cwd: projectDir, stdio: "pipe" });
|
|
2016
|
+
console.error(chalk9.dim(" Changes staged."));
|
|
2017
|
+
} catch {
|
|
2018
|
+
console.error(chalk9.yellow(" Could not stage changes (not a git repo?)."));
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const exitReason2 = applied === 0 ? "no_diff" : "continue";
|
|
2022
|
+
rounds.push({
|
|
2023
|
+
round,
|
|
2024
|
+
reviewVerdict: verdict,
|
|
2025
|
+
reviewScore: score,
|
|
2026
|
+
criticalCount,
|
|
2027
|
+
warningCount,
|
|
2028
|
+
fixesProposed: fixes.length,
|
|
2029
|
+
fixesApplied: applied,
|
|
2030
|
+
fixesFailed: failed,
|
|
2031
|
+
exitReason: exitReason2,
|
|
2032
|
+
durationMs: Date.now() - roundStart
|
|
2033
|
+
});
|
|
2034
|
+
if (applied === 0) {
|
|
2035
|
+
console.error(chalk9.yellow(" No fixes applied \u2014 stopping loop."));
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
if (stuckFingerprints.size >= fixes.length) {
|
|
2039
|
+
console.error(chalk9.yellow(" All remaining issues are stuck \u2014 stopping."));
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
console.error(chalk9.dim(` Applied ${applied}, failed ${failed}. Continuing to re-review...`));
|
|
2043
|
+
}
|
|
2044
|
+
const lastRound = rounds[rounds.length - 1];
|
|
2045
|
+
const totalApplied = rounds.reduce((sum, r) => sum + r.fixesApplied, 0);
|
|
2046
|
+
const totalProposed = rounds.reduce((sum, r) => sum + r.fixesProposed, 0);
|
|
2047
|
+
const policyCtx = {
|
|
2048
|
+
criticalCount: lastRound?.criticalCount ?? 0,
|
|
2049
|
+
warningCount: lastRound?.warningCount ?? 0,
|
|
2050
|
+
verdict: lastRound?.reviewVerdict ?? "unknown",
|
|
2051
|
+
stepsCompleted: { fix: converged ? "passed" : "failed" },
|
|
2052
|
+
cleanupHighCount: 0
|
|
2053
|
+
};
|
|
2054
|
+
const policy = evaluatePolicy("review.completed", policyCtx, DEFAULT_RULES);
|
|
2055
|
+
const exitReason = converged ? "all_resolved" : stuckFingerprints.size > 0 ? "all_stuck" : lastRound?.exitReason ?? "max_iterations";
|
|
2056
|
+
const output = {
|
|
2057
|
+
target: fileOrGlob,
|
|
2058
|
+
converged,
|
|
2059
|
+
exitReason,
|
|
2060
|
+
rounds,
|
|
2061
|
+
totalRounds: rounds.length,
|
|
2062
|
+
totalFixesProposed: totalProposed,
|
|
2063
|
+
totalFixesApplied: totalApplied,
|
|
2064
|
+
stuckCount: stuckFingerprints.size,
|
|
2065
|
+
finalVerdict: lastRound?.reviewVerdict ?? "unknown",
|
|
2066
|
+
finalScore: lastRound?.reviewScore ?? null,
|
|
2067
|
+
policy,
|
|
2068
|
+
sessionId: session2.id,
|
|
2069
|
+
codexThreadId: threadId
|
|
2070
|
+
};
|
|
2071
|
+
const color = converged ? chalk9.green : chalk9.red;
|
|
2072
|
+
console.error(color(`
|
|
2073
|
+
Result: ${converged ? "CONVERGED" : "NOT CONVERGED"} (${exitReason})`));
|
|
2074
|
+
console.error(` Rounds: ${rounds.length}/${options.maxRounds}`);
|
|
2075
|
+
console.error(` Fixes: ${totalApplied} applied, ${totalProposed - totalApplied} failed/skipped`);
|
|
2076
|
+
if (stuckFingerprints.size > 0) {
|
|
2077
|
+
console.error(chalk9.yellow(` Stuck issues: ${stuckFingerprints.size}`));
|
|
2078
|
+
}
|
|
2079
|
+
console.error(` Final: ${lastRound?.reviewVerdict ?? "?"} (${lastRound?.reviewScore ?? "?"}/10)`);
|
|
2080
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2081
|
+
db.close();
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/commands/init.ts
|
|
2085
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2086
|
+
import { basename, join as join5 } from "path";
|
|
2087
|
+
import { loadConfig as loadConfig5, writeConfig } from "@mulep/core";
|
|
2088
|
+
import chalk10 from "chalk";
|
|
2089
|
+
async function initCommand(options) {
|
|
2090
|
+
const cwd = process.cwd();
|
|
2091
|
+
const configPath = join5(cwd, ".mulep.yml");
|
|
2092
|
+
if (existsSync2(configPath) && !options.force) {
|
|
2093
|
+
console.error(chalk10.red("Already initialized. Use --force to overwrite."));
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
const validPresets = ["cli-first"];
|
|
2097
|
+
let presetName = "cli-first";
|
|
2098
|
+
if (options.preset) {
|
|
2099
|
+
if (!validPresets.includes(options.preset)) {
|
|
2100
|
+
console.error(chalk10.red(`Unknown preset: ${options.preset}. Available: ${validPresets.join(", ")}`));
|
|
2101
|
+
process.exit(1);
|
|
2102
|
+
}
|
|
2103
|
+
presetName = options.preset;
|
|
2104
|
+
} else if (options.nonInteractive) {
|
|
2105
|
+
presetName = "cli-first";
|
|
2106
|
+
} else {
|
|
2107
|
+
const { selectPreset } = await import("./prompts-N6ZYOVSU.js");
|
|
2108
|
+
presetName = await selectPreset();
|
|
2109
|
+
}
|
|
2110
|
+
const config = loadConfig5({ preset: presetName, skipFile: options.force });
|
|
2111
|
+
config.project.name = basename(cwd);
|
|
2112
|
+
writeConfig(config, cwd);
|
|
2113
|
+
console.log(chalk10.green(`
|
|
2114
|
+
Initialized with '${presetName}' preset`));
|
|
2115
|
+
const modelEntries = Object.entries(config.models);
|
|
2116
|
+
for (const [alias, modelConfig] of modelEntries) {
|
|
2117
|
+
console.log(chalk10.gray(` ${alias}: ${modelConfig.model}`));
|
|
2118
|
+
}
|
|
2119
|
+
console.log(chalk10.gray('\nNext: mulep plan "describe your task"'));
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// src/commands/install-skills.ts
|
|
2123
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2124
|
+
import { dirname, join as join6 } from "path";
|
|
2125
|
+
import chalk11 from "chalk";
|
|
2126
|
+
var SKILLS = [
|
|
2127
|
+
{
|
|
2128
|
+
path: ".claude/skills/codex-review/SKILL.md",
|
|
2129
|
+
description: "/codex-review \u2014 Get GPT review via Codex CLI",
|
|
2130
|
+
content: `---
|
|
2131
|
+
name: codex-review
|
|
2132
|
+
description: Get an independent GPT review via Codex CLI. Use when you want a second opinion on code, plans, or architecture from a different AI model.
|
|
2133
|
+
user-invocable: true
|
|
2134
|
+
---
|
|
2135
|
+
|
|
2136
|
+
# /codex-review \u2014 Get GPT Review via Codex CLI
|
|
2137
|
+
|
|
2138
|
+
## Usage
|
|
2139
|
+
\`/codex-review <file, glob, or description of what to review>\`
|
|
2140
|
+
|
|
2141
|
+
## Description
|
|
2142
|
+
Sends content to GPT via \`mulep review\` for an independent review with session persistence. GPT has full codebase access and reviews are tracked in SQLite. Uses your ChatGPT subscription \u2014 zero API cost.
|
|
2143
|
+
|
|
2144
|
+
## Instructions
|
|
2145
|
+
|
|
2146
|
+
When the user invokes \`/codex-review\`, follow these steps:
|
|
2147
|
+
|
|
2148
|
+
### Step 1: Gather project context
|
|
2149
|
+
Before sending to GPT, gather relevant context so GPT understands the project:
|
|
2150
|
+
|
|
2151
|
+
1. Check if \`CLAUDE.md\` or \`README.md\` exists \u2014 read the first 200 lines for project overview
|
|
2152
|
+
2. Check if \`.claude/settings.json\` or similar config exists
|
|
2153
|
+
3. Note the project language, framework, and key patterns
|
|
2154
|
+
|
|
2155
|
+
This context will be included in the prompt to reduce false positives.
|
|
2156
|
+
|
|
2157
|
+
### Step 2: Determine review mode
|
|
2158
|
+
|
|
2159
|
+
**If the user specifies a file or glob:**
|
|
2160
|
+
\`\`\`bash
|
|
2161
|
+
mulep review <file-or-glob> --focus all
|
|
2162
|
+
\`\`\`
|
|
2163
|
+
|
|
2164
|
+
**If the user specifies a diff:**
|
|
2165
|
+
\`\`\`bash
|
|
2166
|
+
mulep review --diff HEAD~3..HEAD
|
|
2167
|
+
\`\`\`
|
|
2168
|
+
|
|
2169
|
+
**If the user gives a freeform description:**
|
|
2170
|
+
\`\`\`bash
|
|
2171
|
+
mulep review --prompt "PROJECT CONTEXT: <context from step 1>
|
|
2172
|
+
|
|
2173
|
+
REVIEW TASK: <user's description>
|
|
2174
|
+
|
|
2175
|
+
Evaluate on: Correctness, Completeness, Quality, Security, Feasibility.
|
|
2176
|
+
For security findings, verify by reading the actual code before flagging.
|
|
2177
|
+
Provide SCORE: X/10 and VERDICT: APPROVED or NEEDS_REVISION"
|
|
2178
|
+
\`\`\`
|
|
2179
|
+
|
|
2180
|
+
**For presets:**
|
|
2181
|
+
\`\`\`bash
|
|
2182
|
+
mulep review <target> --preset security-audit
|
|
2183
|
+
mulep review <target> --preset quick-scan
|
|
2184
|
+
mulep review <target> --preset performance
|
|
2185
|
+
\`\`\`
|
|
2186
|
+
|
|
2187
|
+
### Step 3: Parse and present the output
|
|
2188
|
+
The command outputs JSON to stdout. Parse it and present as clean markdown:
|
|
2189
|
+
|
|
2190
|
+
\`\`\`
|
|
2191
|
+
## GPT Review Results
|
|
2192
|
+
|
|
2193
|
+
**Score**: X/10 | **Verdict**: APPROVED/NEEDS_REVISION
|
|
2194
|
+
|
|
2195
|
+
### Findings
|
|
2196
|
+
- [CRITICAL] file:line \u2014 description
|
|
2197
|
+
- [WARNING] file:line \u2014 description
|
|
2198
|
+
|
|
2199
|
+
### GPT's Full Analysis
|
|
2200
|
+
<review text>
|
|
2201
|
+
|
|
2202
|
+
Session: <sessionId> | Tokens: <usage> | Duration: <durationMs>ms
|
|
2203
|
+
\`\`\`
|
|
2204
|
+
|
|
2205
|
+
### Step 4: If NEEDS_REVISION
|
|
2206
|
+
Ask if user wants to fix and re-review. If yes:
|
|
2207
|
+
1. Fix the issues
|
|
2208
|
+
2. Run \`mulep review\` again \u2014 session resume gives GPT context of prior review
|
|
2209
|
+
3. GPT will check if previous issues were addressed
|
|
2210
|
+
|
|
2211
|
+
### Important Notes
|
|
2212
|
+
- **Session resume**: Each review builds on prior context. GPT remembers what it reviewed before.
|
|
2213
|
+
- **Codebase access**: GPT can read project files during review via codex tools.
|
|
2214
|
+
- **No arg size limits**: Content is piped via stdin, not passed as CLI args.
|
|
2215
|
+
- **Presets**: Use --preset for specialized reviews (security-audit, performance, quick-scan, pre-commit, api-review).
|
|
2216
|
+
- **Background mode**: Add --background to enqueue and continue working.
|
|
2217
|
+
- **Output capped**: JSON \`review\` field is capped to 2KB to prevent terminal crashes. Full text is stored in session_events.
|
|
2218
|
+
- **Progress**: Heartbeat every 60s, throttled to 30s intervals. No agent_message dumps.
|
|
2219
|
+
`
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
path: ".claude/skills/plan-review/SKILL.md",
|
|
2223
|
+
description: "/plan-review \u2014 GPT review of execution plans",
|
|
2224
|
+
content: `---
|
|
2225
|
+
name: plan-review
|
|
2226
|
+
description: Send execution plans to GPT for structured review via Codex CLI. Use when you have a written plan and want independent validation before implementation.
|
|
2227
|
+
user-invocable: true
|
|
2228
|
+
---
|
|
2229
|
+
|
|
2230
|
+
# /plan-review \u2014 GPT Review of Execution Plans
|
|
2231
|
+
|
|
2232
|
+
## Usage
|
|
2233
|
+
\`/plan-review <plan-file-or-description>\`
|
|
2234
|
+
|
|
2235
|
+
## Description
|
|
2236
|
+
Sends a Claude-authored execution plan to GPT via \`mulep plan review\` for structured critique. GPT returns ISSUE (HIGH/MEDIUM/LOW) and SUGGEST lines with file-level references.
|
|
2237
|
+
|
|
2238
|
+
## Instructions
|
|
2239
|
+
|
|
2240
|
+
### Step 1: Prepare the plan
|
|
2241
|
+
- If the user provides a file path, use it directly
|
|
2242
|
+
- If reviewing the current conversation's plan, write to a temp file first
|
|
2243
|
+
|
|
2244
|
+
### Step 2: Run plan review
|
|
2245
|
+
\`\`\`bash
|
|
2246
|
+
mulep plan review <plan-file> [--phase N] [--build BUILD_ID] [--timeout ms] [--output file]
|
|
2247
|
+
\`\`\`
|
|
2248
|
+
|
|
2249
|
+
### Step 3: Parse and present output
|
|
2250
|
+
JSON output includes: \`verdict\`, \`score\`, \`issues[]\` (severity + message), \`suggestions[]\`.
|
|
2251
|
+
|
|
2252
|
+
Present as:
|
|
2253
|
+
\`\`\`
|
|
2254
|
+
## GPT Plan Review
|
|
2255
|
+
**Score**: X/10 | **Verdict**: APPROVED/NEEDS_REVISION
|
|
2256
|
+
### Issues
|
|
2257
|
+
- [HIGH] description
|
|
2258
|
+
### Suggestions
|
|
2259
|
+
- suggestion text
|
|
2260
|
+
\`\`\`
|
|
2261
|
+
|
|
2262
|
+
### Step 4: If NEEDS_REVISION
|
|
2263
|
+
Fix HIGH issues, revise the plan, re-run. Session resume gives GPT context of prior review.
|
|
2264
|
+
|
|
2265
|
+
### Important Notes
|
|
2266
|
+
- **Session resume**: GPT remembers prior reviews via thread resume
|
|
2267
|
+
- **Codebase access**: GPT reads actual project files to verify plan references
|
|
2268
|
+
- **Output capped**: JSON \`review\` field is capped to 2KB. Use \`--output\` for full text
|
|
2269
|
+
- **Zero API cost**: Uses ChatGPT subscription via Codex CLI
|
|
2270
|
+
`
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
path: ".claude/skills/debate/SKILL.md",
|
|
2274
|
+
description: "/debate \u2014 Claude vs GPT multi-round debate",
|
|
2275
|
+
content: `---
|
|
2276
|
+
name: debate
|
|
2277
|
+
description: Real Claude vs GPT multi-round debate. Use when you need a second opinion, want to debate architecture decisions, or evaluate competing approaches.
|
|
2278
|
+
user-invocable: true
|
|
2279
|
+
---
|
|
2280
|
+
|
|
2281
|
+
# /debate \u2014 Real Claude vs GPT Multi-Round Debate
|
|
2282
|
+
|
|
2283
|
+
## Usage
|
|
2284
|
+
\`/debate <topic or question>\`
|
|
2285
|
+
|
|
2286
|
+
## Description
|
|
2287
|
+
Structured debate: Claude proposes, GPT critiques, Claude revises, GPT re-evaluates \u2014 looping until convergence or max rounds. Real multi-model collaboration via mulep CLI with session persistence.
|
|
2288
|
+
|
|
2289
|
+
## Instructions
|
|
2290
|
+
|
|
2291
|
+
### Phase 0: Setup
|
|
2292
|
+
1. Parse topic from user's message
|
|
2293
|
+
2. Start debate:
|
|
2294
|
+
\`\`\`bash
|
|
2295
|
+
mulep debate start "TOPIC_HERE"
|
|
2296
|
+
\`\`\`
|
|
2297
|
+
3. Save the \`debateId\` from JSON output
|
|
2298
|
+
4. Announce: "Entering debate mode: Claude vs GPT"
|
|
2299
|
+
|
|
2300
|
+
### Phase 1: Claude's Opening Proposal
|
|
2301
|
+
Think deeply. Generate your genuine proposal. Be thorough and specific.
|
|
2302
|
+
|
|
2303
|
+
### Phase 1.5: Gather Codebase Context
|
|
2304
|
+
If topic relates to code, use Grep/Glob/Read to find relevant files. Summarize for GPT.
|
|
2305
|
+
|
|
2306
|
+
### Phase 2: Send to GPT
|
|
2307
|
+
\`\`\`bash
|
|
2308
|
+
mulep debate turn DEBATE_ID "You are a senior technical reviewer debating with Claude about a codebase. You have full access to project files.
|
|
2309
|
+
|
|
2310
|
+
DEBATE TOPIC: <topic>
|
|
2311
|
+
CODEBASE CONTEXT: <summary>
|
|
2312
|
+
CLAUDE'S PROPOSAL: <proposal>
|
|
2313
|
+
|
|
2314
|
+
Respond with:
|
|
2315
|
+
1. What you agree with
|
|
2316
|
+
2. What you disagree with
|
|
2317
|
+
3. Suggested improvements
|
|
2318
|
+
4. STANCE: SUPPORT, OPPOSE, or UNCERTAIN" --round N
|
|
2319
|
+
\`\`\`
|
|
2320
|
+
|
|
2321
|
+
### Phase 3: Check Convergence
|
|
2322
|
+
- STANCE: SUPPORT \u2192 go to Phase 5
|
|
2323
|
+
- Max rounds reached \u2192 go to Phase 5
|
|
2324
|
+
- Otherwise \u2192 Phase 4
|
|
2325
|
+
|
|
2326
|
+
### Phase 4: Claude's Revision
|
|
2327
|
+
Read GPT's critique. Revise genuinely. Send back to GPT.
|
|
2328
|
+
|
|
2329
|
+
### Phase 5: Final Synthesis
|
|
2330
|
+
\`\`\`bash
|
|
2331
|
+
mulep debate complete DEBATE_ID
|
|
2332
|
+
\`\`\`
|
|
2333
|
+
Present: final position, agreements, disagreements, stats.
|
|
2334
|
+
|
|
2335
|
+
### Rules
|
|
2336
|
+
1. Be genuine \u2014 don't just agree to end the debate
|
|
2337
|
+
2. Session resume is automatic via callWithResume()
|
|
2338
|
+
3. State persisted to SQLite
|
|
2339
|
+
4. Zero API cost (ChatGPT subscription)
|
|
2340
|
+
5. 600s default timeout per turn
|
|
2341
|
+
6. JSON \`response\` field capped to 2KB \u2014 check \`responseTruncated\` boolean
|
|
2342
|
+
7. Progress heartbeat every 60s, throttled to 30s intervals
|
|
2343
|
+
`
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
path: ".claude/skills/build/SKILL.md",
|
|
2347
|
+
description: "/build \u2014 Autonomous build loop with GPT review",
|
|
2348
|
+
content: `---
|
|
2349
|
+
name: build
|
|
2350
|
+
description: Autonomous build loop \u2014 debate, plan, implement, review, fix \u2014 all in one session with GPT review.
|
|
2351
|
+
user-invocable: true
|
|
2352
|
+
---
|
|
2353
|
+
|
|
2354
|
+
# /build \u2014 Autonomous Build Loop
|
|
2355
|
+
|
|
2356
|
+
## Usage
|
|
2357
|
+
\`/build <task description>\`
|
|
2358
|
+
|
|
2359
|
+
## Description
|
|
2360
|
+
Full pipeline: debate approach with GPT \u2192 user approval \u2192 implement \u2192 GPT review \u2192 fix \u2192 re-review until approved. SQLite tracking throughout.
|
|
2361
|
+
|
|
2362
|
+
## Instructions
|
|
2363
|
+
|
|
2364
|
+
### Phase 0: Initialize
|
|
2365
|
+
1. Record user's exact request (acceptance criteria)
|
|
2366
|
+
2. \`mulep build start "TASK"\`
|
|
2367
|
+
3. Save buildId and debateId
|
|
2368
|
+
|
|
2369
|
+
### Phase 1: Debate the Approach (MANDATORY)
|
|
2370
|
+
Use /debate protocol. Loop until GPT says STANCE: SUPPORT.
|
|
2371
|
+
- Gather codebase context first
|
|
2372
|
+
- Send detailed implementation plan to GPT
|
|
2373
|
+
- Revise on OPPOSE/UNCERTAIN \u2014 never skip
|
|
2374
|
+
|
|
2375
|
+
### Phase 1.25: Plan Review (Recommended)
|
|
2376
|
+
Before user approval, validate the plan with GPT:
|
|
2377
|
+
\`\`\`bash
|
|
2378
|
+
mulep plan review /path/to/plan.md --build BUILD_ID
|
|
2379
|
+
\`\`\`
|
|
2380
|
+
GPT reviews the plan against actual codebase, returns ISSUE/SUGGEST lines.
|
|
2381
|
+
Fix HIGH issues before presenting to user.
|
|
2382
|
+
|
|
2383
|
+
### Phase 1.5: User Approval Gate
|
|
2384
|
+
Present agreed plan. Wait for explicit approval via AskUserQuestion.
|
|
2385
|
+
\`\`\`bash
|
|
2386
|
+
mulep build event BUILD_ID plan_approved
|
|
2387
|
+
mulep debate complete DEBATE_ID
|
|
2388
|
+
\`\`\`
|
|
2389
|
+
|
|
2390
|
+
### Phase 2: Implement
|
|
2391
|
+
Write code. Run tests: \`pnpm run test\`
|
|
2392
|
+
Never send broken code to review.
|
|
2393
|
+
\`\`\`bash
|
|
2394
|
+
mulep build event BUILD_ID impl_completed
|
|
2395
|
+
\`\`\`
|
|
2396
|
+
|
|
2397
|
+
### Phase 3: GPT Review
|
|
2398
|
+
\`\`\`bash
|
|
2399
|
+
mulep build review BUILD_ID
|
|
2400
|
+
\`\`\`
|
|
2401
|
+
Parse verdict: approved \u2192 Phase 4.5, needs_revision \u2192 Phase 4
|
|
2402
|
+
|
|
2403
|
+
### Phase 4: Fix Issues
|
|
2404
|
+
Fix every CRITICAL and BUG. Run tests. Back to Phase 3.
|
|
2405
|
+
\`\`\`bash
|
|
2406
|
+
mulep build event BUILD_ID fix_completed
|
|
2407
|
+
\`\`\`
|
|
2408
|
+
|
|
2409
|
+
### Phase 4.5: Completeness Check
|
|
2410
|
+
Compare deliverables against original request. Every requirement must be met.
|
|
2411
|
+
|
|
2412
|
+
### Phase 5: Done
|
|
2413
|
+
\`\`\`bash
|
|
2414
|
+
mulep build status BUILD_ID
|
|
2415
|
+
\`\`\`
|
|
2416
|
+
Present summary with metrics, requirements checklist, GPT verdict.
|
|
2417
|
+
|
|
2418
|
+
### Rules
|
|
2419
|
+
1. NEVER skip debate rounds
|
|
2420
|
+
2. NEVER skip user approval
|
|
2421
|
+
3. NEVER declare done without completeness check
|
|
2422
|
+
4. Run tests after every implementation/fix
|
|
2423
|
+
5. Zero API cost (ChatGPT subscription)
|
|
2424
|
+
`
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
path: ".claude/skills/cleanup/SKILL.md",
|
|
2428
|
+
description: "/cleanup \u2014 Bidirectional AI slop scanner",
|
|
2429
|
+
content: `---
|
|
2430
|
+
name: cleanup
|
|
2431
|
+
description: Bidirectional AI slop scanner \u2014 Claude + GPT independently analyze, then debate disagreements.
|
|
2432
|
+
user-invocable: true
|
|
2433
|
+
---
|
|
2434
|
+
|
|
2435
|
+
# /cleanup \u2014 Bidirectional AI Slop Scanner
|
|
2436
|
+
|
|
2437
|
+
## Usage
|
|
2438
|
+
\`/cleanup [scope]\` where scope is: deps, unused-exports, hardcoded, duplicates, deadcode, or all
|
|
2439
|
+
|
|
2440
|
+
## Description
|
|
2441
|
+
Claude analyzes independently, then mulep cleanup runs deterministic regex + GPT scans. 3-way merge with majority-vote confidence.
|
|
2442
|
+
|
|
2443
|
+
## Instructions
|
|
2444
|
+
|
|
2445
|
+
### Phase 1: Claude Independent Analysis
|
|
2446
|
+
Scan the codebase yourself using Grep/Glob/Read. For each scope:
|
|
2447
|
+
- **deps**: Check package.json deps against actual imports
|
|
2448
|
+
- **unused-exports**: Find exported symbols not imported elsewhere
|
|
2449
|
+
- **hardcoded**: Magic numbers, URLs, credentials
|
|
2450
|
+
- **duplicates**: Similar function logic across files
|
|
2451
|
+
- **deadcode**: Declared but never referenced
|
|
2452
|
+
|
|
2453
|
+
Save findings as JSON to a temp file.
|
|
2454
|
+
|
|
2455
|
+
### Phase 2: Run mulep cleanup
|
|
2456
|
+
\`\`\`bash
|
|
2457
|
+
mulep cleanup --scope SCOPE --host-findings /path/to/claude-findings.json
|
|
2458
|
+
\`\`\`
|
|
2459
|
+
|
|
2460
|
+
### Phase 3: Present merged results
|
|
2461
|
+
Show summary: total, high confidence, disputed, adjudicated, by source.
|
|
2462
|
+
|
|
2463
|
+
### Phase 4: Rebuttal Round
|
|
2464
|
+
For Claude/GPT disagreements, optionally debate via \`mulep debate turn\`.
|
|
2465
|
+
`
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
path: ".claude/agents/codex-liaison.md",
|
|
2469
|
+
description: "Codex Liaison agent \u2014 iterates with GPT until 9.5/10",
|
|
2470
|
+
content: `# Codex Liaison Agent
|
|
2471
|
+
|
|
2472
|
+
## Role
|
|
2473
|
+
Specialized teammate that communicates with GPT via mulep CLI to get independent reviews and iterate until quality threshold is met.
|
|
2474
|
+
|
|
2475
|
+
## How You Work
|
|
2476
|
+
1. Send content to GPT via \`mulep review\` or \`mulep plan review\`
|
|
2477
|
+
2. Parse JSON output for score, verdict, findings
|
|
2478
|
+
3. If NEEDS_REVISION: fix issues and re-submit (session resume retains context)
|
|
2479
|
+
4. Loop until APPROVED or max iterations
|
|
2480
|
+
5. Report final version back to team lead
|
|
2481
|
+
|
|
2482
|
+
## Available Commands
|
|
2483
|
+
\`\`\`bash
|
|
2484
|
+
mulep review <file-or-glob> # Code review
|
|
2485
|
+
mulep review --diff HEAD~3..HEAD # Diff review
|
|
2486
|
+
mulep plan review <plan-file> # Plan review
|
|
2487
|
+
mulep debate turn DEBATE_ID "prompt" # Debate turn
|
|
2488
|
+
mulep fix <file-or-glob> # Autofix loop
|
|
2489
|
+
\`\`\`
|
|
2490
|
+
|
|
2491
|
+
## Important Rules
|
|
2492
|
+
- NEVER fabricate GPT's responses \u2014 always parse actual JSON output
|
|
2493
|
+
- NEVER skip iterations if GPT says NEEDS_REVISION
|
|
2494
|
+
- Use your own judgment when GPT's feedback conflicts with project requirements
|
|
2495
|
+
- JSON \`review\`/\`response\` fields are capped to 2KB; use \`--output\` for full text
|
|
2496
|
+
- All commands use session resume \u2014 GPT retains context across calls
|
|
2497
|
+
- Zero API cost (ChatGPT subscription via Codex CLI)
|
|
2498
|
+
`
|
|
2499
|
+
}
|
|
2500
|
+
];
|
|
2501
|
+
var CLAUDE_MD_SECTION = `
|
|
2502
|
+
## MuleP \u2014 Multi-Model Collaboration
|
|
2503
|
+
|
|
2504
|
+
This project uses [MuleP](https://github.com/katarmal-ram/mulep) for Claude + GPT collaboration. MuleP bridges Claude Code and Codex CLI so they work as partners \u2014 one plans, the other reviews.
|
|
2505
|
+
|
|
2506
|
+
### How Sessions Work
|
|
2507
|
+
- Every \`mulep\` command uses a **unified session** with GPT via Codex CLI
|
|
2508
|
+
- Sessions persist across commands \u2014 GPT remembers prior reviews, debates, and fixes
|
|
2509
|
+
- Sessions are stored in \`.mulep/db/mulep.db\` (SQLite)
|
|
2510
|
+
- When a session's token budget fills up, it auto-rolls to a new thread
|
|
2511
|
+
- Run \`mulep session current\` to see the active session
|
|
2512
|
+
|
|
2513
|
+
### Available Commands (use these, not raw codex)
|
|
2514
|
+
- \`mulep review <file-or-dir>\` \u2014 GPT reviews code with codebase access
|
|
2515
|
+
- \`mulep review --prompt "question"\` \u2014 GPT explores codebase to answer
|
|
2516
|
+
- \`mulep review --diff HEAD~3..HEAD\` \u2014 Review git changes
|
|
2517
|
+
- \`mulep review --preset security-audit\` \u2014 Specialized review presets
|
|
2518
|
+
- \`mulep fix <file>\` \u2014 Autofix loop: review \u2192 apply fixes \u2192 re-review
|
|
2519
|
+
- \`mulep debate start "topic"\` \u2014 Multi-round Claude vs GPT debate
|
|
2520
|
+
- \`mulep cleanup\` \u2014 Scan for unused deps, dead code, duplicates
|
|
2521
|
+
- \`mulep plan generate "task"\` \u2014 Generate plans via multi-model loop
|
|
2522
|
+
- \`mulep plan review <plan-file>\` \u2014 GPT review of execution plans
|
|
2523
|
+
- \`mulep shipit --profile safe\` \u2014 Composite workflow (lint+test+review)
|
|
2524
|
+
- \`mulep cost\` \u2014 Token usage dashboard
|
|
2525
|
+
- \`mulep doctor\` \u2014 Check prerequisites
|
|
2526
|
+
|
|
2527
|
+
### Slash Commands
|
|
2528
|
+
- \`/codex-review\` \u2014 Quick GPT review (uses mulep review internally)
|
|
2529
|
+
- \`/debate\` \u2014 Start a Claude vs GPT debate
|
|
2530
|
+
- \`/plan-review\` \u2014 GPT review of execution plans
|
|
2531
|
+
- \`/build\` \u2014 Full build loop: debate \u2192 plan \u2192 implement \u2192 GPT review \u2192 fix
|
|
2532
|
+
- \`/cleanup\` \u2014 Bidirectional AI slop scanner
|
|
2533
|
+
|
|
2534
|
+
### When to Use MuleP
|
|
2535
|
+
- After implementing a feature \u2192 \`mulep review src/\`
|
|
2536
|
+
- Before committing \u2192 \`mulep review --diff HEAD --preset pre-commit\`
|
|
2537
|
+
- Architecture decisions \u2192 \`/debate "REST vs GraphQL?"\`
|
|
2538
|
+
- Full feature build \u2192 \`/build "add user authentication"\`
|
|
2539
|
+
- After shipping \u2192 \`mulep shipit --profile safe\`
|
|
2540
|
+
|
|
2541
|
+
### Session Tips
|
|
2542
|
+
- Sessions auto-resume \u2014 GPT retains context from prior commands
|
|
2543
|
+
- \`mulep session list\` shows all sessions with token usage
|
|
2544
|
+
- \`mulep cost --scope session\` shows current session spend
|
|
2545
|
+
- Start fresh with \`mulep session start --name "new-feature"\`
|
|
2546
|
+
`;
|
|
2547
|
+
var HOOKS_CONFIG = {
|
|
2548
|
+
hooks: {
|
|
2549
|
+
PostToolUse: [
|
|
2550
|
+
{
|
|
2551
|
+
matcher: "Bash",
|
|
2552
|
+
pattern: "git commit",
|
|
2553
|
+
command: 'echo "Tip: Run mulep review --diff HEAD~1 for a GPT review of this commit"'
|
|
2554
|
+
}
|
|
2555
|
+
]
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
async function installSkillsCommand(options) {
|
|
2559
|
+
const cwd = process.cwd();
|
|
2560
|
+
let installed = 0;
|
|
2561
|
+
let skipped = 0;
|
|
2562
|
+
console.error(chalk11.cyan("\n Installing MuleP integration for Claude Code\n"));
|
|
2563
|
+
console.error(chalk11.dim(" Skills & Agents:"));
|
|
2564
|
+
for (const skill of SKILLS) {
|
|
2565
|
+
const fullPath = join6(cwd, skill.path);
|
|
2566
|
+
const dir = dirname(fullPath);
|
|
2567
|
+
if (existsSync3(fullPath) && !options.force) {
|
|
2568
|
+
console.error(chalk11.dim(` SKIP ${skill.path} (exists)`));
|
|
2569
|
+
skipped++;
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
mkdirSync3(dir, { recursive: true });
|
|
2573
|
+
writeFileSync3(fullPath, skill.content, "utf-8");
|
|
2574
|
+
console.error(chalk11.green(` OK ${skill.path}`));
|
|
2575
|
+
installed++;
|
|
2576
|
+
}
|
|
2577
|
+
console.error("");
|
|
2578
|
+
console.error(chalk11.dim(" CLAUDE.md:"));
|
|
2579
|
+
const claudeMdPath = join6(cwd, "CLAUDE.md");
|
|
2580
|
+
const marker = "## MuleP \u2014 Multi-Model Collaboration";
|
|
2581
|
+
if (existsSync3(claudeMdPath)) {
|
|
2582
|
+
const existing = readFileSync3(claudeMdPath, "utf-8");
|
|
2583
|
+
if (existing.includes(marker)) {
|
|
2584
|
+
if (options.force) {
|
|
2585
|
+
const markerIdx = existing.indexOf(marker);
|
|
2586
|
+
const before = existing.slice(0, markerIdx);
|
|
2587
|
+
const sectionEnd = existing.indexOf(CLAUDE_MD_SECTION.trimEnd(), markerIdx);
|
|
2588
|
+
const afterMarker = sectionEnd >= 0 ? existing.slice(sectionEnd + CLAUDE_MD_SECTION.trimEnd().length) : existing.slice(markerIdx + marker.length);
|
|
2589
|
+
const nextHeadingMatch = sectionEnd >= 0 ? null : afterMarker.match(/\n#{1,6} (?!MuleP)/);
|
|
2590
|
+
const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
|
|
2591
|
+
writeFileSync3(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
|
|
2592
|
+
console.error(chalk11.green(" OK CLAUDE.md (updated MuleP section)"));
|
|
2593
|
+
installed++;
|
|
2594
|
+
} else {
|
|
2595
|
+
console.error(chalk11.dim(" SKIP CLAUDE.md (MuleP section exists)"));
|
|
2596
|
+
skipped++;
|
|
2597
|
+
}
|
|
2598
|
+
} else {
|
|
2599
|
+
writeFileSync3(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
|
|
2600
|
+
console.error(chalk11.green(" OK CLAUDE.md (appended MuleP section)"));
|
|
2601
|
+
installed++;
|
|
2602
|
+
}
|
|
2603
|
+
} else {
|
|
2604
|
+
writeFileSync3(claudeMdPath, `# Project Instructions
|
|
2605
|
+
${CLAUDE_MD_SECTION}`, "utf-8");
|
|
2606
|
+
console.error(chalk11.green(" OK CLAUDE.md (created with MuleP section)"));
|
|
2607
|
+
installed++;
|
|
2608
|
+
}
|
|
2609
|
+
console.error("");
|
|
2610
|
+
console.error(chalk11.dim(" Hooks:"));
|
|
2611
|
+
const settingsDir = join6(cwd, ".claude");
|
|
2612
|
+
const settingsPath = join6(settingsDir, "settings.json");
|
|
2613
|
+
if (existsSync3(settingsPath)) {
|
|
2614
|
+
try {
|
|
2615
|
+
const existing = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
2616
|
+
const hasMulepHook = Array.isArray(existing.hooks?.PostToolUse) && existing.hooks.PostToolUse.some((h) => h.command?.includes("mulep"));
|
|
2617
|
+
if (hasMulepHook && !options.force) {
|
|
2618
|
+
console.error(chalk11.dim(" SKIP .claude/settings.json (mulep hook exists)"));
|
|
2619
|
+
skipped++;
|
|
2620
|
+
} else {
|
|
2621
|
+
const otherHooks = Array.isArray(existing.hooks?.PostToolUse) ? existing.hooks.PostToolUse.filter((h) => !h.command?.includes("mulep")) : [];
|
|
2622
|
+
existing.hooks = {
|
|
2623
|
+
...existing.hooks,
|
|
2624
|
+
PostToolUse: [...otherHooks, ...HOOKS_CONFIG.hooks.PostToolUse]
|
|
2625
|
+
};
|
|
2626
|
+
writeFileSync3(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
2627
|
+
console.error(chalk11.green(" OK .claude/settings.json (added post-commit hint hook)"));
|
|
2628
|
+
installed++;
|
|
2629
|
+
}
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
console.error(chalk11.yellow(` WARN .claude/settings.json parse error: ${err.message}`));
|
|
2632
|
+
console.error(chalk11.yellow(" Back up and delete the file, then re-run install-skills"));
|
|
2633
|
+
skipped++;
|
|
2634
|
+
}
|
|
2635
|
+
} else {
|
|
2636
|
+
mkdirSync3(settingsDir, { recursive: true });
|
|
2637
|
+
writeFileSync3(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
|
|
2638
|
+
console.error(chalk11.green(" OK .claude/settings.json (created with post-commit hook)"));
|
|
2639
|
+
installed++;
|
|
2640
|
+
}
|
|
2641
|
+
console.error("");
|
|
2642
|
+
console.error(chalk11.cyan(` Installed: ${installed}, Skipped: ${skipped}`));
|
|
2643
|
+
console.error("");
|
|
2644
|
+
console.error(chalk11.dim(" Slash commands: /codex-review, /debate, /plan-review, /build, /cleanup"));
|
|
2645
|
+
console.error(chalk11.dim(" CLAUDE.md: Claude now knows about mulep commands & sessions"));
|
|
2646
|
+
console.error(chalk11.dim(" Hook: Post-commit hint to run mulep review"));
|
|
2647
|
+
console.error("");
|
|
2648
|
+
const output = {
|
|
2649
|
+
installed,
|
|
2650
|
+
skipped,
|
|
2651
|
+
total: SKILLS.length + 2,
|
|
2652
|
+
// +CLAUDE.md +hooks
|
|
2653
|
+
skills: SKILLS.map((s) => ({ path: s.path, description: s.description }))
|
|
2654
|
+
};
|
|
2655
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// src/commands/session.ts
|
|
2659
|
+
import { SessionManager as SessionManager6 } from "@mulep/core";
|
|
2660
|
+
import chalk12 from "chalk";
|
|
2661
|
+
async function sessionStartCommand(options) {
|
|
2662
|
+
await withDatabase(async (db) => {
|
|
2663
|
+
const mgr = new SessionManager6(db);
|
|
2664
|
+
const id = mgr.create(options.name);
|
|
2665
|
+
const session2 = mgr.get(id);
|
|
2666
|
+
mgr.recordEvent({
|
|
2667
|
+
sessionId: id,
|
|
2668
|
+
command: "session",
|
|
2669
|
+
subcommand: "start",
|
|
2670
|
+
promptPreview: `Session started: ${session2?.name ?? id}`
|
|
2671
|
+
});
|
|
2672
|
+
console.log(JSON.stringify({
|
|
2673
|
+
sessionId: id,
|
|
2674
|
+
name: session2?.name ?? null,
|
|
2675
|
+
status: "active",
|
|
2676
|
+
message: "Session created. All GPT commands will now use this session."
|
|
2677
|
+
}, null, 2));
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
async function sessionCurrentCommand() {
|
|
2681
|
+
await withDatabase(async (db) => {
|
|
2682
|
+
const mgr = new SessionManager6(db);
|
|
2683
|
+
const session2 = mgr.getActive();
|
|
2684
|
+
if (!session2) {
|
|
2685
|
+
console.log(JSON.stringify({ active: false, message: 'No active session. Run "mulep session start" to create one.' }));
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
const events = mgr.getEvents(session2.id, 5);
|
|
2689
|
+
console.log(JSON.stringify({
|
|
2690
|
+
sessionId: session2.id,
|
|
2691
|
+
name: session2.name,
|
|
2692
|
+
codexThreadId: session2.codexThreadId,
|
|
2693
|
+
status: session2.status,
|
|
2694
|
+
tokenUsage: session2.tokenUsage,
|
|
2695
|
+
recentEvents: events.map((e) => ({
|
|
2696
|
+
command: e.command,
|
|
2697
|
+
subcommand: e.subcommand,
|
|
2698
|
+
durationMs: e.durationMs,
|
|
2699
|
+
createdAt: new Date(e.createdAt).toISOString()
|
|
2700
|
+
})),
|
|
2701
|
+
createdAt: new Date(session2.createdAt).toISOString(),
|
|
2702
|
+
updatedAt: new Date(session2.updatedAt).toISOString()
|
|
2703
|
+
}, null, 2));
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
async function sessionListCommand(options) {
|
|
2707
|
+
await withDatabase(async (db) => {
|
|
2708
|
+
const mgr = new SessionManager6(db);
|
|
2709
|
+
const sessions = mgr.list({
|
|
2710
|
+
status: options.status,
|
|
2711
|
+
limit: options.limit ?? 20
|
|
2712
|
+
});
|
|
2713
|
+
const output = sessions.map((s) => ({
|
|
2714
|
+
sessionId: s.id,
|
|
2715
|
+
name: s.name,
|
|
2716
|
+
status: s.status,
|
|
2717
|
+
codexThreadId: s.codexThreadId ? `${s.codexThreadId.slice(0, 12)}...` : null,
|
|
2718
|
+
tokenUsage: s.tokenUsage,
|
|
2719
|
+
createdAt: new Date(s.createdAt).toISOString(),
|
|
2720
|
+
updatedAt: new Date(s.updatedAt).toISOString()
|
|
2721
|
+
}));
|
|
2722
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
async function sessionStatusCommand(sessionId) {
|
|
2726
|
+
await withDatabase(async (db) => {
|
|
2727
|
+
const mgr = new SessionManager6(db);
|
|
2728
|
+
const session2 = mgr.get(sessionId);
|
|
2729
|
+
if (!session2) {
|
|
2730
|
+
console.error(chalk12.red(`No session found with ID: ${sessionId}`));
|
|
2731
|
+
process.exit(1);
|
|
2732
|
+
}
|
|
2733
|
+
const events = mgr.getEvents(sessionId, 20);
|
|
2734
|
+
console.log(JSON.stringify({
|
|
2735
|
+
sessionId: session2.id,
|
|
2736
|
+
name: session2.name,
|
|
2737
|
+
codexThreadId: session2.codexThreadId,
|
|
2738
|
+
status: session2.status,
|
|
2739
|
+
tokenUsage: session2.tokenUsage,
|
|
2740
|
+
eventCount: events.length,
|
|
2741
|
+
events: events.map((e) => ({
|
|
2742
|
+
command: e.command,
|
|
2743
|
+
subcommand: e.subcommand,
|
|
2744
|
+
promptPreview: e.promptPreview,
|
|
2745
|
+
responsePreview: e.responsePreview,
|
|
2746
|
+
durationMs: e.durationMs,
|
|
2747
|
+
createdAt: new Date(e.createdAt).toISOString()
|
|
2748
|
+
})),
|
|
2749
|
+
createdAt: new Date(session2.createdAt).toISOString(),
|
|
2750
|
+
updatedAt: new Date(session2.updatedAt).toISOString(),
|
|
2751
|
+
completedAt: session2.completedAt ? new Date(session2.completedAt).toISOString() : null
|
|
2752
|
+
}, null, 2));
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
async function sessionCloseCommand(sessionId) {
|
|
2756
|
+
await withDatabase(async (db) => {
|
|
2757
|
+
const mgr = new SessionManager6(db);
|
|
2758
|
+
const session2 = mgr.get(sessionId);
|
|
2759
|
+
if (!session2) {
|
|
2760
|
+
console.error(chalk12.red(`No session found with ID: ${sessionId}`));
|
|
2761
|
+
process.exit(1);
|
|
2762
|
+
}
|
|
2763
|
+
mgr.complete(sessionId);
|
|
2764
|
+
console.log(JSON.stringify({ sessionId, status: "completed" }));
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// src/commands/jobs.ts
|
|
2769
|
+
import { JobStore as JobStore2, openDatabase as openDatabase8 } from "@mulep/core";
|
|
2770
|
+
import chalk13 from "chalk";
|
|
2771
|
+
async function jobsListCommand(options) {
|
|
2772
|
+
let db;
|
|
2773
|
+
try {
|
|
2774
|
+
db = openDatabase8(getDbPath());
|
|
2775
|
+
const store = new JobStore2(db);
|
|
2776
|
+
const jobs2 = store.list({
|
|
2777
|
+
status: options.status,
|
|
2778
|
+
type: options.type,
|
|
2779
|
+
limit: options.limit ?? 20
|
|
2780
|
+
});
|
|
2781
|
+
const output = jobs2.map((j) => ({
|
|
2782
|
+
id: j.id,
|
|
2783
|
+
type: j.type,
|
|
2784
|
+
status: j.status,
|
|
2785
|
+
priority: j.priority,
|
|
2786
|
+
retryCount: j.retryCount,
|
|
2787
|
+
workerId: j.workerId,
|
|
2788
|
+
createdAt: new Date(j.createdAt).toISOString(),
|
|
2789
|
+
startedAt: j.startedAt ? new Date(j.startedAt).toISOString() : null,
|
|
2790
|
+
finishedAt: j.finishedAt ? new Date(j.finishedAt).toISOString() : null
|
|
2791
|
+
}));
|
|
2792
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2793
|
+
db.close();
|
|
2794
|
+
} catch (error) {
|
|
2795
|
+
db?.close();
|
|
2796
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2797
|
+
process.exit(1);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
async function jobsLogsCommand(jobId, options) {
|
|
2801
|
+
let db;
|
|
2802
|
+
try {
|
|
2803
|
+
db = openDatabase8(getDbPath());
|
|
2804
|
+
const store = new JobStore2(db);
|
|
2805
|
+
const job = store.get(jobId);
|
|
2806
|
+
if (!job) {
|
|
2807
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2808
|
+
db.close();
|
|
2809
|
+
process.exit(1);
|
|
2810
|
+
}
|
|
2811
|
+
const logs = store.getLogs(jobId, options.fromSeq ?? 0, options.limit ?? 100);
|
|
2812
|
+
const output = {
|
|
2813
|
+
jobId: job.id,
|
|
2814
|
+
type: job.type,
|
|
2815
|
+
status: job.status,
|
|
2816
|
+
logs: logs.map((l) => ({
|
|
2817
|
+
seq: l.seq,
|
|
2818
|
+
level: l.level,
|
|
2819
|
+
event: l.eventType,
|
|
2820
|
+
message: l.message,
|
|
2821
|
+
time: new Date(l.createdAt).toISOString()
|
|
2822
|
+
}))
|
|
2823
|
+
};
|
|
2824
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2825
|
+
db.close();
|
|
2826
|
+
} catch (error) {
|
|
2827
|
+
db?.close();
|
|
2828
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2829
|
+
process.exit(1);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
async function jobsCancelCommand(jobId) {
|
|
2833
|
+
let db;
|
|
2834
|
+
try {
|
|
2835
|
+
db = openDatabase8(getDbPath());
|
|
2836
|
+
const store = new JobStore2(db);
|
|
2837
|
+
const job = store.get(jobId);
|
|
2838
|
+
if (!job) {
|
|
2839
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2840
|
+
db.close();
|
|
2841
|
+
process.exit(1);
|
|
2842
|
+
}
|
|
2843
|
+
store.cancel(jobId);
|
|
2844
|
+
console.log(JSON.stringify({ jobId, status: "canceled" }));
|
|
2845
|
+
db.close();
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
db?.close();
|
|
2848
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2849
|
+
process.exit(1);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
async function jobsRetryCommand(jobId) {
|
|
2853
|
+
let db;
|
|
2854
|
+
try {
|
|
2855
|
+
db = openDatabase8(getDbPath());
|
|
2856
|
+
const store = new JobStore2(db);
|
|
2857
|
+
const job = store.get(jobId);
|
|
2858
|
+
if (!job) {
|
|
2859
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2860
|
+
db.close();
|
|
2861
|
+
process.exit(1);
|
|
2862
|
+
}
|
|
2863
|
+
const retried = store.retry(jobId);
|
|
2864
|
+
if (!retried) {
|
|
2865
|
+
console.error(chalk13.red(`Cannot retry job ${jobId}: status=${job.status}, retries=${job.retryCount}/${job.maxRetries}`));
|
|
2866
|
+
db.close();
|
|
2867
|
+
process.exit(1);
|
|
2868
|
+
}
|
|
2869
|
+
console.log(JSON.stringify({ jobId, status: "queued", retryCount: job.retryCount + 1 }));
|
|
2870
|
+
db.close();
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
db?.close();
|
|
2873
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2874
|
+
process.exit(1);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
async function jobsStatusCommand(jobId) {
|
|
2878
|
+
let db;
|
|
2879
|
+
try {
|
|
2880
|
+
db = openDatabase8(getDbPath());
|
|
2881
|
+
const store = new JobStore2(db);
|
|
2882
|
+
const job = store.get(jobId);
|
|
2883
|
+
if (!job) {
|
|
2884
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2885
|
+
db.close();
|
|
2886
|
+
process.exit(1);
|
|
2887
|
+
}
|
|
2888
|
+
const logs = store.getLogs(jobId, 0, 5);
|
|
2889
|
+
const output = {
|
|
2890
|
+
id: job.id,
|
|
2891
|
+
type: job.type,
|
|
2892
|
+
status: job.status,
|
|
2893
|
+
priority: job.priority,
|
|
2894
|
+
retryCount: job.retryCount,
|
|
2895
|
+
maxRetries: job.maxRetries,
|
|
2896
|
+
workerId: job.workerId,
|
|
2897
|
+
sessionId: job.sessionId,
|
|
2898
|
+
payload: JSON.parse(job.payloadJson),
|
|
2899
|
+
result: job.resultJson ? JSON.parse(job.resultJson) : null,
|
|
2900
|
+
error: job.errorText,
|
|
2901
|
+
recentLogs: logs.map((l) => ({
|
|
2902
|
+
seq: l.seq,
|
|
2903
|
+
level: l.level,
|
|
2904
|
+
event: l.eventType,
|
|
2905
|
+
message: l.message
|
|
2906
|
+
})),
|
|
2907
|
+
createdAt: new Date(job.createdAt).toISOString(),
|
|
2908
|
+
startedAt: job.startedAt ? new Date(job.startedAt).toISOString() : null,
|
|
2909
|
+
finishedAt: job.finishedAt ? new Date(job.finishedAt).toISOString() : null
|
|
2910
|
+
};
|
|
2911
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2912
|
+
db.close();
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
db?.close();
|
|
2915
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2916
|
+
process.exit(1);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// src/commands/plan.ts
|
|
2921
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2922
|
+
import {
|
|
2923
|
+
ModelRegistry as ModelRegistry4,
|
|
2924
|
+
Orchestrator,
|
|
2925
|
+
SessionManager as SessionManager7,
|
|
2926
|
+
buildHandoffEnvelope as buildHandoffEnvelope4,
|
|
2927
|
+
loadConfig as loadConfig6,
|
|
2928
|
+
openDatabase as openDatabase9
|
|
2929
|
+
} from "@mulep/core";
|
|
2930
|
+
import chalk15 from "chalk";
|
|
2931
|
+
|
|
2932
|
+
// src/render.ts
|
|
2933
|
+
import chalk14 from "chalk";
|
|
2934
|
+
import ora from "ora";
|
|
2935
|
+
var roleColors = {
|
|
2936
|
+
architect: chalk14.blue,
|
|
2937
|
+
reviewer: chalk14.yellow,
|
|
2938
|
+
implementer: chalk14.green
|
|
2939
|
+
};
|
|
2940
|
+
function getRoleColor(role) {
|
|
2941
|
+
return roleColors[role] ?? chalk14.white;
|
|
2942
|
+
}
|
|
2943
|
+
function renderEvent(event, _config) {
|
|
2944
|
+
switch (event.type) {
|
|
2945
|
+
case "session.started":
|
|
2946
|
+
console.log(chalk14.gray(`
|
|
2947
|
+
\u2501\u2501\u2501 Session ${event.sessionId} \u2501\u2501\u2501`));
|
|
2948
|
+
console.log(chalk14.gray(`Workflow: ${event.workflow}`));
|
|
2949
|
+
console.log(chalk14.gray(`Task: ${event.task}
|
|
2950
|
+
`));
|
|
2951
|
+
break;
|
|
2952
|
+
case "session.completed":
|
|
2953
|
+
console.log(chalk14.green("\n\u2501\u2501\u2501 Session Complete \u2501\u2501\u2501"));
|
|
2954
|
+
console.log(chalk14.cyan(` Cost: $${event.totalCost.toFixed(4)}`));
|
|
2955
|
+
console.log(chalk14.cyan(` Tokens: ${event.totalTokens.toLocaleString()}`));
|
|
2956
|
+
console.log(chalk14.cyan(` Duration: ${(event.durationMs / 1e3).toFixed(1)}s`));
|
|
2957
|
+
break;
|
|
2958
|
+
case "session.failed":
|
|
2959
|
+
console.log(chalk14.red("\n\u2501\u2501\u2501 Session Failed \u2501\u2501\u2501"));
|
|
2960
|
+
console.log(chalk14.red(` Error: ${event.error}`));
|
|
2961
|
+
console.log(chalk14.red(` Last step: ${event.lastStep}`));
|
|
2962
|
+
break;
|
|
2963
|
+
case "step.started": {
|
|
2964
|
+
const color = getRoleColor(event.role);
|
|
2965
|
+
console.log(
|
|
2966
|
+
color(`
|
|
2967
|
+
\u25B6 [${event.role}] ${event.stepId} (${event.model}, iter ${event.iteration})`)
|
|
2968
|
+
);
|
|
2969
|
+
break;
|
|
2970
|
+
}
|
|
2971
|
+
case "step.completed":
|
|
2972
|
+
console.log(
|
|
2973
|
+
chalk14.gray(
|
|
2974
|
+
` \u2713 ${event.stepId} (${(event.durationMs / 1e3).toFixed(1)}s, ${event.tokenUsage.totalTokens} tokens)`
|
|
2975
|
+
)
|
|
2976
|
+
);
|
|
2977
|
+
break;
|
|
2978
|
+
case "step.failed":
|
|
2979
|
+
console.log(chalk14.red(` \u2717 ${event.stepId}: ${event.error}`));
|
|
2980
|
+
break;
|
|
2981
|
+
case "text.delta": {
|
|
2982
|
+
const deltaColor = getRoleColor(event.role);
|
|
2983
|
+
process.stdout.write(deltaColor(event.delta));
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
case "text.done":
|
|
2987
|
+
process.stdout.write("\n");
|
|
2988
|
+
break;
|
|
2989
|
+
case "loop.iteration": {
|
|
2990
|
+
const verdictColor = event.verdict === "approved" ? chalk14.green : chalk14.yellow;
|
|
2991
|
+
console.log(
|
|
2992
|
+
verdictColor(`
|
|
2993
|
+
\u21BB Loop ${event.iteration}/${event.maxIterations}: ${event.verdict}`)
|
|
2994
|
+
);
|
|
2995
|
+
if (event.feedback) {
|
|
2996
|
+
console.log(chalk14.gray(` Feedback: ${event.feedback.slice(0, 200)}...`));
|
|
2997
|
+
}
|
|
2998
|
+
break;
|
|
2999
|
+
}
|
|
3000
|
+
case "cost.update":
|
|
3001
|
+
console.log(
|
|
3002
|
+
chalk14.cyan(
|
|
3003
|
+
` $${event.costUsd.toFixed(4)} (cumulative: $${event.cumulativeSessionCost.toFixed(4)})`
|
|
3004
|
+
)
|
|
3005
|
+
);
|
|
3006
|
+
break;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
function printSessionSummary(result) {
|
|
3010
|
+
console.log(chalk14.bold("\nSession Summary"));
|
|
3011
|
+
console.log(chalk14.gray("-".repeat(40)));
|
|
3012
|
+
console.log(` Session: ${chalk14.white(result.sessionId)}`);
|
|
3013
|
+
console.log(
|
|
3014
|
+
` Status: ${result.status === "completed" ? chalk14.green("completed") : chalk14.red(result.status)}`
|
|
3015
|
+
);
|
|
3016
|
+
console.log(` Cost: ${chalk14.cyan(`$${result.totalCost.toFixed(4)}`)}`);
|
|
3017
|
+
console.log(` Tokens: ${chalk14.cyan(result.totalTokens.toLocaleString())}`);
|
|
3018
|
+
console.log(` Duration: ${chalk14.cyan(`${(result.durationMs / 1e3).toFixed(1)}s`)}`);
|
|
3019
|
+
console.log(` Iterations: ${chalk14.cyan(String(result.iterations))}`);
|
|
3020
|
+
console.log(chalk14.gray("-".repeat(40)));
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/commands/plan.ts
|
|
3024
|
+
async function planGenerateCommand(task, options) {
|
|
3025
|
+
let db;
|
|
3026
|
+
try {
|
|
3027
|
+
const config = loadConfig6();
|
|
3028
|
+
const projectDir = process.cwd();
|
|
3029
|
+
const registry = ModelRegistry4.fromConfig(config, projectDir);
|
|
3030
|
+
const dbPath = getDbPath();
|
|
3031
|
+
db = openDatabase9(dbPath);
|
|
3032
|
+
const orchestrator = new Orchestrator({ registry, db, config });
|
|
3033
|
+
orchestrator.on("event", (event) => renderEvent(event, config));
|
|
3034
|
+
const result = await orchestrator.plan(task, {
|
|
3035
|
+
maxRounds: options.rounds
|
|
3036
|
+
});
|
|
3037
|
+
if (options.output) {
|
|
3038
|
+
writeFileSync4(options.output, result.finalOutput, "utf-8");
|
|
3039
|
+
console.error(chalk15.green(`Plan saved to ${options.output}`));
|
|
3040
|
+
}
|
|
3041
|
+
printSessionSummary(result);
|
|
3042
|
+
db.close();
|
|
3043
|
+
process.exit(result.status === "completed" ? 0 : 2);
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
db?.close();
|
|
3046
|
+
console.error(chalk15.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
3047
|
+
process.exit(1);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
async function planReviewCommand(planFile, options) {
|
|
3051
|
+
let db;
|
|
3052
|
+
try {
|
|
3053
|
+
let planContent;
|
|
3054
|
+
if (planFile === "-") {
|
|
3055
|
+
const chunks = [];
|
|
3056
|
+
for await (const chunk of process.stdin) {
|
|
3057
|
+
chunks.push(chunk);
|
|
3058
|
+
}
|
|
3059
|
+
planContent = Buffer.concat(chunks).toString("utf-8");
|
|
3060
|
+
} else {
|
|
3061
|
+
planContent = readFileSync4(planFile, "utf-8");
|
|
3062
|
+
}
|
|
3063
|
+
if (!planContent.trim()) {
|
|
3064
|
+
console.error(chalk15.red("Plan file is empty."));
|
|
3065
|
+
process.exit(1);
|
|
3066
|
+
}
|
|
3067
|
+
const config = loadConfig6();
|
|
3068
|
+
const projectDir = process.cwd();
|
|
3069
|
+
const registry = ModelRegistry4.fromConfig(config, projectDir);
|
|
3070
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
3071
|
+
if (!adapter) {
|
|
3072
|
+
console.error(chalk15.red("No codex adapter found in config. Run: mulep init"));
|
|
3073
|
+
process.exit(1);
|
|
3074
|
+
}
|
|
3075
|
+
const dbPath = getDbPath();
|
|
3076
|
+
db = openDatabase9(dbPath);
|
|
3077
|
+
const sessionMgr = new SessionManager7(db);
|
|
3078
|
+
const session2 = sessionMgr.resolveActive("plan-review");
|
|
3079
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
3080
|
+
const threadId = currentSession?.codexThreadId ?? void 0;
|
|
3081
|
+
const phaseContext = options.phase ? `
|
|
3082
|
+
This is Phase ${options.phase} of a multi-phase plan.` : "";
|
|
3083
|
+
const buildContext = options.build ? `
|
|
3084
|
+
Build ID: ${options.build}` : "";
|
|
3085
|
+
const prompt = buildHandoffEnvelope4({
|
|
3086
|
+
command: "plan-review",
|
|
3087
|
+
task: `Review the following execution plan for completeness, correctness, and feasibility. Read relevant codebase files to verify the plan's assumptions.${phaseContext}${buildContext}
|
|
3088
|
+
|
|
3089
|
+
PLAN:
|
|
3090
|
+
${planContent.slice(0, 5e4)}
|
|
3091
|
+
|
|
3092
|
+
Review criteria:
|
|
3093
|
+
1. Are all files/functions mentioned actually present in the codebase?
|
|
3094
|
+
2. Are there missing steps or dependencies between phases?
|
|
3095
|
+
3. Are there architectural concerns or better approaches?
|
|
3096
|
+
4. Is the scope realistic for the described phases?
|
|
3097
|
+
5. Are there security or performance concerns?
|
|
3098
|
+
|
|
3099
|
+
Output format:
|
|
3100
|
+
- For each issue, output: ISSUE: [HIGH|MEDIUM|LOW] <description>
|
|
3101
|
+
- For each suggestion, output: SUGGEST: <description>
|
|
3102
|
+
- End with: VERDICT: APPROVED or VERDICT: NEEDS_REVISION
|
|
3103
|
+
- End with: SCORE: X/10`,
|
|
3104
|
+
constraints: [
|
|
3105
|
+
"Verify file paths and function names against the actual codebase before flagging issues.",
|
|
3106
|
+
"Be specific \u2014 reference exact files and line numbers when possible.",
|
|
3107
|
+
"Focus on feasibility, not style preferences."
|
|
3108
|
+
],
|
|
3109
|
+
resumed: Boolean(threadId)
|
|
3110
|
+
});
|
|
3111
|
+
const timeoutMs = (options.timeout ?? 300) * 1e3;
|
|
3112
|
+
const progress = createProgressCallbacks("plan-review");
|
|
3113
|
+
console.error(chalk15.cyan("Sending plan to codex for review..."));
|
|
3114
|
+
let result;
|
|
3115
|
+
try {
|
|
3116
|
+
result = await adapter.callWithResume(prompt, {
|
|
3117
|
+
sessionId: threadId,
|
|
3118
|
+
timeout: timeoutMs,
|
|
3119
|
+
...progress
|
|
3120
|
+
});
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
if (threadId) {
|
|
3123
|
+
console.error(chalk15.yellow(" Clearing stale codex thread ID after failure."));
|
|
3124
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
3125
|
+
}
|
|
3126
|
+
throw err;
|
|
3127
|
+
}
|
|
3128
|
+
if (threadId && result.sessionId !== threadId) {
|
|
3129
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
3130
|
+
}
|
|
3131
|
+
if (result.sessionId) {
|
|
3132
|
+
sessionMgr.updateThreadId(session2.id, result.sessionId);
|
|
3133
|
+
}
|
|
3134
|
+
sessionMgr.addUsageFromResult(session2.id, result.usage, prompt, result.text);
|
|
3135
|
+
sessionMgr.recordEvent({
|
|
3136
|
+
sessionId: session2.id,
|
|
3137
|
+
command: "plan",
|
|
3138
|
+
subcommand: "review",
|
|
3139
|
+
promptPreview: `Plan review: ${planFile}${options.phase ? ` (phase ${options.phase})` : ""}`,
|
|
3140
|
+
responsePreview: result.text.slice(0, 500),
|
|
3141
|
+
promptFull: prompt,
|
|
3142
|
+
responseFull: result.text,
|
|
3143
|
+
usageJson: JSON.stringify(result.usage),
|
|
3144
|
+
durationMs: result.durationMs,
|
|
3145
|
+
codexThreadId: result.sessionId
|
|
3146
|
+
});
|
|
3147
|
+
const tail = result.text.slice(-500);
|
|
3148
|
+
const verdictMatch = tail.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
3149
|
+
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
3150
|
+
const verdict = verdictMatch ? verdictMatch[1].toLowerCase() : "unknown";
|
|
3151
|
+
const score = scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null;
|
|
3152
|
+
const issues = [];
|
|
3153
|
+
const suggestions = [];
|
|
3154
|
+
for (const line of result.text.split("\n")) {
|
|
3155
|
+
const issueMatch = line.match(/^[-*]?\s*ISSUE:\s*\[(HIGH|MEDIUM|LOW)]\s*(.*)/i);
|
|
3156
|
+
if (issueMatch) {
|
|
3157
|
+
issues.push({ severity: issueMatch[1].toLowerCase(), message: issueMatch[2].trim() });
|
|
3158
|
+
}
|
|
3159
|
+
const suggestMatch = line.match(/^[-*]?\s*SUGGEST:\s*(.*)/i);
|
|
3160
|
+
if (suggestMatch) {
|
|
3161
|
+
suggestions.push(suggestMatch[1].trim());
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
const verdictColor = verdict === "approved" ? chalk15.green : chalk15.red;
|
|
3165
|
+
console.error(verdictColor(`
|
|
3166
|
+
Verdict: ${verdict.toUpperCase()} (${score ?? "?"}/10)`));
|
|
3167
|
+
if (issues.length > 0) {
|
|
3168
|
+
console.error(chalk15.yellow(`Issues (${issues.length}):`));
|
|
3169
|
+
for (const issue of issues) {
|
|
3170
|
+
const sevColor = issue.severity === "high" ? chalk15.red : issue.severity === "medium" ? chalk15.yellow : chalk15.dim;
|
|
3171
|
+
console.error(` ${sevColor(issue.severity.toUpperCase())} ${issue.message}`);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
if (suggestions.length > 0) {
|
|
3175
|
+
console.error(chalk15.cyan(`Suggestions (${suggestions.length}):`));
|
|
3176
|
+
for (const s of suggestions) {
|
|
3177
|
+
console.error(` ${chalk15.dim("\u2192")} ${s}`);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
console.error(chalk15.dim(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s | Tokens: ${result.usage.totalTokens}`));
|
|
3181
|
+
const output = {
|
|
3182
|
+
planFile,
|
|
3183
|
+
phase: options.phase ?? null,
|
|
3184
|
+
buildId: options.build ?? null,
|
|
3185
|
+
verdict,
|
|
3186
|
+
score,
|
|
3187
|
+
issues,
|
|
3188
|
+
suggestions,
|
|
3189
|
+
review: result.text.slice(0, 2e3),
|
|
3190
|
+
sessionId: result.sessionId,
|
|
3191
|
+
resumed: threadId ? result.sessionId === threadId : false,
|
|
3192
|
+
usage: result.usage,
|
|
3193
|
+
durationMs: result.durationMs
|
|
3194
|
+
};
|
|
3195
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3196
|
+
if (options.output) {
|
|
3197
|
+
writeFileSync4(options.output, JSON.stringify(output, null, 2), "utf-8");
|
|
3198
|
+
console.error(chalk15.green(`Review saved to ${options.output}`));
|
|
3199
|
+
}
|
|
3200
|
+
db.close();
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
db?.close();
|
|
3203
|
+
console.error(chalk15.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
3204
|
+
process.exit(1);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// src/commands/review.ts
|
|
3209
|
+
import { loadConfig as loadConfig7, ModelRegistry as ModelRegistry5, BINARY_SNIFF_BYTES, REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS3, SessionManager as SessionManager8, JobStore as JobStore3, openDatabase as openDatabase10, buildHandoffEnvelope as buildHandoffEnvelope5, getReviewPreset } from "@mulep/core";
|
|
3210
|
+
import chalk16 from "chalk";
|
|
3211
|
+
import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
|
|
3212
|
+
import { closeSync, globSync, openSync, readFileSync as readFileSync5, readSync, statSync, existsSync as existsSync4 } from "fs";
|
|
3213
|
+
import { resolve as resolve2 } from "path";
|
|
3214
|
+
var MAX_FILE_SIZE = 100 * 1024;
|
|
3215
|
+
var MAX_TOTAL_SIZE = 200 * 1024;
|
|
3216
|
+
async function reviewCommand(fileOrGlob, options) {
|
|
3217
|
+
try {
|
|
3218
|
+
const projectDir = process.cwd();
|
|
3219
|
+
const modes = [
|
|
3220
|
+
fileOrGlob ? "file" : "",
|
|
3221
|
+
options.prompt ? "prompt" : "",
|
|
3222
|
+
options.stdin ? "stdin" : "",
|
|
3223
|
+
options.diff ? "diff" : ""
|
|
3224
|
+
].filter(Boolean);
|
|
3225
|
+
if (modes.length === 0) {
|
|
3226
|
+
console.error(chalk16.red("No input specified. Use: <file-or-glob>, --prompt, --stdin, or --diff"));
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
if (modes.length > 1) {
|
|
3230
|
+
console.error(chalk16.red(`Conflicting input modes: ${modes.join(", ")}. Use exactly one.`));
|
|
3231
|
+
process.exit(1);
|
|
3232
|
+
}
|
|
3233
|
+
if (options.scope && !options.prompt && !options.stdin) {
|
|
3234
|
+
console.error(chalk16.red("--scope can only be used with --prompt or --stdin"));
|
|
3235
|
+
process.exit(1);
|
|
3236
|
+
}
|
|
3237
|
+
const config = loadConfig7();
|
|
3238
|
+
const registry = ModelRegistry5.fromConfig(config, projectDir);
|
|
3239
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
3240
|
+
if (!adapter) {
|
|
3241
|
+
try {
|
|
3242
|
+
execSync4("codex --version", { stdio: "pipe", encoding: "utf-8" });
|
|
3243
|
+
} catch {
|
|
3244
|
+
console.error(chalk16.red("Codex CLI is not installed or not in PATH."));
|
|
3245
|
+
console.error(chalk16.yellow("Install it: npm install -g @openai/codex"));
|
|
3246
|
+
console.error(chalk16.yellow("Then run: mulep init"));
|
|
3247
|
+
process.exit(1);
|
|
3248
|
+
}
|
|
3249
|
+
console.error(chalk16.red("No codex adapter found in config. Run: mulep init"));
|
|
3250
|
+
console.error(chalk16.dim("Diagnose: mulep doctor"));
|
|
3251
|
+
process.exit(1);
|
|
3252
|
+
}
|
|
3253
|
+
if (options.background) {
|
|
3254
|
+
const db2 = openDatabase10(getDbPath());
|
|
3255
|
+
const jobStore = new JobStore3(db2);
|
|
3256
|
+
const jobId = jobStore.enqueue({
|
|
3257
|
+
type: "review",
|
|
3258
|
+
payload: {
|
|
3259
|
+
fileOrGlob: fileOrGlob ?? null,
|
|
3260
|
+
focus: options.focus,
|
|
3261
|
+
timeout: options.timeout,
|
|
3262
|
+
prompt: options.prompt,
|
|
3263
|
+
stdin: options.stdin,
|
|
3264
|
+
diff: options.diff,
|
|
3265
|
+
scope: options.scope,
|
|
3266
|
+
cwd: projectDir
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
3269
|
+
console.log(JSON.stringify({ jobId, status: "queued", message: "Review enqueued. Check with: mulep jobs status " + jobId }));
|
|
3270
|
+
db2.close();
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
const db = openDatabase10(getDbPath());
|
|
3274
|
+
const sessionMgr = new SessionManager8(db);
|
|
3275
|
+
const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("review");
|
|
3276
|
+
if (!session2) {
|
|
3277
|
+
console.error(chalk16.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: mulep init"));
|
|
3278
|
+
db.close();
|
|
3279
|
+
process.exit(1);
|
|
3280
|
+
}
|
|
3281
|
+
const preset = options.preset ? getReviewPreset(options.preset) : void 0;
|
|
3282
|
+
if (options.preset && !preset) {
|
|
3283
|
+
console.error(chalk16.red(`Unknown preset: ${options.preset}. Use: security-audit, performance, quick-scan, pre-commit, api-review`));
|
|
3284
|
+
db.close();
|
|
3285
|
+
process.exit(1);
|
|
3286
|
+
}
|
|
3287
|
+
const focusArea = preset?.focus ?? options.focus ?? "all";
|
|
3288
|
+
const focusConstraint = focusArea === "all" ? "Review for: correctness, bugs, security, performance, code quality" : `Focus specifically on: ${focusArea}`;
|
|
3289
|
+
const presetConstraints = preset?.constraints ?? [];
|
|
3290
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
3291
|
+
const sessionThreadId = currentSession?.codexThreadId ?? void 0;
|
|
3292
|
+
const isResumed = Boolean(sessionThreadId);
|
|
3293
|
+
let prompt;
|
|
3294
|
+
let promptPreview;
|
|
3295
|
+
const mode = modes[0];
|
|
3296
|
+
if (mode === "prompt" || mode === "stdin") {
|
|
3297
|
+
let instruction = options.prompt ?? "";
|
|
3298
|
+
if (mode === "stdin") {
|
|
3299
|
+
const chunks = [];
|
|
3300
|
+
for await (const chunk of process.stdin) {
|
|
3301
|
+
chunks.push(chunk);
|
|
3302
|
+
}
|
|
3303
|
+
instruction = Buffer.concat(chunks).toString("utf-8").trim();
|
|
3304
|
+
if (!instruction) {
|
|
3305
|
+
console.error(chalk16.red("No input received from stdin"));
|
|
3306
|
+
db.close();
|
|
3307
|
+
process.exit(1);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
prompt = buildHandoffEnvelope5({
|
|
3311
|
+
command: "review",
|
|
3312
|
+
task: `TASK: ${instruction}
|
|
3313
|
+
|
|
3314
|
+
Start by listing candidate files, then inspect them thoroughly.`,
|
|
3315
|
+
constraints: [focusConstraint, ...presetConstraints],
|
|
3316
|
+
scope: options.scope,
|
|
3317
|
+
resumed: isResumed
|
|
3318
|
+
});
|
|
3319
|
+
promptPreview = `Prompt review: ${instruction.slice(0, 100)}`;
|
|
3320
|
+
console.error(chalk16.cyan(`Reviewing via prompt (session: ${session2.id.slice(0, 8)}...)...`));
|
|
3321
|
+
} else if (mode === "diff") {
|
|
3322
|
+
let diff;
|
|
3323
|
+
try {
|
|
3324
|
+
diff = execFileSync3("git", ["diff", "--", ...options.diff.split(/\s+/)], {
|
|
3325
|
+
cwd: projectDir,
|
|
3326
|
+
encoding: "utf-8",
|
|
3327
|
+
maxBuffer: 1024 * 1024
|
|
3328
|
+
});
|
|
3329
|
+
} catch (err) {
|
|
3330
|
+
console.error(chalk16.red(`Failed to get diff for ${options.diff}: ${err instanceof Error ? err.message : String(err)}`));
|
|
3331
|
+
db.close();
|
|
3332
|
+
process.exit(1);
|
|
3333
|
+
}
|
|
3334
|
+
if (!diff.trim()) {
|
|
3335
|
+
console.error(chalk16.yellow(`No changes in diff: ${options.diff}`));
|
|
3336
|
+
db.close();
|
|
3337
|
+
process.exit(0);
|
|
3338
|
+
}
|
|
3339
|
+
prompt = buildHandoffEnvelope5({
|
|
3340
|
+
command: "review",
|
|
3341
|
+
task: `Review the following code changes.
|
|
3342
|
+
|
|
3343
|
+
GIT DIFF (${options.diff}):
|
|
3344
|
+
${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
|
|
3345
|
+
constraints: [focusConstraint, ...presetConstraints],
|
|
3346
|
+
resumed: isResumed
|
|
3347
|
+
});
|
|
3348
|
+
promptPreview = `Diff review: ${options.diff}`;
|
|
3349
|
+
console.error(chalk16.cyan(`Reviewing diff ${options.diff} (session: ${session2.id.slice(0, 8)}...)...`));
|
|
3350
|
+
} else {
|
|
3351
|
+
let globPattern = fileOrGlob;
|
|
3352
|
+
const resolvedInput = resolve2(projectDir, globPattern);
|
|
3353
|
+
if (existsSync4(resolvedInput) && statSync(resolvedInput).isDirectory()) {
|
|
3354
|
+
globPattern = `${globPattern}/**/*`;
|
|
3355
|
+
console.error(chalk16.dim(` Expanding directory to: ${globPattern}`));
|
|
3356
|
+
}
|
|
3357
|
+
const projectRoot = resolve2(projectDir) + (process.platform === "win32" ? "\\" : "/");
|
|
3358
|
+
const paths = globSync(globPattern, { cwd: projectDir }).map((p) => resolve2(projectDir, p)).filter((p) => p.startsWith(projectRoot) || p === resolve2(projectDir));
|
|
3359
|
+
if (paths.length === 0) {
|
|
3360
|
+
console.error(chalk16.red(`No files matched: ${fileOrGlob}`));
|
|
3361
|
+
db.close();
|
|
3362
|
+
process.exit(1);
|
|
3363
|
+
}
|
|
3364
|
+
const files = [];
|
|
3365
|
+
let totalSize = 0;
|
|
3366
|
+
for (const filePath of paths) {
|
|
3367
|
+
const stat = statSync(filePath);
|
|
3368
|
+
if (!stat.isFile()) continue;
|
|
3369
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
3370
|
+
console.error(chalk16.yellow(`Skipping ${filePath} (${(stat.size / 1024).toFixed(0)}KB > 100KB limit)`));
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
if (totalSize + stat.size > MAX_TOTAL_SIZE) {
|
|
3374
|
+
console.error(chalk16.yellow(`Skipping remaining files (total would exceed 200KB)`));
|
|
3375
|
+
break;
|
|
3376
|
+
}
|
|
3377
|
+
const buf = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
3378
|
+
const fd = openSync(filePath, "r");
|
|
3379
|
+
const bytesRead = readSync(fd, buf, 0, BINARY_SNIFF_BYTES, 0);
|
|
3380
|
+
closeSync(fd);
|
|
3381
|
+
if (buf.subarray(0, bytesRead).includes(0)) {
|
|
3382
|
+
console.error(chalk16.yellow(`Skipping ${filePath} (binary file)`));
|
|
3383
|
+
continue;
|
|
3384
|
+
}
|
|
3385
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3386
|
+
const relativePath = filePath.replace(projectDir, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
3387
|
+
files.push({ path: relativePath, content });
|
|
3388
|
+
totalSize += stat.size;
|
|
3389
|
+
}
|
|
3390
|
+
if (files.length === 0) {
|
|
3391
|
+
console.error(chalk16.red("No readable files to review"));
|
|
3392
|
+
db.close();
|
|
3393
|
+
process.exit(1);
|
|
3394
|
+
}
|
|
3395
|
+
const fileContents = files.map((f) => `--- ${f.path} ---
|
|
3396
|
+
${f.content}`).join("\n\n");
|
|
3397
|
+
prompt = buildHandoffEnvelope5({
|
|
3398
|
+
command: "review",
|
|
3399
|
+
task: `Review the following code files.
|
|
3400
|
+
|
|
3401
|
+
FILES TO REVIEW:
|
|
3402
|
+
${fileContents}`,
|
|
3403
|
+
constraints: [focusConstraint, ...presetConstraints],
|
|
3404
|
+
resumed: isResumed
|
|
3405
|
+
});
|
|
3406
|
+
promptPreview = `Review ${files.length} file(s): ${files.map((f) => f.path).join(", ")}`;
|
|
3407
|
+
console.error(chalk16.cyan(`Reviewing ${files.length} file(s) via codex (session: ${session2.id.slice(0, 8)}...)...`));
|
|
3408
|
+
}
|
|
3409
|
+
const timeoutMs = (options.timeout ?? 600) * 1e3;
|
|
3410
|
+
const progress = createProgressCallbacks("review");
|
|
3411
|
+
let result;
|
|
3412
|
+
try {
|
|
3413
|
+
result = await adapter.callWithResume(prompt, {
|
|
3414
|
+
sessionId: sessionThreadId,
|
|
3415
|
+
timeout: timeoutMs,
|
|
3416
|
+
...progress
|
|
3417
|
+
});
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
if (sessionThreadId) {
|
|
3420
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
3421
|
+
}
|
|
3422
|
+
throw err;
|
|
3423
|
+
}
|
|
3424
|
+
if (sessionThreadId && result.sessionId !== sessionThreadId) {
|
|
3425
|
+
sessionMgr.updateThreadId(session2.id, null);
|
|
3426
|
+
}
|
|
3427
|
+
if (result.sessionId) {
|
|
3428
|
+
sessionMgr.updateThreadId(session2.id, result.sessionId);
|
|
3429
|
+
}
|
|
3430
|
+
sessionMgr.addUsageFromResult(session2.id, result.usage, prompt, result.text);
|
|
3431
|
+
sessionMgr.recordEvent({
|
|
3432
|
+
sessionId: session2.id,
|
|
3433
|
+
command: "review",
|
|
3434
|
+
subcommand: mode,
|
|
3435
|
+
promptPreview: promptPreview.slice(0, 500),
|
|
3436
|
+
responsePreview: result.text.slice(0, 500),
|
|
3437
|
+
promptFull: prompt,
|
|
3438
|
+
responseFull: result.text,
|
|
3439
|
+
usageJson: JSON.stringify(result.usage),
|
|
3440
|
+
durationMs: result.durationMs,
|
|
3441
|
+
codexThreadId: result.sessionId
|
|
3442
|
+
});
|
|
3443
|
+
const findings = [];
|
|
3444
|
+
for (const line of result.text.split("\n")) {
|
|
3445
|
+
const match = line.match(/^-\s*(CRITICAL|WARNING|INFO):\s*(\S+?)(?::(\d+))?\s+(.+)/);
|
|
3446
|
+
if (match) {
|
|
3447
|
+
findings.push({
|
|
3448
|
+
severity: match[1].toLowerCase(),
|
|
3449
|
+
file: match[2],
|
|
3450
|
+
line: match[3] ?? "?",
|
|
3451
|
+
message: match[4]
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
const tail = result.text.slice(-500);
|
|
3456
|
+
const verdictMatch = tail.match(/^(?:-\s*)?VERDICT:\s*(APPROVED|NEEDS_REVISION)/m);
|
|
3457
|
+
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
3458
|
+
const output = {
|
|
3459
|
+
mode,
|
|
3460
|
+
findings,
|
|
3461
|
+
verdict: verdictMatch ? verdictMatch[1].toLowerCase() : "unknown",
|
|
3462
|
+
score: scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null,
|
|
3463
|
+
review: result.text.slice(0, 2e3),
|
|
3464
|
+
sessionId: session2.id,
|
|
3465
|
+
codexThreadId: result.sessionId,
|
|
3466
|
+
resumed: sessionThreadId ? result.sessionId === sessionThreadId : false,
|
|
3467
|
+
usage: result.usage,
|
|
3468
|
+
durationMs: result.durationMs
|
|
3469
|
+
};
|
|
3470
|
+
const verdictColor = output.verdict === "approved" ? chalk16.green : chalk16.red;
|
|
3471
|
+
console.error(verdictColor(`
|
|
3472
|
+
Verdict: ${output.verdict.toUpperCase()} (${output.score ?? "?"}/10)`));
|
|
3473
|
+
if (findings.length > 0) {
|
|
3474
|
+
console.error(chalk16.yellow(`Findings (${findings.length}):`));
|
|
3475
|
+
for (const f of findings) {
|
|
3476
|
+
const sev = f.severity === "critical" ? chalk16.red("CRITICAL") : f.severity === "warning" ? chalk16.yellow("WARNING") : chalk16.dim("INFO");
|
|
3477
|
+
console.error(` ${sev} ${f.file}:${f.line} \u2014 ${f.message}`);
|
|
3478
|
+
}
|
|
3479
|
+
} else {
|
|
3480
|
+
console.error(chalk16.green("No issues found."));
|
|
3481
|
+
}
|
|
3482
|
+
console.error(chalk16.dim(`Duration: ${(output.durationMs / 1e3).toFixed(1)}s | Tokens: ${output.usage?.totalTokens ?? "?"}`));
|
|
3483
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3484
|
+
db.close();
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
console.error(chalk16.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
3487
|
+
process.exit(1);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// src/commands/run.ts
|
|
3492
|
+
import { ModelRegistry as ModelRegistry6, Orchestrator as Orchestrator2, loadConfig as loadConfig8, openDatabase as openDatabase11 } from "@mulep/core";
|
|
3493
|
+
import chalk17 from "chalk";
|
|
3494
|
+
async function runCommand(task, options) {
|
|
3495
|
+
try {
|
|
3496
|
+
const config = loadConfig8();
|
|
3497
|
+
const projectDir = process.cwd();
|
|
3498
|
+
const registry = ModelRegistry6.fromConfig(config, projectDir);
|
|
3499
|
+
const health = await registry.healthCheckAll();
|
|
3500
|
+
for (const [alias, hasKey] of health) {
|
|
3501
|
+
if (!hasKey) {
|
|
3502
|
+
console.warn(chalk17.yellow(`Warning: No API key for model "${alias}"`));
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
const dbPath = getDbPath();
|
|
3506
|
+
const db = openDatabase11(dbPath);
|
|
3507
|
+
const orchestrator = new Orchestrator2({ registry, db, config });
|
|
3508
|
+
orchestrator.on("event", (event) => renderEvent(event, config));
|
|
3509
|
+
const result = await orchestrator.run(task, {
|
|
3510
|
+
mode: options.mode ?? config.mode,
|
|
3511
|
+
maxIterations: options.maxIterations,
|
|
3512
|
+
stream: options.stream
|
|
3513
|
+
});
|
|
3514
|
+
printSessionSummary(result);
|
|
3515
|
+
db.close();
|
|
3516
|
+
process.exit(result.status === "completed" ? 0 : 2);
|
|
3517
|
+
} catch (error) {
|
|
3518
|
+
console.error(chalk17.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
3519
|
+
process.exit(1);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// src/commands/shipit.ts
|
|
3524
|
+
import { execSync as execSync5 } from "child_process";
|
|
3525
|
+
import { DEFAULT_RULES as DEFAULT_RULES2, evaluatePolicy as evaluatePolicy2 } from "@mulep/core";
|
|
3526
|
+
import chalk18 from "chalk";
|
|
3527
|
+
var PROFILES = {
|
|
3528
|
+
fast: ["review"],
|
|
3529
|
+
safe: ["lint", "test", "review", "cleanup"],
|
|
3530
|
+
full: ["lint", "test", "review", "cleanup", "commit"]
|
|
3531
|
+
};
|
|
3532
|
+
function runStep(name, dryRun) {
|
|
3533
|
+
const start = Date.now();
|
|
3534
|
+
if (dryRun) {
|
|
3535
|
+
return { name, status: "skipped", output: "dry-run", durationMs: 0 };
|
|
3536
|
+
}
|
|
3537
|
+
try {
|
|
3538
|
+
let cmd;
|
|
3539
|
+
switch (name) {
|
|
3540
|
+
case "lint":
|
|
3541
|
+
cmd = "npx biome check .";
|
|
3542
|
+
break;
|
|
3543
|
+
case "test":
|
|
3544
|
+
cmd = "pnpm run test";
|
|
3545
|
+
break;
|
|
3546
|
+
case "review":
|
|
3547
|
+
cmd = "mulep review --preset pre-commit --diff HEAD";
|
|
3548
|
+
break;
|
|
3549
|
+
case "cleanup":
|
|
3550
|
+
cmd = "mulep cleanup --scope deps";
|
|
3551
|
+
break;
|
|
3552
|
+
case "commit":
|
|
3553
|
+
return { name, status: "skipped", output: "handled by shipit", durationMs: 0 };
|
|
3554
|
+
default:
|
|
3555
|
+
return { name, status: "skipped", output: `unknown step: ${name}`, durationMs: 0 };
|
|
3556
|
+
}
|
|
3557
|
+
const output = execSync5(cmd, {
|
|
3558
|
+
encoding: "utf-8",
|
|
3559
|
+
timeout: 3e5,
|
|
3560
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3561
|
+
});
|
|
3562
|
+
return {
|
|
3563
|
+
name,
|
|
3564
|
+
status: "passed",
|
|
3565
|
+
output: output.slice(0, 2e3),
|
|
3566
|
+
durationMs: Date.now() - start
|
|
3567
|
+
};
|
|
3568
|
+
} catch (err) {
|
|
3569
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3570
|
+
return { name, status: "failed", output: msg.slice(0, 2e3), durationMs: Date.now() - start };
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
async function shipitCommand(options) {
|
|
3574
|
+
const profile = options.profile;
|
|
3575
|
+
const steps = PROFILES[profile];
|
|
3576
|
+
if (!steps) {
|
|
3577
|
+
console.error(chalk18.red(`Unknown profile: ${profile}. Use: fast, safe, full`));
|
|
3578
|
+
process.exit(1);
|
|
3579
|
+
}
|
|
3580
|
+
if (options.dryRun) {
|
|
3581
|
+
console.error(chalk18.cyan(`Shipit dry-run (profile: ${profile})`));
|
|
3582
|
+
console.error(chalk18.dim(`Steps: ${steps.join(" \u2192 ")}`));
|
|
3583
|
+
} else {
|
|
3584
|
+
console.error(chalk18.cyan(`Shipit (profile: ${profile}): ${steps.join(" \u2192 ")}`));
|
|
3585
|
+
}
|
|
3586
|
+
const results = [];
|
|
3587
|
+
let shouldStop = false;
|
|
3588
|
+
for (const step of steps) {
|
|
3589
|
+
if (shouldStop) {
|
|
3590
|
+
results.push({ name: step, status: "skipped", durationMs: 0 });
|
|
3591
|
+
continue;
|
|
3592
|
+
}
|
|
3593
|
+
const result = runStep(step, options.dryRun);
|
|
3594
|
+
results.push(result);
|
|
3595
|
+
if (!options.dryRun) {
|
|
3596
|
+
const icon = result.status === "passed" ? chalk18.green("OK") : result.status === "failed" ? chalk18.red("FAIL") : chalk18.dim("SKIP");
|
|
3597
|
+
console.error(` ${icon} ${result.name} (${result.durationMs}ms)`);
|
|
3598
|
+
}
|
|
3599
|
+
if (result.status === "failed" && (step === "lint" || step === "test")) {
|
|
3600
|
+
shouldStop = true;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
const reviewResult = results.find((r) => r.name === "review");
|
|
3604
|
+
const criticalCount = reviewResult?.output?.match(/CRITICAL/gi)?.length ?? 0;
|
|
3605
|
+
const warningCount = reviewResult?.output?.match(/WARNING/gi)?.length ?? 0;
|
|
3606
|
+
const verdictMatch = reviewResult?.output?.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
3607
|
+
const policyCtx = {
|
|
3608
|
+
criticalCount,
|
|
3609
|
+
warningCount,
|
|
3610
|
+
verdict: verdictMatch ? verdictMatch[1].toLowerCase() : "unknown",
|
|
3611
|
+
stepsCompleted: Object.fromEntries(results.map((r) => [r.name, r.status])),
|
|
3612
|
+
cleanupHighCount: 0
|
|
3613
|
+
};
|
|
3614
|
+
const policyResult = evaluatePolicy2("review.completed", policyCtx, DEFAULT_RULES2);
|
|
3615
|
+
if (policyResult.decision === "block") {
|
|
3616
|
+
console.error(chalk18.red("Policy BLOCKED:"));
|
|
3617
|
+
for (const v of policyResult.violations) {
|
|
3618
|
+
console.error(chalk18.red(` - ${v.message}`));
|
|
3619
|
+
}
|
|
3620
|
+
} else if (policyResult.decision === "warn") {
|
|
3621
|
+
for (const v of policyResult.violations) {
|
|
3622
|
+
console.error(chalk18.yellow(` Warning: ${v.message}`));
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
const output = {
|
|
3626
|
+
profile,
|
|
3627
|
+
steps: results,
|
|
3628
|
+
policy: policyResult,
|
|
3629
|
+
canCommit: policyResult.decision !== "block" && !shouldStop && !options.noCommit
|
|
3630
|
+
};
|
|
3631
|
+
if (options.json) {
|
|
3632
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3633
|
+
} else {
|
|
3634
|
+
const allPassed = results.every((r) => r.status === "passed" || r.status === "skipped");
|
|
3635
|
+
if (allPassed && policyResult.decision !== "block") {
|
|
3636
|
+
console.error(chalk18.green("\nAll checks passed. Ready to commit."));
|
|
3637
|
+
} else {
|
|
3638
|
+
console.error(chalk18.red("\nSome checks failed or policy blocked."));
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// src/commands/start.ts
|
|
3644
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3645
|
+
import { execFileSync as execFileSync4, execSync as execSync6 } from "child_process";
|
|
3646
|
+
import { join as join7, basename as basename2 } from "path";
|
|
3647
|
+
import chalk19 from "chalk";
|
|
3648
|
+
import { loadConfig as loadConfig9, writeConfig as writeConfig2 } from "@mulep/core";
|
|
3649
|
+
async function startCommand() {
|
|
3650
|
+
const cwd = process.cwd();
|
|
3651
|
+
console.error(chalk19.cyan("\n MuleP \u2014 First Run Setup\n"));
|
|
3652
|
+
console.error(chalk19.dim(" [1/4] Checking Codex CLI..."));
|
|
3653
|
+
let codexVersion = null;
|
|
3654
|
+
try {
|
|
3655
|
+
codexVersion = execSync6("codex --version", { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
3656
|
+
} catch {
|
|
3657
|
+
}
|
|
3658
|
+
if (!codexVersion) {
|
|
3659
|
+
console.error(chalk19.red(" Codex CLI is not installed."));
|
|
3660
|
+
console.error(chalk19.yellow(" Install it: npm install -g @openai/codex"));
|
|
3661
|
+
console.error(chalk19.yellow(" Then run: mulep start"));
|
|
3662
|
+
process.exit(1);
|
|
3663
|
+
}
|
|
3664
|
+
console.error(chalk19.green(` Codex CLI ${codexVersion} found.`));
|
|
3665
|
+
console.error(chalk19.dim(" [2/4] Checking project config..."));
|
|
3666
|
+
const configPath = join7(cwd, ".mulep.yml");
|
|
3667
|
+
if (existsSync5(configPath)) {
|
|
3668
|
+
console.error(chalk19.green(" .mulep.yml exists \u2014 using it."));
|
|
3669
|
+
} else {
|
|
3670
|
+
const config = loadConfig9({ preset: "cli-first", skipFile: true });
|
|
3671
|
+
config.project.name = basename2(cwd);
|
|
3672
|
+
writeConfig2(config, cwd);
|
|
3673
|
+
console.error(chalk19.green(" Created .mulep.yml with cli-first preset."));
|
|
3674
|
+
}
|
|
3675
|
+
console.error(chalk19.dim(" [3/4] Detecting project..."));
|
|
3676
|
+
const hasGit = existsSync5(join7(cwd, ".git"));
|
|
3677
|
+
const hasSrc = existsSync5(join7(cwd, "src"));
|
|
3678
|
+
const hasPackageJson = existsSync5(join7(cwd, "package.json"));
|
|
3679
|
+
let reviewTarget = "";
|
|
3680
|
+
if (hasGit) {
|
|
3681
|
+
try {
|
|
3682
|
+
const diff = execSync6("git diff --name-only HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
3683
|
+
if (diff) {
|
|
3684
|
+
const files = diff.split("\n").filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".tsx") || f.endsWith(".jsx") || f.endsWith(".py"));
|
|
3685
|
+
if (files.length > 0) {
|
|
3686
|
+
reviewTarget = files.slice(0, 10).join(" ");
|
|
3687
|
+
console.error(chalk19.green(` Found ${files.length} changed file(s) \u2014 reviewing those.`));
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
} catch {
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
if (!reviewTarget) {
|
|
3694
|
+
if (hasSrc) {
|
|
3695
|
+
reviewTarget = "src/";
|
|
3696
|
+
console.error(chalk19.green(" Found src/ directory \u2014 reviewing it."));
|
|
3697
|
+
} else if (hasPackageJson) {
|
|
3698
|
+
reviewTarget = "**/*.ts";
|
|
3699
|
+
console.error(chalk19.green(" TypeScript project \u2014 reviewing *.ts files."));
|
|
3700
|
+
} else {
|
|
3701
|
+
console.error(chalk19.yellow(" No src/ or package.json found. Try: mulep review <path>"));
|
|
3702
|
+
process.exit(0);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
console.error(chalk19.dim(" [4/4] Running quick review..."));
|
|
3706
|
+
console.error(chalk19.cyan(`
|
|
3707
|
+
mulep review ${reviewTarget} --preset quick-scan
|
|
3708
|
+
`));
|
|
3709
|
+
try {
|
|
3710
|
+
const output = execFileSync4("mulep", ["review", reviewTarget, "--preset", "quick-scan"], {
|
|
3711
|
+
cwd,
|
|
3712
|
+
encoding: "utf-8",
|
|
3713
|
+
timeout: 3e5,
|
|
3714
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
3715
|
+
shell: process.platform === "win32"
|
|
3716
|
+
});
|
|
3717
|
+
try {
|
|
3718
|
+
const result = JSON.parse(output);
|
|
3719
|
+
const findingCount = result.findings?.length ?? 0;
|
|
3720
|
+
const verdict = result.verdict ?? "unknown";
|
|
3721
|
+
const score = result.score;
|
|
3722
|
+
console.error("");
|
|
3723
|
+
if (findingCount > 0) {
|
|
3724
|
+
console.error(chalk19.yellow(` Found ${findingCount} issue(s). Score: ${score ?? "?"}/10`));
|
|
3725
|
+
console.error("");
|
|
3726
|
+
console.error(chalk19.cyan(" Next steps:"));
|
|
3727
|
+
console.error(chalk19.dim(` mulep fix ${reviewTarget} --dry-run # preview fixes`));
|
|
3728
|
+
console.error(chalk19.dim(` mulep fix ${reviewTarget} # apply fixes`));
|
|
3729
|
+
console.error(chalk19.dim(" mulep review --preset security-audit # deeper scan"));
|
|
3730
|
+
} else if (verdict === "approved") {
|
|
3731
|
+
console.error(chalk19.green(` Code looks good! Score: ${score ?? "?"}/10`));
|
|
3732
|
+
console.error("");
|
|
3733
|
+
console.error(chalk19.cyan(" Next steps:"));
|
|
3734
|
+
console.error(chalk19.dim(" mulep review --preset security-audit # security scan"));
|
|
3735
|
+
console.error(chalk19.dim(' mulep debate start "your question" # debate with GPT'));
|
|
3736
|
+
console.error(chalk19.dim(" mulep watch # watch for changes"));
|
|
3737
|
+
} else {
|
|
3738
|
+
console.error(chalk19.dim(` Review complete. Verdict: ${verdict}, Score: ${score ?? "?"}/10`));
|
|
3739
|
+
}
|
|
3740
|
+
} catch {
|
|
3741
|
+
console.log(output);
|
|
3742
|
+
}
|
|
3743
|
+
} catch (err) {
|
|
3744
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3745
|
+
if (msg.includes("ETIMEDOUT") || msg.includes("timeout")) {
|
|
3746
|
+
console.error(chalk19.yellow(" Review timed out. Try: mulep review --preset quick-scan"));
|
|
3747
|
+
} else {
|
|
3748
|
+
console.error(chalk19.red(` Review failed: ${msg.slice(0, 200)}`));
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
console.error(chalk19.dim(" Tip: Run mulep install-skills to add /debate, /build, /cleanup"));
|
|
3752
|
+
console.error(chalk19.dim(" slash commands to Claude Code in this project."));
|
|
3753
|
+
console.error("");
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
// src/commands/watch.ts
|
|
3757
|
+
import { JobStore as JobStore4, openDatabase as openDatabase12 } from "@mulep/core";
|
|
3758
|
+
import chalk20 from "chalk";
|
|
3759
|
+
import { watch } from "chokidar";
|
|
3760
|
+
|
|
3761
|
+
// src/watch/debouncer.ts
|
|
3762
|
+
var DEFAULT_CONFIG = {
|
|
3763
|
+
quietMs: 800,
|
|
3764
|
+
maxWaitMs: 5e3,
|
|
3765
|
+
cooldownMs: 1500,
|
|
3766
|
+
maxBatchSize: 50
|
|
3767
|
+
};
|
|
3768
|
+
var batchCounter = 0;
|
|
3769
|
+
var Debouncer = class {
|
|
3770
|
+
config;
|
|
3771
|
+
pending = /* @__PURE__ */ new Map();
|
|
3772
|
+
quietTimer = null;
|
|
3773
|
+
maxWaitTimer = null;
|
|
3774
|
+
cooldownUntil = 0;
|
|
3775
|
+
windowStart = 0;
|
|
3776
|
+
onFlush;
|
|
3777
|
+
destroyed = false;
|
|
3778
|
+
constructor(onFlush, config) {
|
|
3779
|
+
this.onFlush = onFlush;
|
|
3780
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3781
|
+
}
|
|
3782
|
+
push(e) {
|
|
3783
|
+
if (this.destroyed) return false;
|
|
3784
|
+
if (e.ts < this.cooldownUntil) return false;
|
|
3785
|
+
if (this.pending.size === 0) {
|
|
3786
|
+
this.windowStart = e.ts;
|
|
3787
|
+
this.startMaxWaitTimer();
|
|
3788
|
+
}
|
|
3789
|
+
this.pending.set(e.path, e);
|
|
3790
|
+
this.restartQuietTimer();
|
|
3791
|
+
if (this.pending.size >= this.config.maxBatchSize) {
|
|
3792
|
+
this.flush("maxBatch");
|
|
3793
|
+
}
|
|
3794
|
+
return true;
|
|
3795
|
+
}
|
|
3796
|
+
flushNow() {
|
|
3797
|
+
this.flush("manual");
|
|
3798
|
+
}
|
|
3799
|
+
cancel() {
|
|
3800
|
+
this.clearTimers();
|
|
3801
|
+
this.pending.clear();
|
|
3802
|
+
}
|
|
3803
|
+
destroy() {
|
|
3804
|
+
this.cancel();
|
|
3805
|
+
this.destroyed = true;
|
|
3806
|
+
}
|
|
3807
|
+
getPendingCount() {
|
|
3808
|
+
return this.pending.size;
|
|
3809
|
+
}
|
|
3810
|
+
flush(reason) {
|
|
3811
|
+
if (this.pending.size === 0) return;
|
|
3812
|
+
this.clearTimers();
|
|
3813
|
+
const now = Date.now();
|
|
3814
|
+
const batch = {
|
|
3815
|
+
files: [...this.pending.keys()],
|
|
3816
|
+
batchId: `wb-${++batchCounter}`,
|
|
3817
|
+
windowStart: this.windowStart,
|
|
3818
|
+
windowEnd: now,
|
|
3819
|
+
reason
|
|
3820
|
+
};
|
|
3821
|
+
this.pending.clear();
|
|
3822
|
+
this.cooldownUntil = now + this.config.cooldownMs;
|
|
3823
|
+
this.onFlush(batch);
|
|
3824
|
+
}
|
|
3825
|
+
restartQuietTimer() {
|
|
3826
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
3827
|
+
this.quietTimer = setTimeout(() => this.flush("quiet"), this.config.quietMs);
|
|
3828
|
+
}
|
|
3829
|
+
startMaxWaitTimer() {
|
|
3830
|
+
if (this.maxWaitTimer) return;
|
|
3831
|
+
this.maxWaitTimer = setTimeout(() => this.flush("maxWait"), this.config.maxWaitMs);
|
|
3832
|
+
}
|
|
3833
|
+
clearTimers() {
|
|
3834
|
+
if (this.quietTimer) {
|
|
3835
|
+
clearTimeout(this.quietTimer);
|
|
3836
|
+
this.quietTimer = null;
|
|
3837
|
+
}
|
|
3838
|
+
if (this.maxWaitTimer) {
|
|
3839
|
+
clearTimeout(this.maxWaitTimer);
|
|
3840
|
+
this.maxWaitTimer = null;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
};
|
|
3844
|
+
|
|
3845
|
+
// src/commands/watch.ts
|
|
3846
|
+
async function watchCommand(options) {
|
|
3847
|
+
const projectDir = process.cwd();
|
|
3848
|
+
const db = openDatabase12(getDbPath());
|
|
3849
|
+
const jobStore = new JobStore4(db);
|
|
3850
|
+
const debouncer = new Debouncer(
|
|
3851
|
+
(batch) => {
|
|
3852
|
+
const dedupeKey = `watch-review:${batch.files.sort().join(",")}`.slice(0, 255);
|
|
3853
|
+
if (jobStore.hasActiveByType("watch-review")) {
|
|
3854
|
+
console.error(
|
|
3855
|
+
chalk20.yellow(` Skipping batch ${batch.batchId} \u2014 watch-review already queued/running`)
|
|
3856
|
+
);
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
const jobId = jobStore.enqueue({
|
|
3860
|
+
type: "watch-review",
|
|
3861
|
+
payload: {
|
|
3862
|
+
files: batch.files,
|
|
3863
|
+
focus: options.focus,
|
|
3864
|
+
timeout: options.timeout,
|
|
3865
|
+
cwd: projectDir,
|
|
3866
|
+
batchId: batch.batchId,
|
|
3867
|
+
reason: batch.reason
|
|
3868
|
+
},
|
|
3869
|
+
dedupeKey
|
|
3870
|
+
});
|
|
3871
|
+
const event = {
|
|
3872
|
+
type: "watch_batch",
|
|
3873
|
+
jobId,
|
|
3874
|
+
batchId: batch.batchId,
|
|
3875
|
+
files: batch.files.length,
|
|
3876
|
+
reason: batch.reason,
|
|
3877
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
3878
|
+
};
|
|
3879
|
+
console.log(JSON.stringify(event));
|
|
3880
|
+
},
|
|
3881
|
+
{
|
|
3882
|
+
quietMs: options.quietMs,
|
|
3883
|
+
maxWaitMs: options.maxWaitMs,
|
|
3884
|
+
cooldownMs: options.cooldownMs
|
|
3885
|
+
}
|
|
3886
|
+
);
|
|
3887
|
+
const ignored = [
|
|
3888
|
+
"**/node_modules/**",
|
|
3889
|
+
"**/.git/**",
|
|
3890
|
+
"**/dist/**",
|
|
3891
|
+
"**/.mulep/**",
|
|
3892
|
+
"**/coverage/**",
|
|
3893
|
+
"**/*.db",
|
|
3894
|
+
"**/*.db-journal",
|
|
3895
|
+
"**/*.db-wal"
|
|
3896
|
+
];
|
|
3897
|
+
console.error(chalk20.cyan(`Watching ${options.glob} for changes...`));
|
|
3898
|
+
console.error(
|
|
3899
|
+
chalk20.dim(
|
|
3900
|
+
` quiet=${options.quietMs}ms, maxWait=${options.maxWaitMs}ms, cooldown=${options.cooldownMs}ms`
|
|
3901
|
+
)
|
|
3902
|
+
);
|
|
3903
|
+
const watcher = watch(options.glob, {
|
|
3904
|
+
cwd: projectDir,
|
|
3905
|
+
ignored,
|
|
3906
|
+
ignoreInitial: true,
|
|
3907
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
3908
|
+
});
|
|
3909
|
+
watcher.on("all", (event, path) => {
|
|
3910
|
+
if (event === "add" || event === "change" || event === "unlink") {
|
|
3911
|
+
debouncer.push({ path, event, ts: Date.now() });
|
|
3912
|
+
}
|
|
3913
|
+
});
|
|
3914
|
+
const shutdown = () => {
|
|
3915
|
+
console.error(chalk20.dim("\nShutting down watcher..."));
|
|
3916
|
+
debouncer.flushNow();
|
|
3917
|
+
debouncer.destroy();
|
|
3918
|
+
watcher.close();
|
|
3919
|
+
db.close();
|
|
3920
|
+
process.exit(0);
|
|
3921
|
+
};
|
|
3922
|
+
process.on("SIGINT", shutdown);
|
|
3923
|
+
process.on("SIGTERM", shutdown);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// src/commands/worker.ts
|
|
3927
|
+
import {
|
|
3928
|
+
JobStore as JobStore5,
|
|
3929
|
+
ModelRegistry as ModelRegistry7,
|
|
3930
|
+
REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS4,
|
|
3931
|
+
buildHandoffEnvelope as buildHandoffEnvelope6,
|
|
3932
|
+
loadConfig as loadConfig10,
|
|
3933
|
+
openDatabase as openDatabase13
|
|
3934
|
+
} from "@mulep/core";
|
|
3935
|
+
import chalk21 from "chalk";
|
|
3936
|
+
import { execSync as execSync7 } from "child_process";
|
|
3937
|
+
async function workerCommand(options) {
|
|
3938
|
+
const projectDir = process.cwd();
|
|
3939
|
+
const db = openDatabase13(getDbPath());
|
|
3940
|
+
const jobStore = new JobStore5(db);
|
|
3941
|
+
const config = loadConfig10();
|
|
3942
|
+
const registry = ModelRegistry7.fromConfig(config, projectDir);
|
|
3943
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
3944
|
+
if (!adapter) {
|
|
3945
|
+
try {
|
|
3946
|
+
execSync7("codex --version", { stdio: "pipe", encoding: "utf-8" });
|
|
3947
|
+
} catch {
|
|
3948
|
+
console.error(chalk21.red("Codex CLI is not installed or not in PATH."));
|
|
3949
|
+
console.error(chalk21.yellow("Install it: npm install -g @openai/codex"));
|
|
3950
|
+
db.close();
|
|
3951
|
+
process.exit(1);
|
|
3952
|
+
}
|
|
3953
|
+
console.error(chalk21.red("No codex adapter found in config. Run: mulep init"));
|
|
3954
|
+
db.close();
|
|
3955
|
+
process.exit(1);
|
|
3956
|
+
}
|
|
3957
|
+
const workerId = options.workerId;
|
|
3958
|
+
console.error(
|
|
3959
|
+
chalk21.cyan(`Worker ${workerId} started (poll: ${options.pollMs}ms, once: ${options.once})`)
|
|
3960
|
+
);
|
|
3961
|
+
let running = true;
|
|
3962
|
+
const shutdown = () => {
|
|
3963
|
+
running = false;
|
|
3964
|
+
console.error(chalk21.dim("\nWorker shutting down..."));
|
|
3965
|
+
};
|
|
3966
|
+
process.on("SIGINT", shutdown);
|
|
3967
|
+
process.on("SIGTERM", shutdown);
|
|
3968
|
+
while (running) {
|
|
3969
|
+
const job = jobStore.claimNext(workerId)[0];
|
|
3970
|
+
if (!job) {
|
|
3971
|
+
if (options.once) {
|
|
3972
|
+
console.error(chalk21.dim("No jobs in queue. Exiting (--once mode)."));
|
|
3973
|
+
break;
|
|
3974
|
+
}
|
|
3975
|
+
await new Promise((r) => setTimeout(r, options.pollMs));
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
console.error(chalk21.cyan(`Processing job ${job.id} (type: ${job.type})`));
|
|
3979
|
+
jobStore.appendLog(job.id, "info", "job_started", `Worker ${workerId} claimed job`);
|
|
3980
|
+
try {
|
|
3981
|
+
const { resolve: resolve6, normalize } = await import("path");
|
|
3982
|
+
const payload = JSON.parse(job.payloadJson);
|
|
3983
|
+
const rawCwd = resolve6(payload.path ?? payload.cwd ?? projectDir);
|
|
3984
|
+
const cwd = normalize(rawCwd);
|
|
3985
|
+
const sep = process.platform === "win32" ? "\\" : "/";
|
|
3986
|
+
if (cwd !== normalize(projectDir) && !cwd.startsWith(normalize(projectDir) + sep)) {
|
|
3987
|
+
throw new Error(`Path traversal blocked: "${cwd}" is outside project directory "${projectDir}"`);
|
|
3988
|
+
}
|
|
3989
|
+
const timeout = (payload.timeout ?? 600) * 1e3;
|
|
3990
|
+
let prompt;
|
|
3991
|
+
if (job.type === "review" || job.type === "watch-review") {
|
|
3992
|
+
const focus = payload.focus ?? "all";
|
|
3993
|
+
const focusConstraint = focus === "all" ? "Review for: correctness, bugs, security, performance, code quality" : `Focus specifically on: ${focus}`;
|
|
3994
|
+
if (payload.prompt) {
|
|
3995
|
+
prompt = buildHandoffEnvelope6({
|
|
3996
|
+
command: "review",
|
|
3997
|
+
task: `TASK: ${payload.prompt}
|
|
3998
|
+
|
|
3999
|
+
Start by listing candidate files, then inspect them thoroughly.`,
|
|
4000
|
+
constraints: [focusConstraint],
|
|
4001
|
+
resumed: false
|
|
4002
|
+
});
|
|
4003
|
+
} else if (payload.diff) {
|
|
4004
|
+
const { execFileSync: execFileSync5 } = await import("child_process");
|
|
4005
|
+
const diffArgs = payload.diff.split(/\s+/).filter((a) => a.length > 0);
|
|
4006
|
+
for (const arg of diffArgs) {
|
|
4007
|
+
if (arg.startsWith("-") || !/^[a-zA-Z0-9_.~^:\/\\@{}]+$/.test(arg)) {
|
|
4008
|
+
throw new Error(`Invalid diff argument: "${arg}" \u2014 only git refs and paths allowed`);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
const diff = execFileSync5("git", ["diff", ...diffArgs], {
|
|
4012
|
+
cwd,
|
|
4013
|
+
encoding: "utf-8",
|
|
4014
|
+
maxBuffer: 1024 * 1024
|
|
4015
|
+
});
|
|
4016
|
+
prompt = buildHandoffEnvelope6({
|
|
4017
|
+
command: "review",
|
|
4018
|
+
task: `Review these code changes.
|
|
4019
|
+
|
|
4020
|
+
GIT DIFF:
|
|
4021
|
+
${diff.slice(0, REVIEW_DIFF_MAX_CHARS4)}`,
|
|
4022
|
+
constraints: [focusConstraint],
|
|
4023
|
+
resumed: false
|
|
4024
|
+
});
|
|
4025
|
+
} else if (payload.files && Array.isArray(payload.files)) {
|
|
4026
|
+
prompt = buildHandoffEnvelope6({
|
|
4027
|
+
command: "review",
|
|
4028
|
+
task: `Review these files: ${payload.files.join(", ")}. Read each file and report issues.`,
|
|
4029
|
+
constraints: [focusConstraint],
|
|
4030
|
+
resumed: false
|
|
4031
|
+
});
|
|
4032
|
+
} else {
|
|
4033
|
+
prompt = buildHandoffEnvelope6({
|
|
4034
|
+
command: "review",
|
|
4035
|
+
task: "Review the codebase for issues. Start by listing key files.",
|
|
4036
|
+
constraints: [focusConstraint],
|
|
4037
|
+
resumed: false
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
} else if (job.type === "cleanup") {
|
|
4041
|
+
prompt = buildHandoffEnvelope6({
|
|
4042
|
+
command: "cleanup",
|
|
4043
|
+
task: `Scan ${cwd} for: unused dependencies, dead code, duplicates, hardcoded values. Report findings with confidence levels.`,
|
|
4044
|
+
constraints: [`Scope: ${payload.scope ?? "all"}`],
|
|
4045
|
+
resumed: false
|
|
4046
|
+
});
|
|
4047
|
+
} else {
|
|
4048
|
+
jobStore.fail(job.id, `Unsupported job type: ${job.type}`);
|
|
4049
|
+
continue;
|
|
4050
|
+
}
|
|
4051
|
+
jobStore.appendLog(job.id, "info", "codex_started", "Sending to codex...");
|
|
4052
|
+
const progress = createProgressCallbacks("worker");
|
|
4053
|
+
const result = await adapter.callWithResume(prompt, {
|
|
4054
|
+
timeout,
|
|
4055
|
+
...progress
|
|
4056
|
+
});
|
|
4057
|
+
jobStore.appendLog(
|
|
4058
|
+
job.id,
|
|
4059
|
+
"info",
|
|
4060
|
+
"codex_completed",
|
|
4061
|
+
`Received ${result.text.length} chars in ${result.durationMs}ms`
|
|
4062
|
+
);
|
|
4063
|
+
const resultData = {
|
|
4064
|
+
text: result.text,
|
|
4065
|
+
usage: result.usage,
|
|
4066
|
+
durationMs: result.durationMs,
|
|
4067
|
+
sessionId: result.sessionId
|
|
4068
|
+
};
|
|
4069
|
+
jobStore.succeed(job.id, resultData);
|
|
4070
|
+
console.error(chalk21.green(`Job ${job.id} completed (${result.durationMs}ms)`));
|
|
4071
|
+
} catch (err) {
|
|
4072
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4073
|
+
jobStore.appendLog(job.id, "error", "job_failed", errMsg);
|
|
4074
|
+
jobStore.fail(job.id, errMsg);
|
|
4075
|
+
console.error(chalk21.red(`Job ${job.id} failed: ${errMsg}`));
|
|
4076
|
+
}
|
|
4077
|
+
if (options.once) break;
|
|
4078
|
+
}
|
|
4079
|
+
db.close();
|
|
4080
|
+
console.error(chalk21.dim("Worker stopped."));
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
// src/commands/workspace.ts
|
|
4084
|
+
import chalk22 from "chalk";
|
|
4085
|
+
import { WorkspaceScanner, createLogger } from "@mulep/core";
|
|
4086
|
+
var logger = createLogger("workspace");
|
|
4087
|
+
async function workspaceInitCommand() {
|
|
4088
|
+
const cwd = process.cwd();
|
|
4089
|
+
console.log(chalk22.blue("Initializing MuleP workspace..."));
|
|
4090
|
+
console.log(chalk22.dim(`Scanning ${cwd} for Mule projects...`));
|
|
4091
|
+
const scanner = new WorkspaceScanner(cwd);
|
|
4092
|
+
const registry = scanner.scan();
|
|
4093
|
+
if (registry.totalProjects === 0) {
|
|
4094
|
+
console.log(chalk22.yellow("\nNo Mule projects found in this directory."));
|
|
4095
|
+
console.log(chalk22.dim("Mule projects are detected by pom.xml + src/main/mule/ structure"));
|
|
4096
|
+
console.log(chalk22.dim("or by the presence of mule-maven-plugin in pom.xml"));
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
printRegistrySummary(registry);
|
|
4100
|
+
console.log(chalk22.green("\nWorkspace initialized successfully."));
|
|
4101
|
+
}
|
|
4102
|
+
async function workspaceListCommand() {
|
|
4103
|
+
const cwd = process.cwd();
|
|
4104
|
+
const scanner = new WorkspaceScanner(cwd);
|
|
4105
|
+
const registry = scanner.scan();
|
|
4106
|
+
if (registry.totalProjects === 0) {
|
|
4107
|
+
console.log(chalk22.yellow("No Mule projects found. Run `mulep workspace init` first."));
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
printRegistrySummary(registry);
|
|
4111
|
+
}
|
|
4112
|
+
async function workspaceValidateCommand() {
|
|
4113
|
+
const cwd = process.cwd();
|
|
4114
|
+
console.log(chalk22.blue("Validating workspace integrity..."));
|
|
4115
|
+
const scanner = new WorkspaceScanner(cwd);
|
|
4116
|
+
const registry = scanner.scan();
|
|
4117
|
+
if (registry.totalProjects === 0) {
|
|
4118
|
+
console.log(chalk22.yellow("No Mule projects found."));
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
let issues = 0;
|
|
4122
|
+
for (const project of registry.projects) {
|
|
4123
|
+
if (!project.muleVersion) {
|
|
4124
|
+
console.log(chalk22.yellow(` [WARN] ${project.name}: Mule version not detected`));
|
|
4125
|
+
issues++;
|
|
4126
|
+
}
|
|
4127
|
+
if (project.flowFiles.length > 0 && !project.hasMunitTests) {
|
|
4128
|
+
console.log(chalk22.yellow(` [WARN] ${project.name}: Has flows but no MUnit tests`));
|
|
4129
|
+
issues++;
|
|
4130
|
+
}
|
|
4131
|
+
if (project.connectors.length === 0 && project.flowFiles.length > 0) {
|
|
4132
|
+
console.log(chalk22.yellow(` [WARN] ${project.name}: Has flows but no connectors detected`));
|
|
4133
|
+
issues++;
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
4137
|
+
for (const project of registry.projects) {
|
|
4138
|
+
for (const connector2 of project.connectors) {
|
|
4139
|
+
if (!versionMap.has(connector2.artifactId)) {
|
|
4140
|
+
versionMap.set(connector2.artifactId, /* @__PURE__ */ new Set());
|
|
4141
|
+
}
|
|
4142
|
+
versionMap.get(connector2.artifactId).add(connector2.version);
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
for (const [artifact, versions] of versionMap) {
|
|
4146
|
+
if (versions.size > 1) {
|
|
4147
|
+
console.log(
|
|
4148
|
+
chalk22.red(` [ERROR] Connector version conflict: ${artifact} has versions: ${Array.from(versions).join(", ")}`)
|
|
4149
|
+
);
|
|
4150
|
+
issues++;
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
if (issues === 0) {
|
|
4154
|
+
console.log(chalk22.green("\nWorkspace validation passed \u2014 no issues found."));
|
|
4155
|
+
} else {
|
|
4156
|
+
console.log(chalk22.yellow(`
|
|
4157
|
+
Workspace validation found ${issues} issue(s).`));
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
async function workspaceSyncCommand() {
|
|
4161
|
+
const cwd = process.cwd();
|
|
4162
|
+
console.log(chalk22.blue("Syncing workspace project registry..."));
|
|
4163
|
+
const scanner = new WorkspaceScanner(cwd);
|
|
4164
|
+
const registry = scanner.scan();
|
|
4165
|
+
printRegistrySummary(registry);
|
|
4166
|
+
console.log(chalk22.green("\nProject registry synced."));
|
|
4167
|
+
}
|
|
4168
|
+
function printRegistrySummary(registry) {
|
|
4169
|
+
console.log(chalk22.bold(`
|
|
4170
|
+
Workspace: ${registry.workspaceRoot}`));
|
|
4171
|
+
console.log(chalk22.dim(`Discovered at: ${registry.discoveredAt}`));
|
|
4172
|
+
console.log(`Projects: ${chalk22.cyan(String(registry.totalProjects))}`);
|
|
4173
|
+
console.log(`Flow files: ${chalk22.cyan(String(registry.totalFlowFiles))}`);
|
|
4174
|
+
console.log(`DataWeave files: ${chalk22.cyan(String(registry.totalDataweaveFiles))}`);
|
|
4175
|
+
console.log(chalk22.bold("\nProjects:"));
|
|
4176
|
+
for (const project of registry.projects) {
|
|
4177
|
+
const muleVer = project.muleVersion ? chalk22.dim(`(Mule ${project.muleVersion})`) : chalk22.dim("(version unknown)");
|
|
4178
|
+
const edition = project.runtimeEdition !== "unknown" ? chalk22.dim(`[${project.runtimeEdition}]`) : "";
|
|
4179
|
+
console.log(` ${chalk22.green("\u25CF")} ${chalk22.bold(project.name)} ${muleVer} ${edition}`);
|
|
4180
|
+
console.log(chalk22.dim(` Path: ${project.relativePath || "."}`));
|
|
4181
|
+
console.log(chalk22.dim(` Flows: ${project.flowFiles.length} | DataWeave: ${project.dataweaveFiles.length} | Connectors: ${project.connectors.length}`));
|
|
4182
|
+
if (project.hasMunitTests) {
|
|
4183
|
+
console.log(chalk22.dim(" MUnit: yes"));
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
// src/commands/flow.ts
|
|
4189
|
+
import chalk23 from "chalk";
|
|
4190
|
+
import { FlowParser, WorkspaceScanner as WorkspaceScanner2 } from "@mulep/core";
|
|
4191
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
4192
|
+
import { join as join8, resolve as resolve3 } from "path";
|
|
4193
|
+
async function flowVisualizeCommand(flowName, options) {
|
|
4194
|
+
const flowFile = findFlowFile(flowName, options.project);
|
|
4195
|
+
if (!flowFile) return;
|
|
4196
|
+
const parser = new FlowParser();
|
|
4197
|
+
const complexity = parser.parse(flowFile);
|
|
4198
|
+
const mermaid = parser.generateMermaid(complexity);
|
|
4199
|
+
if (options.output) {
|
|
4200
|
+
writeFileSync5(options.output, mermaid, "utf-8");
|
|
4201
|
+
console.log(chalk23.green(`Mermaid diagram written to ${options.output}`));
|
|
4202
|
+
} else {
|
|
4203
|
+
console.log(chalk23.bold("\nMermaid Flow Diagram:"));
|
|
4204
|
+
console.log(chalk23.dim("(Copy to https://mermaid.live to visualize)\n"));
|
|
4205
|
+
console.log(mermaid);
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
async function flowAnalyzeCommand(flowName, options) {
|
|
4209
|
+
const flowFile = findFlowFile(flowName, options.project);
|
|
4210
|
+
if (!flowFile) return;
|
|
4211
|
+
const parser = new FlowParser();
|
|
4212
|
+
const complexity = parser.parse(flowFile);
|
|
4213
|
+
printFlowReport(complexity);
|
|
4214
|
+
}
|
|
4215
|
+
async function flowTestGenCommand(flowName, options) {
|
|
4216
|
+
const flowFile = findFlowFile(flowName, options.project);
|
|
4217
|
+
if (!flowFile) return;
|
|
4218
|
+
const parser = new FlowParser();
|
|
4219
|
+
const complexity = parser.parse(flowFile);
|
|
4220
|
+
console.log(chalk23.bold("\nMUnit Test Suggestions:"));
|
|
4221
|
+
for (const flow2 of complexity.flows) {
|
|
4222
|
+
console.log(chalk23.cyan(`
|
|
4223
|
+
Flow: ${flow2.name}`));
|
|
4224
|
+
console.log(chalk23.dim(` Steps: ${flow2.steps.length}`));
|
|
4225
|
+
console.log(chalk23.bold(" Suggested test cases:"));
|
|
4226
|
+
console.log(chalk23.dim(" 1. Happy path \u2014 verify end-to-end flow execution"));
|
|
4227
|
+
if (flow2.errorHandlers.length > 0) {
|
|
4228
|
+
console.log(chalk23.dim(" 2. Error handling \u2014 verify error handlers trigger correctly"));
|
|
4229
|
+
} else {
|
|
4230
|
+
console.log(chalk23.yellow(" 2. [MISSING] No error handlers \u2014 add on-error-propagate"));
|
|
4231
|
+
}
|
|
4232
|
+
const connectorSteps = flow2.steps.filter((s) => s.connectorType);
|
|
4233
|
+
if (connectorSteps.length > 0) {
|
|
4234
|
+
console.log(
|
|
4235
|
+
chalk23.dim(
|
|
4236
|
+
` 3. Connector mocking \u2014 mock ${connectorSteps.map((s) => s.connectorType).join(", ")} connectors`
|
|
4237
|
+
)
|
|
4238
|
+
);
|
|
4239
|
+
}
|
|
4240
|
+
if (flow2.steps.some((s) => s.type.includes("choice"))) {
|
|
4241
|
+
console.log(chalk23.dim(" 4. Branch coverage \u2014 test all choice router paths"));
|
|
4242
|
+
}
|
|
4243
|
+
if (flow2.steps.some((s) => s.type === "set-variable")) {
|
|
4244
|
+
console.log(chalk23.dim(" 5. Variable assertions \u2014 verify variable values after flow"));
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
function findFlowFile(flowName, projectName) {
|
|
4249
|
+
if (existsSync6(flowName) && flowName.endsWith(".xml")) {
|
|
4250
|
+
return resolve3(flowName);
|
|
4251
|
+
}
|
|
4252
|
+
const cwd = process.cwd();
|
|
4253
|
+
const scanner = new WorkspaceScanner2(cwd);
|
|
4254
|
+
const registry = scanner.scan();
|
|
4255
|
+
if (registry.totalProjects === 0) {
|
|
4256
|
+
console.log(chalk23.yellow("No Mule projects found in workspace."));
|
|
4257
|
+
return null;
|
|
4258
|
+
}
|
|
4259
|
+
const projects = projectName ? registry.projects.filter((p) => p.name === projectName) : registry.projects;
|
|
4260
|
+
for (const project of projects) {
|
|
4261
|
+
for (const flowFile of project.flowFiles) {
|
|
4262
|
+
const fullPath = join8(project.path, flowFile);
|
|
4263
|
+
if (flowFile.includes(flowName) || flowFile.endsWith(`${flowName}.xml`)) {
|
|
4264
|
+
return fullPath;
|
|
4265
|
+
}
|
|
4266
|
+
try {
|
|
4267
|
+
const content = readFileSync6(fullPath, "utf-8");
|
|
4268
|
+
if (content.includes(`name="${flowName}"`)) {
|
|
4269
|
+
return fullPath;
|
|
4270
|
+
}
|
|
4271
|
+
} catch {
|
|
4272
|
+
continue;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
console.log(chalk23.yellow(`Flow "${flowName}" not found in workspace.`));
|
|
4277
|
+
return null;
|
|
4278
|
+
}
|
|
4279
|
+
function printFlowReport(complexity) {
|
|
4280
|
+
console.log(chalk23.bold(`
|
|
4281
|
+
Flow Analysis: ${complexity.file}`));
|
|
4282
|
+
console.log(chalk23.dim("\u2500".repeat(60)));
|
|
4283
|
+
console.log(chalk23.bold("\nMetrics:"));
|
|
4284
|
+
console.log(` Total Flows: ${chalk23.cyan(String(complexity.metrics.totalFlows))}`);
|
|
4285
|
+
console.log(` Sub-Flows: ${chalk23.cyan(String(complexity.metrics.totalSubFlows))}`);
|
|
4286
|
+
console.log(` Total Steps: ${chalk23.cyan(String(complexity.metrics.totalSteps))}`);
|
|
4287
|
+
console.log(` Max Nesting Depth: ${chalk23.cyan(String(complexity.metrics.maxNestingDepth))}`);
|
|
4288
|
+
console.log(` Avg Steps/Flow: ${chalk23.cyan(String(complexity.metrics.averageStepsPerFlow))}`);
|
|
4289
|
+
console.log(` Error Handler Coverage: ${colorCoverage(complexity.metrics.errorHandlerCoverage)}%`);
|
|
4290
|
+
console.log(` Connector Diversity: ${chalk23.cyan(String(complexity.metrics.connectorDiversity))}`);
|
|
4291
|
+
console.log(` Complexity Score: ${colorComplexity(complexity.metrics.complexityScore)}`);
|
|
4292
|
+
if (complexity.findings.length > 0) {
|
|
4293
|
+
console.log(chalk23.bold("\nFindings:"));
|
|
4294
|
+
for (const finding of complexity.findings) {
|
|
4295
|
+
const icon = finding.severity === "critical" ? "\u25CF" : finding.severity === "high" ? "\u25B2" : "\u25CB";
|
|
4296
|
+
const color = finding.severity === "critical" ? chalk23.red : finding.severity === "high" ? chalk23.yellow : chalk23.dim;
|
|
4297
|
+
console.log(` ${color(icon)} [${finding.severity.toUpperCase()}] ${finding.message}`);
|
|
4298
|
+
if (finding.suggestion) {
|
|
4299
|
+
console.log(chalk23.dim(` \u2192 ${finding.suggestion}`));
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
} else {
|
|
4303
|
+
console.log(chalk23.green("\nNo issues found."));
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
function colorCoverage(pct) {
|
|
4307
|
+
if (pct >= 80) return chalk23.green(String(pct));
|
|
4308
|
+
if (pct >= 50) return chalk23.yellow(String(pct));
|
|
4309
|
+
return chalk23.red(String(pct));
|
|
4310
|
+
}
|
|
4311
|
+
function colorComplexity(score) {
|
|
4312
|
+
if (score <= 10) return chalk23.green(String(score));
|
|
4313
|
+
if (score <= 25) return chalk23.yellow(String(score));
|
|
4314
|
+
return chalk23.red(String(score));
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
// src/commands/dataweave.ts
|
|
4318
|
+
import chalk24 from "chalk";
|
|
4319
|
+
import { DataWeaveAnalyzer } from "@mulep/core";
|
|
4320
|
+
import { existsSync as existsSync7, readdirSync, statSync as statSync2 } from "fs";
|
|
4321
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
4322
|
+
async function dataWeaveLintCommand(fileOrGlob, options) {
|
|
4323
|
+
const files = resolveDataWeaveFiles(fileOrGlob);
|
|
4324
|
+
if (files.length === 0) {
|
|
4325
|
+
console.log(chalk24.yellow("No DataWeave (.dwl) files found."));
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
const analyzer = new DataWeaveAnalyzer();
|
|
4329
|
+
let totalFindings = 0;
|
|
4330
|
+
for (const file of files) {
|
|
4331
|
+
const report = analyzer.analyze(file);
|
|
4332
|
+
if (report.findings.length > 0) {
|
|
4333
|
+
printDataWeaveReport(report);
|
|
4334
|
+
totalFindings += report.findings.length;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
if (totalFindings === 0) {
|
|
4338
|
+
console.log(chalk24.green(`
|
|
4339
|
+
All ${files.length} DataWeave file(s) passed lint checks.`));
|
|
4340
|
+
} else {
|
|
4341
|
+
console.log(chalk24.yellow(`
|
|
4342
|
+
Found ${totalFindings} issue(s) across ${files.length} file(s).`));
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
async function dataWeaveOptimizeCommand(fileOrGlob, options) {
|
|
4346
|
+
const files = resolveDataWeaveFiles(fileOrGlob);
|
|
4347
|
+
if (files.length === 0) {
|
|
4348
|
+
console.log(chalk24.yellow("No DataWeave (.dwl) files found."));
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
const analyzer = new DataWeaveAnalyzer();
|
|
4352
|
+
for (const file of files) {
|
|
4353
|
+
const report = analyzer.analyze(file);
|
|
4354
|
+
console.log(chalk24.bold(`
|
|
4355
|
+
Optimization Report: ${file}`));
|
|
4356
|
+
console.log(chalk24.dim("\u2500".repeat(60)));
|
|
4357
|
+
console.log(` Lines: ${report.metrics.lineCount}`);
|
|
4358
|
+
console.log(` Functions: ${report.metrics.functionCount}`);
|
|
4359
|
+
console.log(` Complexity: ${colorComplexity2(report.metrics.complexityScore)}`);
|
|
4360
|
+
const perfFindings = report.findings.filter((f) => f.rule === "performance");
|
|
4361
|
+
if (perfFindings.length > 0) {
|
|
4362
|
+
console.log(chalk24.bold("\n Performance Suggestions:"));
|
|
4363
|
+
for (const finding of perfFindings) {
|
|
4364
|
+
console.log(chalk24.yellow(` \u2192 ${finding.message}`));
|
|
4365
|
+
if (finding.suggestion) {
|
|
4366
|
+
console.log(chalk24.dim(` Fix: ${finding.suggestion}`));
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
} else {
|
|
4370
|
+
console.log(chalk24.green("\n No performance issues detected."));
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
async function dataWeaveTestGenCommand(fileOrGlob, options) {
|
|
4375
|
+
const files = resolveDataWeaveFiles(fileOrGlob);
|
|
4376
|
+
if (files.length === 0) {
|
|
4377
|
+
console.log(chalk24.yellow("No DataWeave (.dwl) files found."));
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
const analyzer = new DataWeaveAnalyzer();
|
|
4381
|
+
for (const file of files) {
|
|
4382
|
+
const report = analyzer.analyze(file);
|
|
4383
|
+
console.log(chalk24.bold(`
|
|
4384
|
+
MUnit Test Suggestions: ${file}`));
|
|
4385
|
+
console.log(chalk24.dim("\u2500".repeat(60)));
|
|
4386
|
+
console.log(chalk24.bold(" Suggested test cases:"));
|
|
4387
|
+
console.log(chalk24.dim(" 1. Happy path \u2014 valid input produces expected output"));
|
|
4388
|
+
console.log(chalk24.dim(" 2. Null payload \u2014 verify behavior with null input"));
|
|
4389
|
+
console.log(chalk24.dim(" 3. Empty collection \u2014 verify behavior with empty arrays/objects"));
|
|
4390
|
+
if (report.metrics.hasNullSafetyIssues) {
|
|
4391
|
+
console.log(chalk24.yellow(" 4. [IMPORTANT] Null field access \u2014 test with missing fields"));
|
|
4392
|
+
}
|
|
4393
|
+
if (report.metrics.complexityScore > 10) {
|
|
4394
|
+
console.log(chalk24.dim(" 5. Edge cases \u2014 test boundary conditions for complex logic"));
|
|
4395
|
+
}
|
|
4396
|
+
if (report.findings.some((f) => f.rule === "hardcoded-value")) {
|
|
4397
|
+
console.log(chalk24.dim(" 6. Configuration variations \u2014 test with different property values"));
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
function resolveDataWeaveFiles(fileOrGlob) {
|
|
4402
|
+
const resolved = resolve4(fileOrGlob);
|
|
4403
|
+
if (existsSync7(resolved) && resolved.endsWith(".dwl")) {
|
|
4404
|
+
return [resolved];
|
|
4405
|
+
}
|
|
4406
|
+
if (existsSync7(resolved) && statSync2(resolved).isDirectory()) {
|
|
4407
|
+
return collectDwlFiles(resolved);
|
|
4408
|
+
}
|
|
4409
|
+
const cwd = process.cwd();
|
|
4410
|
+
return collectDwlFiles(cwd);
|
|
4411
|
+
}
|
|
4412
|
+
function collectDwlFiles(dir) {
|
|
4413
|
+
const files = [];
|
|
4414
|
+
try {
|
|
4415
|
+
const entries = readdirSync(dir);
|
|
4416
|
+
for (const entry of entries) {
|
|
4417
|
+
if (entry === "node_modules" || entry === "target" || entry === ".git") continue;
|
|
4418
|
+
const fullPath = join9(dir, entry);
|
|
4419
|
+
try {
|
|
4420
|
+
const stat = statSync2(fullPath);
|
|
4421
|
+
if (stat.isDirectory()) {
|
|
4422
|
+
files.push(...collectDwlFiles(fullPath));
|
|
4423
|
+
} else if (entry.endsWith(".dwl")) {
|
|
4424
|
+
files.push(fullPath);
|
|
4425
|
+
}
|
|
4426
|
+
} catch {
|
|
4427
|
+
continue;
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
} catch {
|
|
4431
|
+
}
|
|
4432
|
+
return files;
|
|
4433
|
+
}
|
|
4434
|
+
function printDataWeaveReport(report) {
|
|
4435
|
+
console.log(chalk24.bold(`
|
|
4436
|
+
${report.file}`));
|
|
4437
|
+
for (const finding of report.findings) {
|
|
4438
|
+
const icon = finding.severity === "critical" ? chalk24.red("\u25CF") : finding.severity === "high" ? chalk24.yellow("\u25B2") : finding.severity === "medium" ? chalk24.blue("\u25A0") : chalk24.dim("\u25CB");
|
|
4439
|
+
console.log(` ${icon} ${chalk24.dim(`L${finding.line}:`)} ${finding.message}`);
|
|
4440
|
+
if (finding.suggestion) {
|
|
4441
|
+
console.log(chalk24.dim(` \u2192 ${finding.suggestion}`));
|
|
4442
|
+
}
|
|
4443
|
+
if (finding.codeSnippet) {
|
|
4444
|
+
console.log(chalk24.dim(` | ${finding.codeSnippet}`));
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function colorComplexity2(score) {
|
|
4449
|
+
if (score <= 5) return chalk24.green(String(score));
|
|
4450
|
+
if (score <= 15) return chalk24.yellow(String(score));
|
|
4451
|
+
return chalk24.red(String(score));
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
// src/commands/connector.ts
|
|
4455
|
+
import chalk25 from "chalk";
|
|
4456
|
+
import { ConnectorAuditor, WorkspaceScanner as WorkspaceScanner3 } from "@mulep/core";
|
|
4457
|
+
async function connectorAuditCommand(options) {
|
|
4458
|
+
const cwd = process.cwd();
|
|
4459
|
+
const scanner = new WorkspaceScanner3(cwd);
|
|
4460
|
+
const registry = scanner.scan();
|
|
4461
|
+
if (registry.totalProjects === 0) {
|
|
4462
|
+
console.log(chalk25.yellow("No Mule projects found in workspace."));
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
const projects = options.project ? registry.projects.filter((p) => p.name === options.project) : registry.projects;
|
|
4466
|
+
if (projects.length === 0) {
|
|
4467
|
+
console.log(chalk25.yellow(`Project "${options.project}" not found.`));
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
const auditor = new ConnectorAuditor();
|
|
4471
|
+
const result = auditor.audit(projects);
|
|
4472
|
+
if (options.json) {
|
|
4473
|
+
console.log(
|
|
4474
|
+
JSON.stringify(
|
|
4475
|
+
{
|
|
4476
|
+
...result,
|
|
4477
|
+
versionMatrix: Object.fromEntries(
|
|
4478
|
+
Array.from(result.versionMatrix.entries()).map(([k, v]) => [
|
|
4479
|
+
k,
|
|
4480
|
+
Object.fromEntries(v)
|
|
4481
|
+
])
|
|
4482
|
+
)
|
|
4483
|
+
},
|
|
4484
|
+
null,
|
|
4485
|
+
2
|
|
4486
|
+
)
|
|
4487
|
+
);
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
console.log(chalk25.bold("\nConnector Audit Report"));
|
|
4491
|
+
console.log(chalk25.dim("\u2500".repeat(60)));
|
|
4492
|
+
console.log(chalk25.bold("\nMetrics:"));
|
|
4493
|
+
console.log(` Total Connectors: ${chalk25.cyan(String(result.metrics.totalConnectors))}`);
|
|
4494
|
+
console.log(` Unique Types: ${chalk25.cyan(String(result.metrics.uniqueConnectors))}`);
|
|
4495
|
+
console.log(
|
|
4496
|
+
` Version Conflicts: ${result.metrics.versionConflicts > 0 ? chalk25.red(String(result.metrics.versionConflicts)) : chalk25.green("0")}`
|
|
4497
|
+
);
|
|
4498
|
+
console.log(
|
|
4499
|
+
` Security Issues: ${result.metrics.securityIssues > 0 ? chalk25.red(String(result.metrics.securityIssues)) : chalk25.green("0")}`
|
|
4500
|
+
);
|
|
4501
|
+
console.log(
|
|
4502
|
+
` Missing Configs: ${result.metrics.missingConfigs > 0 ? chalk25.yellow(String(result.metrics.missingConfigs)) : chalk25.green("0")}`
|
|
4503
|
+
);
|
|
4504
|
+
if (result.findings.length > 0) {
|
|
4505
|
+
console.log(chalk25.bold("\nFindings:"));
|
|
4506
|
+
for (const finding of result.findings) {
|
|
4507
|
+
const icon = finding.severity === "critical" ? chalk25.red("\u25CF") : finding.severity === "high" ? chalk25.yellow("\u25B2") : chalk25.dim("\u25CB");
|
|
4508
|
+
console.log(
|
|
4509
|
+
` ${icon} [${finding.severity.toUpperCase()}] ${chalk25.dim(`(${finding.project})`)} ${finding.message}`
|
|
4510
|
+
);
|
|
4511
|
+
if (finding.suggestion) {
|
|
4512
|
+
console.log(chalk25.dim(` \u2192 ${finding.suggestion}`));
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
} else {
|
|
4516
|
+
console.log(chalk25.green("\nNo issues found."));
|
|
4517
|
+
}
|
|
4518
|
+
if (result.versionMatrix.size > 0 && projects.length > 1) {
|
|
4519
|
+
console.log(chalk25.bold("\nVersion Matrix:"));
|
|
4520
|
+
for (const [artifact, versions] of result.versionMatrix) {
|
|
4521
|
+
const versionStr = Array.from(versions.entries()).map(([proj, ver]) => `${proj}: ${ver}`).join(", ");
|
|
4522
|
+
console.log(chalk25.dim(` ${artifact}: ${versionStr}`));
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
async function connectorVersionsCommand(options) {
|
|
4527
|
+
const cwd = process.cwd();
|
|
4528
|
+
const scanner = new WorkspaceScanner3(cwd);
|
|
4529
|
+
const registry = scanner.scan();
|
|
4530
|
+
if (registry.totalProjects === 0) {
|
|
4531
|
+
console.log(chalk25.yellow("No Mule projects found in workspace."));
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
const projects = options.project ? registry.projects.filter((p) => p.name === options.project) : registry.projects;
|
|
4535
|
+
console.log(chalk25.bold("\nConnector Version Matrix"));
|
|
4536
|
+
console.log(chalk25.dim("\u2500".repeat(60)));
|
|
4537
|
+
const allConnectors = /* @__PURE__ */ new Map();
|
|
4538
|
+
for (const project of projects) {
|
|
4539
|
+
for (const connector2 of project.connectors) {
|
|
4540
|
+
if (!allConnectors.has(connector2.artifactId)) {
|
|
4541
|
+
allConnectors.set(connector2.artifactId, /* @__PURE__ */ new Map());
|
|
4542
|
+
}
|
|
4543
|
+
allConnectors.get(connector2.artifactId).set(project.name, connector2.version);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
if (allConnectors.size === 0) {
|
|
4547
|
+
console.log(chalk25.yellow("No connectors found."));
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
const projNames = projects.map((p) => p.name);
|
|
4551
|
+
console.log(chalk25.bold(`
|
|
4552
|
+
${"Connector".padEnd(40)} ${projNames.join(" | ")}`));
|
|
4553
|
+
console.log(chalk25.dim(` ${"\u2500".repeat(40)} ${projNames.map(() => "\u2500".repeat(12)).join("\u2500\u253C\u2500")}`));
|
|
4554
|
+
for (const [artifact, versions] of allConnectors) {
|
|
4555
|
+
const cols = projNames.map((name) => {
|
|
4556
|
+
const ver = versions.get(name);
|
|
4557
|
+
return ver ? ver.padEnd(12) : chalk25.dim("\u2014".padEnd(12));
|
|
4558
|
+
});
|
|
4559
|
+
console.log(` ${artifact.padEnd(40)} ${cols.join(" | ")}`);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
// src/commands/validate.ts
|
|
4564
|
+
import chalk26 from "chalk";
|
|
4565
|
+
import { ScenarioValidator, WorkspaceScanner as WorkspaceScanner4 } from "@mulep/core";
|
|
4566
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
|
|
4567
|
+
import { resolve as resolve5 } from "path";
|
|
4568
|
+
import YAML from "yaml";
|
|
4569
|
+
async function validateScenarioCommand(scenarioFile, options) {
|
|
4570
|
+
const filePath = resolve5(scenarioFile);
|
|
4571
|
+
if (!existsSync8(filePath)) {
|
|
4572
|
+
console.log(chalk26.red(`Scenario file not found: ${filePath}`));
|
|
4573
|
+
process.exitCode = 1;
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
4576
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
4577
|
+
let scenario;
|
|
4578
|
+
try {
|
|
4579
|
+
if (filePath.endsWith(".json")) {
|
|
4580
|
+
scenario = JSON.parse(content);
|
|
4581
|
+
} else {
|
|
4582
|
+
scenario = YAML.parse(content);
|
|
4583
|
+
}
|
|
4584
|
+
} catch (err) {
|
|
4585
|
+
console.log(chalk26.red(`Failed to parse scenario file: ${err}`));
|
|
4586
|
+
process.exitCode = 1;
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
const cwd = process.cwd();
|
|
4590
|
+
const scanner = new WorkspaceScanner4(cwd);
|
|
4591
|
+
const registry = scanner.scan();
|
|
4592
|
+
if (registry.totalProjects === 0) {
|
|
4593
|
+
console.log(chalk26.yellow("No Mule projects found in workspace."));
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
const projects = options.project ? registry.projects.filter((p) => p.name === options.project) : registry.projects;
|
|
4597
|
+
const validator = new ScenarioValidator(projects);
|
|
4598
|
+
const result = validator.validate(scenario);
|
|
4599
|
+
if (options.json) {
|
|
4600
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
printValidationResult(result);
|
|
4604
|
+
if (result.verdict === "BLOCKED") {
|
|
4605
|
+
process.exitCode = 1;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
async function validateSpecCommand(specFile, options) {
|
|
4609
|
+
const filePath = resolve5(specFile);
|
|
4610
|
+
if (!existsSync8(filePath)) {
|
|
4611
|
+
console.log(chalk26.red(`Spec file not found: ${filePath}`));
|
|
4612
|
+
process.exitCode = 1;
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
console.log(chalk26.blue(`Validating spec compliance: ${filePath}`));
|
|
4616
|
+
console.log(chalk26.dim("Spec validation checks RAML/OAS compliance against workspace..."));
|
|
4617
|
+
const scenario = {
|
|
4618
|
+
name: `Spec: ${specFile}`,
|
|
4619
|
+
description: "Auto-generated from spec file",
|
|
4620
|
+
project: options.project,
|
|
4621
|
+
steps: []
|
|
4622
|
+
};
|
|
4623
|
+
const cwd = process.cwd();
|
|
4624
|
+
const scanner = new WorkspaceScanner4(cwd);
|
|
4625
|
+
const registry = scanner.scan();
|
|
4626
|
+
const projects = options.project ? registry.projects.filter((p) => p.name === options.project) : registry.projects;
|
|
4627
|
+
const validator = new ScenarioValidator(projects);
|
|
4628
|
+
const tierResult = validator.validateTier(scenario, "spec-compliance");
|
|
4629
|
+
console.log(chalk26.bold(`
|
|
4630
|
+
Spec Compliance: ${tierResult.status.toUpperCase()}`));
|
|
4631
|
+
for (const finding of tierResult.findings) {
|
|
4632
|
+
const icon = finding.severity === "critical" ? chalk26.red("\u25CF") : chalk26.yellow("\u25B2");
|
|
4633
|
+
console.log(` ${icon} ${finding.message}`);
|
|
4634
|
+
if (finding.suggestion) {
|
|
4635
|
+
console.log(chalk26.dim(` \u2192 ${finding.suggestion}`));
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
async function validateFeasibilityCommand(scenarioFile, options) {
|
|
4640
|
+
const filePath = resolve5(scenarioFile);
|
|
4641
|
+
if (!existsSync8(filePath)) {
|
|
4642
|
+
console.log(chalk26.red(`Scenario file not found: ${filePath}`));
|
|
4643
|
+
process.exitCode = 1;
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
4647
|
+
let scenario;
|
|
4648
|
+
try {
|
|
4649
|
+
if (filePath.endsWith(".json")) {
|
|
4650
|
+
scenario = JSON.parse(content);
|
|
4651
|
+
} else {
|
|
4652
|
+
scenario = YAML.parse(content);
|
|
4653
|
+
}
|
|
4654
|
+
} catch (err) {
|
|
4655
|
+
console.log(chalk26.red(`Failed to parse scenario file: ${err}`));
|
|
4656
|
+
process.exitCode = 1;
|
|
4657
|
+
return;
|
|
4658
|
+
}
|
|
4659
|
+
const cwd = process.cwd();
|
|
4660
|
+
const scanner = new WorkspaceScanner4(cwd);
|
|
4661
|
+
const registry = scanner.scan();
|
|
4662
|
+
const projects = options.project ? registry.projects.filter((p) => p.name === options.project) : registry.projects;
|
|
4663
|
+
const validator = new ScenarioValidator(projects);
|
|
4664
|
+
const tierResult = validator.validateTier(scenario, "feasibility");
|
|
4665
|
+
console.log(chalk26.bold(`
|
|
4666
|
+
Feasibility Analysis: ${tierResult.status.toUpperCase()}`));
|
|
4667
|
+
for (const finding of tierResult.findings) {
|
|
4668
|
+
const icon = finding.severity === "critical" ? chalk26.red("\u25CF") : chalk26.yellow("\u25B2");
|
|
4669
|
+
console.log(` ${icon} ${finding.message}`);
|
|
4670
|
+
if (finding.suggestion) {
|
|
4671
|
+
console.log(chalk26.dim(` \u2192 ${finding.suggestion}`));
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
function printValidationResult(result) {
|
|
4676
|
+
const verdictColor = result.verdict === "PASS" ? chalk26.green : result.verdict === "PARTIAL" ? chalk26.yellow : chalk26.red;
|
|
4677
|
+
console.log(chalk26.bold(`
|
|
4678
|
+
Scenario Validation: ${result.scenario}`));
|
|
4679
|
+
console.log(chalk26.dim("\u2500".repeat(60)));
|
|
4680
|
+
console.log(`Verdict: ${verdictColor(result.verdict)}`);
|
|
4681
|
+
for (const tier of result.tiers) {
|
|
4682
|
+
const statusColor = tier.status === "pass" ? chalk26.green : tier.status === "partial" ? chalk26.yellow : chalk26.red;
|
|
4683
|
+
console.log(chalk26.bold(`
|
|
4684
|
+
${tier.tier}: ${statusColor(tier.status)}`));
|
|
4685
|
+
for (const finding of tier.findings) {
|
|
4686
|
+
const icon = finding.severity === "critical" ? chalk26.red("\u25CF") : finding.severity === "high" ? chalk26.yellow("\u25B2") : chalk26.dim("\u25CB");
|
|
4687
|
+
console.log(` ${icon} ${finding.message}`);
|
|
4688
|
+
if (finding.suggestion) {
|
|
4689
|
+
console.log(chalk26.dim(` \u2192 ${finding.suggestion}`));
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
if (result.actionItems.length > 0) {
|
|
4694
|
+
console.log(chalk26.bold("\nAction Items:"));
|
|
4695
|
+
for (const item of result.actionItems) {
|
|
4696
|
+
console.log(` \u2022 ${item}`);
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
// src/index.ts
|
|
4702
|
+
var program = new Command();
|
|
4703
|
+
program.name("mulep").description("Mule ESB AI development platform \u2014 multi-project workspace, DataWeave analysis, flow validation, and multi-model collaboration").version(VERSION2).option("--verbose", "Enable debug logging");
|
|
4704
|
+
program.command("start").description("First-run setup: verify codex, init config, run quick review").action(startCommand);
|
|
4705
|
+
program.command("doctor").description("Preflight diagnostics: check codex, config, database, git, node").action(doctorCommand);
|
|
4706
|
+
program.command("install-skills").description("Install Claude Code slash commands (/debate, /build, /mulep-review, /cleanup)").option("--force", "Overwrite existing skill files", false).action(installSkillsCommand);
|
|
4707
|
+
program.command("init").description("Initialize MuleP in the current project").option("--preset <name>", "Use preset (cli-first)").option("--non-interactive", "Skip prompts, use defaults").option("--force", "Overwrite existing .mulep.yml").action(initCommand);
|
|
4708
|
+
program.command("run").description("Run a task through the full workflow").argument("<task>", "Task description (natural language)").option("--mode <mode>", "Execution mode (autonomous|interactive)", "autonomous").option("--max-iterations <n>", "Max review loop iterations", (v) => Number.parseInt(v, 10), 3).option("--no-stream", "Disable streaming output").action(runCommand);
|
|
4709
|
+
program.command("review").description("Review code via codex \u2014 files, prompts, or diffs").argument("[file-or-glob]", "File path or glob pattern to review").option("--prompt <instruction>", "Freeform prompt \u2014 codex explores codebase via tools").option("--stdin", "Read prompt from stdin").option("--diff <revspec>", "Review a git diff (e.g., HEAD~3..HEAD, origin/main...HEAD)").option("--scope <glob>", "Restrict codex exploration to matching files (only with --prompt/--stdin)").addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("all")).option("--preset <name>", "Use named preset (security-audit|performance|quick-scan|pre-commit|api-review)").option("--session <id>", "Use specific session (default: active session)").option("--background", "Enqueue review and return immediately").option("--timeout <seconds>", "Timeout in seconds", (v) => {
|
|
4710
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
4711
|
+
const n = Number.parseInt(v, 10);
|
|
4712
|
+
if (n <= 0) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
4713
|
+
return n;
|
|
4714
|
+
}, 600).action(reviewCommand);
|
|
4715
|
+
program.command("cleanup").description("Scan codebase for AI slop: security vulns, anti-patterns, near-duplicates, dead code, and more").argument("[path]", "Project path to scan", ".").addOption(new Option("--scope <scope>", "What to scan for").choices(["deps", "unused-exports", "hardcoded", "duplicates", "deadcode", "security", "near-duplicates", "anti-patterns", "all"]).default("all")).option("--timeout <seconds>", "Codex scan timeout in seconds", (v) => {
|
|
4716
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
4717
|
+
const n = Number.parseInt(v, 10);
|
|
4718
|
+
if (n <= 0) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
4719
|
+
return n;
|
|
4720
|
+
}, CLEANUP_TIMEOUT_SEC).option("--max-disputes <n>", "Max findings to adjudicate", (v) => {
|
|
4721
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Must be a non-negative integer");
|
|
4722
|
+
return Number.parseInt(v, 10);
|
|
4723
|
+
}, 10).option("--host-findings <path>", "JSON file with host AI findings for 3-way merge").option("--output <path>", "Write findings report to JSON file").option("--background", "Enqueue cleanup and return immediately").option("--no-gitignore", "Skip .gitignore rules (scan everything)").option("--quiet", "Suppress human-readable summary").action(cleanupCommand);
|
|
4724
|
+
var plan = program.command("plan").description("Plan generation and review \u2014 write plans, get GPT review");
|
|
4725
|
+
plan.command("generate").description("Generate a plan using architect + reviewer loop").argument("<task>", "Task to plan").option("--rounds <n>", "Max plan-review rounds", (v) => Number.parseInt(v, 10), 3).option("--output <file>", "Save plan to file").action(planGenerateCommand);
|
|
4726
|
+
plan.command("review").description("Send a host-authored plan to codex for review").argument("<plan-file>", "Plan file to review (use - for stdin)").option("--build <id>", "Link review to a build ID").option("--phase <id>", 'Phase identifier (e.g. "1", "setup")').option("--timeout <seconds>", "Review timeout", (v) => Number.parseInt(v, 10), 300).option("--output <file>", "Save review result to file").action(planReviewCommand);
|
|
4727
|
+
var debate = program.command("debate").description("Multi-model debate with session persistence");
|
|
4728
|
+
debate.command("start").description("Start a new debate").argument("<topic>", "Debate topic or question").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).option("--timeout <seconds>", "Default timeout for debate turns in seconds", (v) => {
|
|
4729
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
4730
|
+
const n = Number.parseInt(v, 10);
|
|
4731
|
+
if (n <= 0) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
4732
|
+
return n;
|
|
4733
|
+
}).action(debateStartCommand);
|
|
4734
|
+
debate.command("turn").description("Send a prompt to GPT and get critique (with session resume)").argument("<debate-id>", "Debate ID from start command").argument("<prompt>", "Prompt to send to GPT").option("--round <n>", "Round number", (v) => Number.parseInt(v, 10)).option("--timeout <seconds>", "Timeout in seconds", (v) => Number.parseInt(v, 10)).option("--output <file>", "Write full untruncated response to file").option("--force", "Continue past token budget limit", false).option("--quiet", "Suppress non-error stderr output", false).option("--response-cap <bytes>", "Max response bytes in JSON output (default: 16384)", (v) => Number.parseInt(v, 10)).action(debateTurnCommand);
|
|
4735
|
+
debate.command("next").description("Continue debate with auto-generated prompt").argument("<debate-id>", "Debate ID").option("--timeout <seconds>", "Timeout in seconds", (v) => Number.parseInt(v, 10)).option("--output <file>", "Write full untruncated response to file").option("--force", "Continue past token budget limit", false).option("--quiet", "Suppress non-error stderr output", false).option("--response-cap <bytes>", "Max response bytes in JSON output (default: 16384)", (v) => Number.parseInt(v, 10)).action(debateNextCommand);
|
|
4736
|
+
debate.command("status").description("Show debate status and session info").argument("<debate-id>", "Debate ID").action(debateStatusCommand);
|
|
4737
|
+
debate.command("list").description("List all debates").option("--status <status>", "Filter by status (active|completed|stale)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(debateListCommand);
|
|
4738
|
+
debate.command("history").description("Show full message history with token budget").argument("<debate-id>", "Debate ID").option("--output <file>", "Write full untruncated history to file").action(debateHistoryCommand);
|
|
4739
|
+
debate.command("complete").description("Mark a debate as completed").argument("<debate-id>", "Debate ID").action(debateCompleteCommand);
|
|
4740
|
+
var build = program.command("build").description("Automated build loop: debate \u2192 plan \u2192 implement \u2192 review \u2192 fix");
|
|
4741
|
+
build.command("start").description("Start a new build session").argument("<task>", "Task description").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).option("--allow-dirty", "Allow starting with dirty working tree (auto-stashes)").action(buildStartCommand);
|
|
4742
|
+
build.command("status").description("Show build status and event log").argument("<build-id>", "Build ID").action(buildStatusCommand);
|
|
4743
|
+
build.command("list").description("List all builds").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(buildListCommand);
|
|
4744
|
+
build.command("event").description("Record a build event (phase transition)").argument("<build-id>", "Build ID").argument("<event-type>", "Event type (plan_approved|impl_completed|fix_completed|etc)").option("--loop <n>", "Loop index", (v) => Number.parseInt(v, 10)).option("--tokens <n>", "Tokens used", (v) => Number.parseInt(v, 10)).action(buildEventCommand);
|
|
4745
|
+
build.command("review").description("Send implementation to codex for review (with codebase access)").argument("<build-id>", "Build ID").action(buildReviewCommand);
|
|
4746
|
+
var session = program.command("session").description("Unified session management \u2014 persistent GPT context across commands");
|
|
4747
|
+
session.command("start").description("Start a new session").option("--name <name>", "Session name").action(sessionStartCommand);
|
|
4748
|
+
session.command("current").description("Show the active session").action(sessionCurrentCommand);
|
|
4749
|
+
session.command("list").description("List all sessions").option("--status <status>", "Filter by status (active|completed|stale)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(sessionListCommand);
|
|
4750
|
+
session.command("status").description("Show detailed session info with events").argument("<session-id>", "Session ID").action(sessionStatusCommand);
|
|
4751
|
+
session.command("close").description("Mark a session as completed").argument("<session-id>", "Session ID").action(sessionCloseCommand);
|
|
4752
|
+
var jobs = program.command("jobs").description("Background job queue \u2014 async reviews, cleanups, and more");
|
|
4753
|
+
jobs.command("list").description("List jobs").option("--status <status>", "Filter by status (queued|running|succeeded|failed|canceled)").option("--type <type>", "Filter by type (review|cleanup|build-review|composite|watch-review)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(jobsListCommand);
|
|
4754
|
+
jobs.command("status").description("Show job details with recent logs").argument("<job-id>", "Job ID").action(jobsStatusCommand);
|
|
4755
|
+
jobs.command("logs").description("Show job logs").argument("<job-id>", "Job ID").option("--from-seq <n>", "Start from log sequence number", (v) => Number.parseInt(v, 10), 0).option("--limit <n>", "Max log entries", (v) => Number.parseInt(v, 10), 100).action(jobsLogsCommand);
|
|
4756
|
+
jobs.command("cancel").description("Cancel a queued or running job").argument("<job-id>", "Job ID").action(jobsCancelCommand);
|
|
4757
|
+
jobs.command("retry").description("Retry a failed job").argument("<job-id>", "Job ID").action(jobsRetryCommand);
|
|
4758
|
+
program.command("fix").description("Autofix loop: review code, apply fixes, re-review until approved").argument("<file-or-glob>", "File path or glob pattern to fix").option("--max-rounds <n>", "Max review-fix rounds", (v) => Number.parseInt(v, 10), 3).addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("bugs")).option("--timeout <seconds>", "Timeout per round", (v) => Number.parseInt(v, 10), 600).option("--dry-run", "Review only, do not apply fixes", false).option("--no-stage", "Do not git-stage applied fixes").option("--diff <revspec>", "Fix issues in a git diff").option("--session <id>", "Use specific session").action(fixCommand);
|
|
4759
|
+
program.command("shipit").description("Run composite workflow: lint \u2192 test \u2192 review \u2192 cleanup \u2192 commit").addOption(new Option("--profile <profile>", "Workflow profile").choices(["fast", "safe", "full"]).default("safe")).option("--dry-run", "Print planned steps without executing", false).option("--no-commit", "Run checks but skip commit step").option("--json", "Machine-readable JSON output", false).option("--strict-output", "Strict model output parsing", false).action(shipitCommand);
|
|
4760
|
+
program.command("cost").description("Token usage and cost dashboard").addOption(new Option("--scope <scope>", "Time scope").choices(["session", "daily", "all"]).default("daily")).option("--days <n>", "Number of days for daily scope", (v) => Number.parseInt(v, 10), 30).option("--session <id>", "Session ID for session scope").action(costCommand);
|
|
4761
|
+
program.command("watch").description("Watch files and enqueue reviews on change").option("--glob <pattern>", "Glob pattern to watch", "**/*.{ts,tsx,js,jsx}").addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("all")).option("--timeout <seconds>", "Review timeout", (v) => Number.parseInt(v, 10), 600).option("--quiet-ms <ms>", "Quiet period before flush", (v) => Number.parseInt(v, 10), 800).option("--max-wait-ms <ms>", "Max wait before forced flush", (v) => Number.parseInt(v, 10), 5e3).option("--cooldown-ms <ms>", "Cooldown after flush", (v) => Number.parseInt(v, 10), 1500).action(watchCommand);
|
|
4762
|
+
program.command("events").description("Stream session events and job logs as JSONL").option("--follow", "Follow mode \u2014 poll for new events", false).option("--since-seq <n>", "Start from sequence number", (v) => Number.parseInt(v, 10), 0).option("--limit <n>", "Max events per poll", (v) => Number.parseInt(v, 10), 100).addOption(new Option("--type <type>", "Event source filter").choices(["all", "sessions", "jobs"]).default("all")).action(eventsCommand);
|
|
4763
|
+
jobs.command("worker").description("Start background job worker (processes queued jobs)").option("--once", "Process one job and exit", false).option("--poll-ms <ms>", "Poll interval in milliseconds", (v) => Number.parseInt(v, 10), 2e3).option("--worker-id <id>", "Worker identifier", `w-${Date.now()}`).action(workerCommand);
|
|
4764
|
+
var workspace = program.command("workspace").description("Multi-project workspace management \u2014 discover and manage Mule projects");
|
|
4765
|
+
workspace.command("init").description("Initialize multi-project workspace (discover all Mule apps)").action(workspaceInitCommand);
|
|
4766
|
+
workspace.command("list").description("List all discovered Mule projects").action(workspaceListCommand);
|
|
4767
|
+
workspace.command("validate").description("Check workspace integrity and dependency consistency").action(workspaceValidateCommand);
|
|
4768
|
+
workspace.command("sync").description("Refresh project registry").action(workspaceSyncCommand);
|
|
4769
|
+
var flow = program.command("flow").description("Mule flow analysis and visualization");
|
|
4770
|
+
flow.command("visualize").description("Generate Mermaid diagram from Mule flow XML").argument("<flow-name>", "Flow name or XML file path").option("--output <file>", "Write diagram to file").option("--project <name>", "Target specific project").action(flowVisualizeCommand);
|
|
4771
|
+
flow.command("analyze").description("Flow complexity and optimization report").argument("<flow-name>", "Flow name or XML file path").option("--project <name>", "Target specific project").action(flowAnalyzeCommand);
|
|
4772
|
+
flow.command("test-gen").description("Generate MUnit test case suggestions").argument("<flow-name>", "Flow name or XML file path").option("--output <file>", "Write test suggestions to file").option("--project <name>", "Target specific project").action(flowTestGenCommand);
|
|
4773
|
+
var dataweave = program.command("dataweave").description("DataWeave transformation analysis and linting");
|
|
4774
|
+
dataweave.command("lint").description("Check DataWeave files for null-safety, type errors, anti-patterns").argument("<file-or-dir>", "DataWeave file or directory to lint").option("--project <name>", "Target specific project").action(dataWeaveLintCommand);
|
|
4775
|
+
dataweave.command("optimize").description("Suggest performance improvements for DataWeave transforms").argument("<file-or-dir>", "DataWeave file or directory to optimize").option("--project <name>", "Target specific project").action(dataWeaveOptimizeCommand);
|
|
4776
|
+
dataweave.command("test-gen").description("Generate MUnit test cases with edge cases").argument("<file-or-dir>", "DataWeave file or directory").option("--output <file>", "Write test suggestions to file").option("--project <name>", "Target specific project").action(dataWeaveTestGenCommand);
|
|
4777
|
+
var connector = program.command("connector").description("Mule connector configuration audit and management");
|
|
4778
|
+
connector.command("audit").description("Audit all connector configurations across workspace").option("--project <name>", "Target specific project").option("--json", "Machine-readable JSON output", false).action(connectorAuditCommand);
|
|
4779
|
+
connector.command("versions").description("Show connector version matrix across projects").option("--project <name>", "Target specific project").action(connectorVersionsCommand);
|
|
4780
|
+
var validate = program.command("validate").description("Scenario and spec validation \u2014 three-tier analysis");
|
|
4781
|
+
validate.command("scenario").description("Full three-tier validation (spec + docs + feasibility)").argument("<file>", "Scenario YAML/JSON file").option("--project <name>", "Target specific project").option("--json", "Machine-readable JSON output", false).action(validateScenarioCommand);
|
|
4782
|
+
validate.command("spec").description("RAML/OAS spec compliance check").argument("<file>", "Spec file path").option("--project <name>", "Target specific project").action(validateSpecCommand);
|
|
4783
|
+
validate.command("feasibility").description("Workspace feasibility analysis for a scenario").argument("<file>", "Scenario YAML/JSON file").option("--project <name>", "Target specific project").action(validateFeasibilityCommand);
|
|
4784
|
+
var design = program.command("design").description("Architecture design discussions and debates");
|
|
4785
|
+
design.command("debate").description("Start a multi-model architecture debate").argument("<topic>", "Debate topic or question").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).action(debateStartCommand);
|
|
4786
|
+
program.parse();
|
|
4787
|
+
export {
|
|
4788
|
+
program
|
|
4789
|
+
};
|
|
4790
|
+
//# sourceMappingURL=index.js.map
|