@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 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