ralph-lisa-loop 0.3.0

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.
@@ -0,0 +1,1812 @@
1
+ "use strict";
2
+ /**
3
+ * CLI commands for Ralph-Lisa Loop.
4
+ * Direct port of io.sh logic to Node/TS.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.cmdInit = cmdInit;
41
+ exports.cmdWhoseTurn = cmdWhoseTurn;
42
+ exports.cmdSubmitRalph = cmdSubmitRalph;
43
+ exports.cmdSubmitLisa = cmdSubmitLisa;
44
+ exports.cmdStatus = cmdStatus;
45
+ exports.cmdRead = cmdRead;
46
+ exports.cmdRecap = cmdRecap;
47
+ exports.cmdStep = cmdStep;
48
+ exports.cmdHistory = cmdHistory;
49
+ exports.cmdArchive = cmdArchive;
50
+ exports.cmdClean = cmdClean;
51
+ exports.cmdUninit = cmdUninit;
52
+ exports.cmdInitProject = cmdInitProject;
53
+ exports.cmdStart = cmdStart;
54
+ exports.cmdAuto = cmdAuto;
55
+ exports.cmdPolicy = cmdPolicy;
56
+ exports.cmdLogs = cmdLogs;
57
+ exports.cmdDoctor = cmdDoctor;
58
+ const fs = __importStar(require("node:fs"));
59
+ const path = __importStar(require("node:path"));
60
+ const node_child_process_1 = require("node:child_process");
61
+ const state_js_1 = require("./state.js");
62
+ const policy_js_1 = require("./policy.js");
63
+ function line(ch = "=", len = 40) {
64
+ return ch.repeat(len);
65
+ }
66
+ /**
67
+ * Resolve submission content from args, --file, or --stdin.
68
+ * Returns content and whether it came from an external source (file/stdin).
69
+ * External sources get compact history entries to reduce context bloat.
70
+ */
71
+ function resolveContent(args) {
72
+ const fileIdx = args.indexOf("--file");
73
+ if (fileIdx !== -1) {
74
+ const filePath = args[fileIdx + 1];
75
+ if (!filePath) {
76
+ console.error("Error: --file requires a file path");
77
+ process.exit(1);
78
+ }
79
+ if (!fs.existsSync(filePath)) {
80
+ console.error(`Error: File not found: ${filePath}`);
81
+ process.exit(1);
82
+ }
83
+ return { content: fs.readFileSync(filePath, "utf-8").trim(), external: true };
84
+ }
85
+ if (args.includes("--stdin")) {
86
+ try {
87
+ return { content: fs.readFileSync(0, "utf-8").trim(), external: true };
88
+ }
89
+ catch {
90
+ console.error("Error: Failed to read from stdin");
91
+ process.exit(1);
92
+ }
93
+ }
94
+ return { content: args.join(" "), external: false };
95
+ }
96
+ /**
97
+ * Get list of changed files via git diff.
98
+ * Returns empty array if not in a git repo or git fails.
99
+ */
100
+ function getFilesChanged() {
101
+ try {
102
+ const output = (0, node_child_process_1.execSync)("git diff --name-only HEAD 2>/dev/null || git diff --name-only", {
103
+ encoding: "utf-8",
104
+ stdio: ["pipe", "pipe", "pipe"],
105
+ }).trim();
106
+ if (!output)
107
+ return [];
108
+ return output.split("\n").filter(Boolean);
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ // ─── init ────────────────────────────────────────
115
+ function cmdInit(args) {
116
+ const task = args.join(" ");
117
+ if (!task) {
118
+ console.error('Usage: ralph-lisa init "task description"');
119
+ process.exit(1);
120
+ }
121
+ const dir = (0, state_js_1.stateDir)();
122
+ if (fs.existsSync(dir)) {
123
+ console.log("Warning: Existing session will be overwritten");
124
+ }
125
+ fs.rmSync(dir, { recursive: true, force: true });
126
+ fs.mkdirSync(dir, { recursive: true });
127
+ const ts = (0, state_js_1.timestamp)();
128
+ (0, state_js_1.writeFile)(path.join(dir, "task.md"), `# Task\n\n${task}\n\n---\nCreated: ${ts}\n`);
129
+ (0, state_js_1.writeFile)(path.join(dir, "round.txt"), "1");
130
+ (0, state_js_1.writeFile)(path.join(dir, "step.txt"), "planning");
131
+ (0, state_js_1.writeFile)(path.join(dir, "turn.txt"), "ralph");
132
+ (0, state_js_1.writeFile)(path.join(dir, "last_action.txt"), "(No action yet)");
133
+ (0, state_js_1.writeFile)(path.join(dir, "plan.md"), "# Plan\n\n(To be drafted by Ralph and reviewed by Lisa)\n");
134
+ (0, state_js_1.writeFile)(path.join(dir, "work.md"), "# Ralph Work\n\n(Waiting for Ralph to submit)\n");
135
+ (0, state_js_1.writeFile)(path.join(dir, "review.md"), "# Lisa Review\n\n(Waiting for Lisa to respond)\n");
136
+ (0, state_js_1.writeFile)(path.join(dir, "history.md"), `# Collaboration History\n\n**Task**: ${task}\n**Started**: ${ts}\n`);
137
+ console.log(line());
138
+ console.log("Session Initialized");
139
+ console.log(line());
140
+ console.log(`Task: ${task}`);
141
+ console.log("Turn: ralph");
142
+ console.log("");
143
+ console.log('Ralph should start with: ralph-lisa submit-ralph "[PLAN] summary..."');
144
+ console.log(line());
145
+ }
146
+ // ─── whose-turn ──────────────────────────────────
147
+ function cmdWhoseTurn() {
148
+ (0, state_js_1.checkSession)();
149
+ console.log((0, state_js_1.getTurn)());
150
+ }
151
+ // ─── submit-ralph ────────────────────────────────
152
+ function cmdSubmitRalph(args) {
153
+ (0, state_js_1.checkSession)();
154
+ const { content, external } = resolveContent(args);
155
+ if (!content) {
156
+ console.error('Usage: ralph-lisa submit-ralph "[TAG] summary\\n\\ndetails..."');
157
+ console.error(' ralph-lisa submit-ralph --file <path>');
158
+ console.error(" echo content | ralph-lisa submit-ralph --stdin");
159
+ console.error("");
160
+ console.error("Valid tags: PLAN, RESEARCH, CODE, FIX, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
161
+ process.exit(1);
162
+ }
163
+ const turn = (0, state_js_1.getTurn)();
164
+ if (turn !== "ralph") {
165
+ console.error("Error: It's Lisa's turn. Wait for her response.");
166
+ console.error("Run: ralph-lisa whose-turn");
167
+ process.exit(1);
168
+ }
169
+ const tag = (0, state_js_1.extractTag)(content);
170
+ if (!tag) {
171
+ console.error("Error: Content must start with a valid tag.");
172
+ console.error("Format: [TAG] One line summary");
173
+ console.error("");
174
+ console.error("Valid tags: PLAN, RESEARCH, CODE, FIX, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
175
+ process.exit(1);
176
+ }
177
+ // Policy check
178
+ if (!(0, policy_js_1.runPolicyCheck)("ralph", tag, content)) {
179
+ process.exit(1);
180
+ }
181
+ const round = (0, state_js_1.getRound)();
182
+ const step = (0, state_js_1.getStep)();
183
+ const ts = (0, state_js_1.timestamp)();
184
+ const summary = (0, state_js_1.extractSummary)(content);
185
+ const dir = (0, state_js_1.stateDir)();
186
+ // Auto-attach files_changed for CODE/FIX submissions
187
+ let filesChangedSection = "";
188
+ if (tag === "CODE" || tag === "FIX") {
189
+ const files = getFilesChanged();
190
+ if (files.length > 0) {
191
+ filesChangedSection = `**Files Changed**:\n${files.map((f) => `- ${f}`).join("\n")}\n\n`;
192
+ }
193
+ }
194
+ (0, state_js_1.writeFile)(path.join(dir, "work.md"), `# Ralph Work\n\n## [${tag}] Round ${round} | Step: ${step}\n**Updated**: ${ts}\n**Summary**: ${summary}\n${filesChangedSection ? "\n" + filesChangedSection : "\n"}${content}\n`);
195
+ // External sources (--file/--stdin) get compact history to reduce context bloat
196
+ const historyContent = external
197
+ ? `[${tag}] ${summary}\n\n(Full content in work.md)`
198
+ : content;
199
+ (0, state_js_1.appendHistory)("Ralph", historyContent);
200
+ (0, state_js_1.updateLastAction)("Ralph", content);
201
+ (0, state_js_1.setTurn)("lisa");
202
+ console.log(line());
203
+ console.log(`Submitted: [${tag}] ${summary}`);
204
+ console.log("Turn passed to: Lisa");
205
+ console.log(line());
206
+ console.log("");
207
+ console.log("Now wait for Lisa. Check with: ralph-lisa whose-turn");
208
+ }
209
+ // ─── submit-lisa ─────────────────────────────────
210
+ function cmdSubmitLisa(args) {
211
+ (0, state_js_1.checkSession)();
212
+ const { content, external } = resolveContent(args);
213
+ if (!content) {
214
+ console.error('Usage: ralph-lisa submit-lisa "[TAG] summary\\n\\ndetails..."');
215
+ console.error(' ralph-lisa submit-lisa --file <path>');
216
+ console.error(" echo content | ralph-lisa submit-lisa --stdin");
217
+ console.error("");
218
+ console.error("Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
219
+ process.exit(1);
220
+ }
221
+ const turn = (0, state_js_1.getTurn)();
222
+ if (turn !== "lisa") {
223
+ console.error("Error: It's Ralph's turn. Wait for his submission.");
224
+ console.error("Run: ralph-lisa whose-turn");
225
+ process.exit(1);
226
+ }
227
+ const tag = (0, state_js_1.extractTag)(content);
228
+ if (!tag) {
229
+ console.error("Error: Content must start with a valid tag.");
230
+ console.error("Format: [TAG] One line summary");
231
+ console.error("");
232
+ console.error("Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
233
+ process.exit(1);
234
+ }
235
+ // Policy check
236
+ if (!(0, policy_js_1.runPolicyCheck)("lisa", tag, content)) {
237
+ process.exit(1);
238
+ }
239
+ const round = (0, state_js_1.getRound)();
240
+ const step = (0, state_js_1.getStep)();
241
+ const ts = (0, state_js_1.timestamp)();
242
+ const summary = (0, state_js_1.extractSummary)(content);
243
+ const dir = (0, state_js_1.stateDir)();
244
+ // Append new review entry, keep last 3
245
+ const reviewPath = path.join(dir, "review.md");
246
+ const newEntry = `## [${tag}] Round ${round} | Step: ${step}\n**Updated**: ${ts}\n**Summary**: ${summary}\n\n${content}`;
247
+ const existing = (0, state_js_1.readFile)(reviewPath);
248
+ // Split existing entries by separator, filter out header/empty
249
+ const REVIEW_SEP = "\n\n---\n\n";
250
+ let entries = [];
251
+ if (existing && !existing.startsWith("# Lisa Review\n\n(Waiting")) {
252
+ // Remove the "# Lisa Review" header if present
253
+ const body = existing.replace(/^# Lisa Review\n\n/, "");
254
+ entries = body.split(REVIEW_SEP).filter((e) => e.trim());
255
+ }
256
+ entries.push(newEntry);
257
+ // Keep only last 3
258
+ if (entries.length > 3) {
259
+ entries = entries.slice(-3);
260
+ }
261
+ (0, state_js_1.writeFile)(reviewPath, `# Lisa Review\n\n${entries.join(REVIEW_SEP)}\n`);
262
+ // External sources (--file/--stdin) get compact history to reduce context bloat
263
+ const historyContent = external
264
+ ? `[${tag}] ${summary}\n\n(Full content in review.md)`
265
+ : content;
266
+ (0, state_js_1.appendHistory)("Lisa", historyContent);
267
+ (0, state_js_1.updateLastAction)("Lisa", content);
268
+ (0, state_js_1.setTurn)("ralph");
269
+ // Increment round
270
+ const nextRound = (parseInt(round, 10) || 0) + 1;
271
+ (0, state_js_1.setRound)(nextRound);
272
+ console.log(line());
273
+ console.log(`Submitted: [${tag}] ${summary}`);
274
+ console.log("Turn passed to: Ralph");
275
+ console.log(`Round: ${round} -> ${nextRound}`);
276
+ console.log(line());
277
+ console.log("");
278
+ console.log("Now wait for Ralph. Check with: ralph-lisa whose-turn");
279
+ }
280
+ // ─── status ──────────────────────────────────────
281
+ function cmdStatus() {
282
+ const dir = (0, state_js_1.stateDir)();
283
+ if (!fs.existsSync(dir)) {
284
+ console.log("Status: Not initialized");
285
+ return;
286
+ }
287
+ const turn = (0, state_js_1.getTurn)();
288
+ const round = (0, state_js_1.getRound)();
289
+ const step = (0, state_js_1.getStep)();
290
+ const last = (0, state_js_1.readFile)(path.join(dir, "last_action.txt")) || "None";
291
+ const taskFile = (0, state_js_1.readFile)(path.join(dir, "task.md"));
292
+ const taskLine = taskFile.split("\n")[2] || "Unknown";
293
+ console.log(line());
294
+ console.log("Ralph Lisa Dual-Agent Loop");
295
+ console.log(line());
296
+ console.log(`Task: ${taskLine}`);
297
+ console.log(`Round: ${round} | Step: ${step}`);
298
+ console.log("");
299
+ console.log(`>>> Turn: ${turn} <<<`);
300
+ console.log(`Last: ${last}`);
301
+ console.log(line());
302
+ }
303
+ // ─── read ────────────────────────────────────────
304
+ function cmdRead(args) {
305
+ (0, state_js_1.checkSession)();
306
+ const file = args[0];
307
+ if (!file) {
308
+ console.error("Usage: ralph-lisa read <file>");
309
+ console.error(" work.md - Ralph's work");
310
+ console.error(" review.md - Lisa's feedback (last 3)");
311
+ console.error(" review --round N - Lisa's review from round N (from history)");
312
+ process.exit(1);
313
+ }
314
+ // Handle: ralph-lisa read review --round N
315
+ const roundIdx = args.indexOf("--round");
316
+ if ((file === "review" || file === "review.md") && roundIdx !== -1) {
317
+ const roundStr = args[roundIdx + 1];
318
+ const roundNum = parseInt(roundStr, 10);
319
+ if (!roundStr || isNaN(roundNum) || roundNum < 1) {
320
+ console.error("Error: --round requires a positive integer");
321
+ process.exit(1);
322
+ }
323
+ const review = extractReviewByRound(roundNum);
324
+ if (review) {
325
+ console.log(review);
326
+ }
327
+ else {
328
+ console.log(`No review found for round ${roundNum}`);
329
+ }
330
+ return;
331
+ }
332
+ const filePath = path.join((0, state_js_1.stateDir)(), file);
333
+ if (fs.existsSync(filePath)) {
334
+ console.log(fs.readFileSync(filePath, "utf-8"));
335
+ }
336
+ else {
337
+ console.log(`(File ${file} does not exist)`);
338
+ }
339
+ }
340
+ /**
341
+ * Extract Lisa's review for a specific round from history.md.
342
+ */
343
+ function extractReviewByRound(round) {
344
+ const dir = (0, state_js_1.stateDir)();
345
+ const history = (0, state_js_1.readFile)(path.join(dir, "history.md"));
346
+ if (!history)
347
+ return null;
348
+ // Find Lisa's entry for the given round
349
+ const entryRe = new RegExp(`## \\[Lisa\\] \\[\\w+\\] Round ${round} \\| Step: .+`, "m");
350
+ const match = entryRe.exec(history);
351
+ if (!match)
352
+ return null;
353
+ // Extract from this header to the next entry separator (--- or end)
354
+ const start = match.index;
355
+ const rest = history.slice(start);
356
+ const nextSep = rest.indexOf("\n---\n");
357
+ const entry = nextSep !== -1 ? rest.slice(0, nextSep) : rest;
358
+ return entry.trim();
359
+ }
360
+ // ─── recap ───────────────────────────────────────
361
+ function cmdRecap() {
362
+ (0, state_js_1.checkSession)();
363
+ const dir = (0, state_js_1.stateDir)();
364
+ const step = (0, state_js_1.getStep)();
365
+ const round = (0, state_js_1.getRound)();
366
+ const turn = (0, state_js_1.getTurn)();
367
+ const history = (0, state_js_1.readFile)(path.join(dir, "history.md"));
368
+ if (!history) {
369
+ console.log("No history to recap.");
370
+ return;
371
+ }
372
+ // Find the current step section in history
373
+ const stepHeaderRe = new RegExp(`^# Step: ${step.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
374
+ const stepMatch = stepHeaderRe.exec(history);
375
+ const stepSection = stepMatch
376
+ ? history.slice(stepMatch.index)
377
+ : history; // If no step header found, use full history
378
+ // Extract all action entries from the current step section
379
+ const entryRe = /^## \[(Ralph|Lisa)\] \[(\w+)\] Round (\d+) \| Step: .+\n\*\*Time\*\*: .+\n\*\*Summary\*\*: (.+)/gm;
380
+ const entries = [];
381
+ let match;
382
+ while ((match = entryRe.exec(stepSection)) !== null) {
383
+ entries.push({
384
+ role: match[1],
385
+ tag: match[2],
386
+ round: match[3],
387
+ summary: match[4],
388
+ });
389
+ }
390
+ // Find unresolved NEEDS_WORK items (NEEDS_WORK from Lisa not followed by FIX/CHALLENGE from Ralph)
391
+ const unresolvedNeedsWork = [];
392
+ for (let i = 0; i < entries.length; i++) {
393
+ const e = entries[i];
394
+ if (e.role === "Lisa" && e.tag === "NEEDS_WORK") {
395
+ // Check if Ralph responded with FIX or CHALLENGE after this
396
+ const resolved = entries
397
+ .slice(i + 1)
398
+ .some((later) => later.role === "Ralph" &&
399
+ (later.tag === "FIX" || later.tag === "CHALLENGE"));
400
+ if (!resolved) {
401
+ unresolvedNeedsWork.push(e.summary);
402
+ }
403
+ }
404
+ }
405
+ // Output recap
406
+ console.log(line());
407
+ console.log("RECAP — Context Recovery");
408
+ console.log(line());
409
+ console.log(`Step: ${step}`);
410
+ console.log(`Round: ${round} | Turn: ${turn}`);
411
+ console.log(`Actions in this step: ${entries.length}`);
412
+ console.log("");
413
+ // Last 3 actions
414
+ const recent = entries.slice(-3);
415
+ if (recent.length > 0) {
416
+ console.log("Recent actions:");
417
+ for (const e of recent) {
418
+ console.log(` R${e.round} ${e.role} [${e.tag}] ${e.summary}`);
419
+ }
420
+ }
421
+ else {
422
+ console.log("Recent actions: (none)");
423
+ }
424
+ // Unresolved NEEDS_WORK
425
+ if (unresolvedNeedsWork.length > 0) {
426
+ console.log("");
427
+ console.log("Unresolved NEEDS_WORK:");
428
+ for (const nw of unresolvedNeedsWork) {
429
+ console.log(` - ${nw}`);
430
+ }
431
+ }
432
+ console.log(line());
433
+ }
434
+ /**
435
+ * Extract the last tag from a work.md or review.md file content.
436
+ * Only matches the canonical metadata header format: ## [TAG] Round N | Step: ...
437
+ * Does NOT match arbitrary ## [TAG] headings in body text.
438
+ */
439
+ function extractLastTag(fileContent) {
440
+ const re = /^## \[(\w+)\] Round \d+ \| Step: /gm;
441
+ let lastTag = "";
442
+ let match;
443
+ while ((match = re.exec(fileContent)) !== null) {
444
+ lastTag = match[1];
445
+ }
446
+ return lastTag;
447
+ }
448
+ // ─── step ────────────────────────────────────────
449
+ function cmdStep(args) {
450
+ (0, state_js_1.checkSession)();
451
+ // Parse --force flag
452
+ const forceIdx = args.indexOf("--force");
453
+ const force = forceIdx !== -1;
454
+ const filteredArgs = force
455
+ ? args.filter((_, i) => i !== forceIdx)
456
+ : args;
457
+ const stepName = filteredArgs.join(" ");
458
+ if (!stepName) {
459
+ console.error('Usage: ralph-lisa step "step name"');
460
+ console.error(" ralph-lisa step --force \"step name\" (skip consensus check)");
461
+ process.exit(1);
462
+ }
463
+ // Check consensus before allowing step transition
464
+ if (!force) {
465
+ const dir = (0, state_js_1.stateDir)();
466
+ const workContent = (0, state_js_1.readFile)(path.join(dir, "work.md"));
467
+ const reviewContent = (0, state_js_1.readFile)(path.join(dir, "review.md"));
468
+ const workTag = extractLastTag(workContent);
469
+ const reviewTag = extractLastTag(reviewContent);
470
+ const consensusReached = (workTag === "CONSENSUS" && reviewTag === "CONSENSUS") ||
471
+ (workTag === "CONSENSUS" && reviewTag === "PASS") ||
472
+ (workTag === "PASS" && reviewTag === "CONSENSUS");
473
+ if (!consensusReached) {
474
+ console.error("Error: Consensus not reached. Cannot proceed to next step.");
475
+ console.error(` Ralph's last tag: [${workTag || "none"}]`);
476
+ console.error(` Lisa's last tag: [${reviewTag || "none"}]`);
477
+ console.error("");
478
+ console.error("Required: both [CONSENSUS], or [PASS]+[CONSENSUS] combination.");
479
+ console.error('Use --force to skip this check: ralph-lisa step --force "step name"');
480
+ process.exit(1);
481
+ }
482
+ }
483
+ (0, state_js_1.setStep)(stepName);
484
+ (0, state_js_1.setRound)(1);
485
+ const dir = (0, state_js_1.stateDir)();
486
+ const ts = (0, state_js_1.timestamp)();
487
+ const entry = `\n---\n\n# Step: ${stepName}\n\nStarted: ${ts}\n\n`;
488
+ fs.appendFileSync(path.join(dir, "history.md"), entry, "utf-8");
489
+ console.log(`Entered step: ${stepName} (round reset to 1)`);
490
+ }
491
+ // ─── history ─────────────────────────────────────
492
+ function cmdHistory() {
493
+ (0, state_js_1.checkSession)();
494
+ const filePath = path.join((0, state_js_1.stateDir)(), "history.md");
495
+ if (fs.existsSync(filePath)) {
496
+ console.log(fs.readFileSync(filePath, "utf-8"));
497
+ }
498
+ }
499
+ // ─── archive ─────────────────────────────────────
500
+ function cmdArchive(args) {
501
+ (0, state_js_1.checkSession)();
502
+ const name = args[0] || new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
503
+ const archiveDir = path.join(process.cwd(), state_js_1.ARCHIVE_DIR);
504
+ const dest = path.join(archiveDir, name);
505
+ fs.mkdirSync(dest, { recursive: true });
506
+ fs.cpSync((0, state_js_1.stateDir)(), dest, { recursive: true });
507
+ console.log(`Archived: ${state_js_1.ARCHIVE_DIR}/${name}/`);
508
+ }
509
+ // ─── clean ───────────────────────────────────────
510
+ function cmdClean() {
511
+ const dir = (0, state_js_1.stateDir)();
512
+ if (fs.existsSync(dir)) {
513
+ fs.rmSync(dir, { recursive: true, force: true });
514
+ console.log("Session cleaned");
515
+ }
516
+ }
517
+ // ─── uninit ──────────────────────────────────────
518
+ const MARKER = "RALPH-LISA-LOOP";
519
+ function cmdUninit() {
520
+ const projectDir = process.cwd();
521
+ // Remove .dual-agent/
522
+ const dualAgentDir = path.join(projectDir, state_js_1.STATE_DIR);
523
+ if (fs.existsSync(dualAgentDir)) {
524
+ fs.rmSync(dualAgentDir, { recursive: true, force: true });
525
+ console.log("Removed: .dual-agent/");
526
+ }
527
+ // Clean CODEX.md marker block (same logic as CLAUDE.md — preserve pre-existing content)
528
+ const codexMd = path.join(projectDir, "CODEX.md");
529
+ if (fs.existsSync(codexMd)) {
530
+ const content = fs.readFileSync(codexMd, "utf-8");
531
+ if (content.includes(MARKER)) {
532
+ const markerIdx = content.indexOf(`<!-- ${MARKER} -->`);
533
+ if (markerIdx >= 0) {
534
+ const before = content.slice(0, markerIdx).trimEnd();
535
+ if (before) {
536
+ fs.writeFileSync(codexMd, before + "\n", "utf-8");
537
+ console.log("Cleaned: CODEX.md (removed Ralph-Lisa-Loop section)");
538
+ }
539
+ else {
540
+ fs.unlinkSync(codexMd);
541
+ console.log("Removed: CODEX.md (was entirely Ralph-Lisa-Loop content)");
542
+ }
543
+ }
544
+ }
545
+ }
546
+ // Clean CLAUDE.md marker block
547
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
548
+ if (fs.existsSync(claudeMd)) {
549
+ const content = fs.readFileSync(claudeMd, "utf-8");
550
+ if (content.includes(MARKER)) {
551
+ // Remove everything from <!-- RALPH-LISA-LOOP --> to end of file
552
+ // or to next <!-- end --> marker
553
+ const markerIdx = content.indexOf(`<!-- ${MARKER} -->`);
554
+ if (markerIdx >= 0) {
555
+ const before = content.slice(0, markerIdx).trimEnd();
556
+ if (before) {
557
+ fs.writeFileSync(claudeMd, before + "\n", "utf-8");
558
+ console.log("Cleaned: CLAUDE.md (removed Ralph-Lisa-Loop section)");
559
+ }
560
+ else {
561
+ fs.unlinkSync(claudeMd);
562
+ console.log("Removed: CLAUDE.md (was entirely Ralph-Lisa-Loop content)");
563
+ }
564
+ }
565
+ }
566
+ }
567
+ // Remove .claude/commands/ (only our files)
568
+ const claudeCmdDir = path.join(projectDir, ".claude", "commands");
569
+ const ourCommands = [
570
+ "check-turn.md",
571
+ "next-step.md",
572
+ "read-review.md",
573
+ "submit-work.md",
574
+ "view-status.md",
575
+ ];
576
+ if (fs.existsSync(claudeCmdDir)) {
577
+ for (const cmd of ourCommands) {
578
+ const cmdPath = path.join(claudeCmdDir, cmd);
579
+ if (fs.existsSync(cmdPath)) {
580
+ fs.unlinkSync(cmdPath);
581
+ }
582
+ }
583
+ // Remove directory if empty
584
+ try {
585
+ const remaining = fs.readdirSync(claudeCmdDir);
586
+ if (remaining.length === 0) {
587
+ fs.rmdirSync(claudeCmdDir);
588
+ // Also remove .claude/ if empty
589
+ const claudeDir = path.join(projectDir, ".claude");
590
+ const claudeRemaining = fs.readdirSync(claudeDir);
591
+ if (claudeRemaining.length === 0) {
592
+ fs.rmdirSync(claudeDir);
593
+ }
594
+ }
595
+ }
596
+ catch {
597
+ // ignore
598
+ }
599
+ console.log("Cleaned: .claude/commands/");
600
+ }
601
+ // Remove only our skill from .codex/ (preserve other content)
602
+ const codexSkillDir = path.join(projectDir, ".codex", "skills", "ralph-lisa-loop");
603
+ if (fs.existsSync(codexSkillDir)) {
604
+ fs.rmSync(codexSkillDir, { recursive: true, force: true });
605
+ console.log("Removed: .codex/skills/ralph-lisa-loop/");
606
+ // Clean up empty parent dirs
607
+ try {
608
+ const skillsDir = path.join(projectDir, ".codex", "skills");
609
+ if (fs.readdirSync(skillsDir).length === 0) {
610
+ fs.rmdirSync(skillsDir);
611
+ }
612
+ }
613
+ catch {
614
+ // ignore
615
+ }
616
+ }
617
+ // Remove .codex/config.toml only if it has our marker
618
+ const codexConfig = path.join(projectDir, ".codex", "config.toml");
619
+ if (fs.existsSync(codexConfig)) {
620
+ const configContent = fs.readFileSync(codexConfig, "utf-8");
621
+ if (configContent.includes(MARKER)) {
622
+ fs.unlinkSync(codexConfig);
623
+ console.log("Removed: .codex/config.toml");
624
+ }
625
+ }
626
+ // Remove .codex/ if empty
627
+ try {
628
+ const codexDir = path.join(projectDir, ".codex");
629
+ if (fs.existsSync(codexDir) && fs.readdirSync(codexDir).length === 0) {
630
+ fs.rmdirSync(codexDir);
631
+ console.log("Removed: .codex/ (empty)");
632
+ }
633
+ }
634
+ catch {
635
+ // ignore
636
+ }
637
+ // Remove io.sh if it exists
638
+ const ioSh = path.join(projectDir, "io.sh");
639
+ if (fs.existsSync(ioSh)) {
640
+ fs.unlinkSync(ioSh);
641
+ console.log("Removed: io.sh");
642
+ }
643
+ console.log("");
644
+ console.log("Ralph-Lisa Loop removed from this project.");
645
+ }
646
+ // ─── init (project setup) ────────────────────────
647
+ function cmdInitProject(args) {
648
+ // Parse --minimal flag
649
+ const minimal = args.includes("--minimal");
650
+ const filteredArgs = args.filter((a) => a !== "--minimal");
651
+ const projectDir = filteredArgs[0] || process.cwd();
652
+ const resolvedDir = path.resolve(projectDir);
653
+ if (!fs.existsSync(resolvedDir)) {
654
+ console.error(`Error: Directory does not exist: ${resolvedDir}`);
655
+ process.exit(1);
656
+ }
657
+ console.log(line());
658
+ console.log(`Ralph-Lisa Loop - Init${minimal ? " (minimal)" : ""}`);
659
+ console.log(line());
660
+ console.log(`Project: ${resolvedDir}`);
661
+ console.log("");
662
+ if (minimal) {
663
+ // Minimal mode: only create .dual-agent/ session state.
664
+ // Use this when Claude Code plugin + Codex global config are installed.
665
+ console.log("[Session] Initializing .dual-agent/ (minimal mode)...");
666
+ const origCwd = process.cwd();
667
+ process.chdir(resolvedDir);
668
+ cmdInit(["Waiting for task assignment"]);
669
+ process.chdir(origCwd);
670
+ console.log("");
671
+ console.log(line());
672
+ console.log("Minimal Init Complete");
673
+ console.log(line());
674
+ console.log("");
675
+ console.log("Files created:");
676
+ console.log(" - .dual-agent/ (session state only)");
677
+ console.log("");
678
+ console.log("No project-level role/command files written.");
679
+ console.log("Requires: Claude Code plugin + Codex global config.");
680
+ console.log(line());
681
+ return;
682
+ }
683
+ // Find templates directory (shipped inside npm package)
684
+ const templatesDir = findTemplatesDir();
685
+ // 1. Append Ralph role to CLAUDE.md
686
+ const claudeMd = path.join(resolvedDir, "CLAUDE.md");
687
+ if (fs.existsSync(claudeMd) && (0, state_js_1.readFile)(claudeMd).includes(MARKER)) {
688
+ console.log("[Claude] Ralph role already in CLAUDE.md, skipping...");
689
+ }
690
+ else {
691
+ console.log("[Claude] Appending Ralph role to CLAUDE.md...");
692
+ const ralphRole = (0, state_js_1.readFile)(path.join(templatesDir, "roles", "ralph.md"));
693
+ if (fs.existsSync(claudeMd)) {
694
+ fs.appendFileSync(claudeMd, "\n\n", "utf-8");
695
+ }
696
+ fs.appendFileSync(claudeMd, ralphRole, "utf-8");
697
+ console.log("[Claude] Done.");
698
+ }
699
+ // 2. Create/update CODEX.md with Lisa role
700
+ const codexMd = path.join(resolvedDir, "CODEX.md");
701
+ if (fs.existsSync(codexMd) && (0, state_js_1.readFile)(codexMd).includes(MARKER)) {
702
+ console.log("[Codex] Lisa role already in CODEX.md, skipping...");
703
+ }
704
+ else {
705
+ console.log("[Codex] Creating CODEX.md with Lisa role...");
706
+ const lisaRole = (0, state_js_1.readFile)(path.join(templatesDir, "roles", "lisa.md"));
707
+ if (fs.existsSync(codexMd)) {
708
+ fs.appendFileSync(codexMd, "\n\n", "utf-8");
709
+ }
710
+ fs.appendFileSync(codexMd, lisaRole, "utf-8");
711
+ console.log("[Codex] Done.");
712
+ }
713
+ // 3. Copy Claude commands
714
+ console.log("[Claude] Copying commands to .claude/commands/...");
715
+ const claudeCmdDir = path.join(resolvedDir, ".claude", "commands");
716
+ fs.mkdirSync(claudeCmdDir, { recursive: true });
717
+ const cmdSrc = path.join(templatesDir, "claude-commands");
718
+ if (fs.existsSync(cmdSrc)) {
719
+ for (const f of fs.readdirSync(cmdSrc)) {
720
+ if (f.endsWith(".md")) {
721
+ fs.copyFileSync(path.join(cmdSrc, f), path.join(claudeCmdDir, f));
722
+ }
723
+ }
724
+ }
725
+ console.log("[Claude] Commands copied.");
726
+ // 4. Copy Codex skills
727
+ console.log("[Codex] Setting up skills in .codex/skills/ralph-lisa-loop/...");
728
+ const codexSkillDir = path.join(resolvedDir, ".codex", "skills", "ralph-lisa-loop");
729
+ fs.mkdirSync(codexSkillDir, { recursive: true });
730
+ const skillContent = `---
731
+ name: ralph-lisa-loop
732
+ description: Lisa review commands for Ralph-Lisa dual-agent collaboration
733
+ ---
734
+
735
+ # Ralph-Lisa Loop - Lisa Skills
736
+
737
+ This skill provides Lisa's review commands for the Ralph-Lisa collaboration.
738
+
739
+ ## Available Commands
740
+
741
+ ### Check Turn
742
+ \`\`\`bash
743
+ ralph-lisa whose-turn
744
+ \`\`\`
745
+ Check if it's your turn before taking action.
746
+
747
+ ### Submit Review
748
+ \`\`\`bash
749
+ ralph-lisa submit-lisa "[TAG] summary
750
+
751
+ detailed content..."
752
+ \`\`\`
753
+ Submit your review. Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS
754
+
755
+ ### View Status
756
+ \`\`\`bash
757
+ ralph-lisa status
758
+ \`\`\`
759
+ View current task, turn, and last action.
760
+
761
+ ### Read Ralph's Work
762
+ \`\`\`bash
763
+ ralph-lisa read work.md
764
+ \`\`\`
765
+ Read Ralph's latest submission.
766
+ `;
767
+ (0, state_js_1.writeFile)(path.join(codexSkillDir, "SKILL.md"), skillContent);
768
+ // Create .codex/config.toml (with marker for safe uninit)
769
+ // Codex reads AGENTS.md by default; fallback to CODEX.md for our setup
770
+ const codexConfig = `# ${MARKER} - managed by ralph-lisa-loop
771
+ project_doc_fallback_filenames = ["CODEX.md"]
772
+
773
+ [skills]
774
+ enabled = true
775
+ path = ".codex/skills"
776
+ `;
777
+ (0, state_js_1.writeFile)(path.join(resolvedDir, ".codex", "config.toml"), codexConfig);
778
+ console.log(`[Codex] Skill created at ${codexSkillDir}/`);
779
+ console.log(`[Codex] Config created at ${path.join(resolvedDir, ".codex", "config.toml")}`);
780
+ // 5. Initialize session state
781
+ console.log("[Session] Initializing .dual-agent/...");
782
+ const origCwd = process.cwd();
783
+ process.chdir(resolvedDir);
784
+ cmdInit(["Waiting for task assignment"]);
785
+ process.chdir(origCwd);
786
+ console.log("");
787
+ console.log(line());
788
+ console.log("Initialization Complete");
789
+ console.log(line());
790
+ console.log("");
791
+ console.log("Files created/updated:");
792
+ console.log(" - CLAUDE.md (Ralph role)");
793
+ console.log(" - CODEX.md (Lisa role)");
794
+ console.log(" - .claude/commands/ (Claude slash commands)");
795
+ console.log(" - .codex/skills/ (Codex skills)");
796
+ console.log(" - .dual-agent/");
797
+ console.log("");
798
+ console.log("Start agents:");
799
+ console.log(" Terminal 1: claude");
800
+ console.log(" Terminal 2: codex");
801
+ console.log("");
802
+ console.log('Or run: ralph-lisa start "your task"');
803
+ console.log(line());
804
+ }
805
+ function findTemplatesDir() {
806
+ // Look for templates relative to the CLI package
807
+ const candidates = [
808
+ // When installed via npm (templates shipped in package)
809
+ path.join(__dirname, "..", "templates"),
810
+ // When running from repo
811
+ path.join(__dirname, "..", "..", "templates"),
812
+ // Repo root
813
+ path.join(__dirname, "..", "..", "..", "templates"),
814
+ ];
815
+ for (const c of candidates) {
816
+ if (fs.existsSync(path.join(c, "roles", "ralph.md"))) {
817
+ return c;
818
+ }
819
+ }
820
+ console.error("Error: Templates directory not found. Reinstall ralph-lisa-loop.");
821
+ process.exit(1);
822
+ }
823
+ // ─── start ───────────────────────────────────────
824
+ function cmdStart(args) {
825
+ const projectDir = process.cwd();
826
+ const fullAuto = args.includes("--full-auto");
827
+ const filteredArgs = args.filter((a) => a !== "--full-auto");
828
+ const task = filteredArgs.join(" ");
829
+ const claudeCmd = fullAuto ? "claude --dangerously-skip-permissions" : "claude";
830
+ const codexCmd = fullAuto ? "codex --full-auto" : "codex";
831
+ console.log(line());
832
+ console.log("Ralph-Lisa Loop - Start");
833
+ console.log(line());
834
+ console.log(`Project: ${projectDir}`);
835
+ if (fullAuto)
836
+ console.log("Mode: FULL AUTO (no permission prompts)");
837
+ console.log("");
838
+ // Check prerequisites
839
+ const { execSync } = require("node:child_process");
840
+ try {
841
+ execSync("which claude", { stdio: "pipe" });
842
+ }
843
+ catch {
844
+ console.error("Error: 'claude' command not found. Install Claude Code first.");
845
+ process.exit(1);
846
+ }
847
+ try {
848
+ execSync("which codex", { stdio: "pipe" });
849
+ }
850
+ catch {
851
+ console.error("Error: 'codex' command not found. Install Codex CLI first.");
852
+ process.exit(1);
853
+ }
854
+ // Check if initialized (full init has CLAUDE.md marker, minimal has .dual-agent/)
855
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
856
+ const hasFullInit = fs.existsSync(claudeMd) && (0, state_js_1.readFile)(claudeMd).includes(MARKER);
857
+ const hasSession = fs.existsSync(path.join(projectDir, state_js_1.STATE_DIR));
858
+ if (!hasFullInit && !hasSession) {
859
+ console.error("Error: Not initialized. Run 'ralph-lisa init' first.");
860
+ process.exit(1);
861
+ }
862
+ // Initialize task if provided
863
+ if (task) {
864
+ console.log(`Task: ${task}`);
865
+ cmdInit(task.split(" "));
866
+ console.log("");
867
+ }
868
+ // Detect terminal and launch
869
+ const platform = process.platform;
870
+ const ralphCmd = `cd '${projectDir}' && echo '=== Ralph (Claude Code) ===' && echo 'Commands: /check-turn, /submit-work, /view-status' && echo 'First: /check-turn' && echo '' && ${claudeCmd}`;
871
+ const lisaCmd = `cd '${projectDir}' && echo '=== Lisa (Codex) ===' && echo 'First: ralph-lisa whose-turn' && echo '' && ${codexCmd}`;
872
+ if (platform === "darwin") {
873
+ try {
874
+ // Try iTerm2 first
875
+ execSync("pgrep -x iTerm2", { stdio: "pipe" });
876
+ console.log("Launching with iTerm2...");
877
+ execSync(`osascript -e 'tell application "iTerm"
878
+ activate
879
+ set ralphWindow to (create window with default profile)
880
+ tell current session of ralphWindow
881
+ write text "${ralphCmd.replace(/"/g, '\\"')}"
882
+ set name to "Ralph (Claude)"
883
+ end tell
884
+ tell current window
885
+ set lisaTab to (create tab with default profile)
886
+ tell current session of lisaTab
887
+ write text "${lisaCmd.replace(/"/g, '\\"')}"
888
+ set name to "Lisa (Codex)"
889
+ end tell
890
+ end tell
891
+ end tell'`, { stdio: "pipe" });
892
+ }
893
+ catch {
894
+ // Fall back to Terminal.app
895
+ console.log("Launching with macOS Terminal...");
896
+ try {
897
+ execSync(`osascript -e 'tell application "Terminal"
898
+ activate
899
+ do script "${ralphCmd.replace(/"/g, '\\"')}"
900
+ end tell'`, { stdio: "pipe" });
901
+ execSync("sleep 1");
902
+ execSync(`osascript -e 'tell application "Terminal"
903
+ activate
904
+ do script "${lisaCmd.replace(/"/g, '\\"')}"
905
+ end tell'`, { stdio: "pipe" });
906
+ }
907
+ catch {
908
+ launchGeneric(projectDir);
909
+ return;
910
+ }
911
+ }
912
+ }
913
+ else {
914
+ // Try tmux
915
+ try {
916
+ execSync("which tmux", { stdio: "pipe" });
917
+ console.log("Launching with tmux...");
918
+ const sessionName = "ralph-lisa";
919
+ execSync(`tmux kill-session -t "${sessionName}" 2>/dev/null || true`);
920
+ execSync(`tmux new-session -d -s "${sessionName}" -n "Ralph" "bash -c '${ralphCmd}; exec bash'"`);
921
+ execSync(`tmux split-window -h -t "${sessionName}" "bash -c '${lisaCmd}; exec bash'"`);
922
+ execSync(`tmux attach-session -t "${sessionName}"`, { stdio: "inherit" });
923
+ }
924
+ catch {
925
+ launchGeneric(projectDir);
926
+ return;
927
+ }
928
+ }
929
+ console.log("");
930
+ console.log(line());
931
+ console.log("Both agents launched!");
932
+ console.log(line());
933
+ const currentTurn = (0, state_js_1.readFile)(path.join(projectDir, state_js_1.STATE_DIR, "turn.txt")) || "ralph";
934
+ console.log(`Current turn: ${currentTurn}`);
935
+ console.log(line());
936
+ }
937
+ function launchGeneric(projectDir) {
938
+ console.log("Please manually open two terminals:");
939
+ console.log("");
940
+ console.log("Terminal 1 (Ralph):");
941
+ console.log(` cd ${projectDir} && claude`);
942
+ console.log("");
943
+ console.log("Terminal 2 (Lisa):");
944
+ console.log(` cd ${projectDir} && codex`);
945
+ }
946
+ // ─── auto ────────────────────────────────────────
947
+ function cmdAuto(args) {
948
+ const projectDir = process.cwd();
949
+ const fullAuto = args.includes("--full-auto");
950
+ const filteredArgs = args.filter((a) => a !== "--full-auto");
951
+ const task = filteredArgs.join(" ");
952
+ const claudeCmd = fullAuto ? "claude --dangerously-skip-permissions" : "claude";
953
+ const codexCmd = fullAuto ? "codex --full-auto" : "codex";
954
+ console.log(line());
955
+ console.log("Ralph-Lisa Loop - Auto Mode");
956
+ console.log(line());
957
+ console.log(`Project: ${projectDir}`);
958
+ if (fullAuto)
959
+ console.log("Mode: FULL AUTO (no permission prompts)");
960
+ console.log("");
961
+ const { execSync } = require("node:child_process");
962
+ // Check prerequisites
963
+ try {
964
+ execSync("which tmux", { stdio: "pipe" });
965
+ }
966
+ catch {
967
+ console.error("Error: tmux is required for auto mode.");
968
+ console.error("Install: brew install tmux (macOS) or apt install tmux (Linux)");
969
+ process.exit(1);
970
+ }
971
+ try {
972
+ execSync("which claude", { stdio: "pipe" });
973
+ }
974
+ catch {
975
+ console.error("Error: 'claude' (Claude Code CLI) not found in PATH.");
976
+ console.error("");
977
+ console.error("Install: npm install -g @anthropic-ai/claude-code");
978
+ process.exit(1);
979
+ }
980
+ try {
981
+ execSync("which codex", { stdio: "pipe" });
982
+ }
983
+ catch {
984
+ console.error("Error: 'codex' (OpenAI Codex CLI) not found in PATH.");
985
+ console.error("");
986
+ console.error("Install: npm install -g @openai/codex");
987
+ process.exit(1);
988
+ }
989
+ // Check file watcher (optional - falls back to polling)
990
+ let watcher = "";
991
+ try {
992
+ execSync("which fswatch", { stdio: "pipe" });
993
+ watcher = "fswatch";
994
+ }
995
+ catch {
996
+ try {
997
+ execSync("which inotifywait", { stdio: "pipe" });
998
+ watcher = "inotifywait";
999
+ }
1000
+ catch {
1001
+ console.log("Note: No file watcher found (fswatch/inotifywait). Using polling mode.");
1002
+ console.log(" Install for faster turn detection: brew install fswatch (macOS) or apt install inotify-tools (Linux)");
1003
+ console.log("");
1004
+ }
1005
+ }
1006
+ // Check if initialized (full init has CLAUDE.md marker, minimal has .dual-agent/)
1007
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
1008
+ const hasFullInit = fs.existsSync(claudeMd) && (0, state_js_1.readFile)(claudeMd).includes(MARKER);
1009
+ const hasSession = fs.existsSync(path.join(projectDir, state_js_1.STATE_DIR));
1010
+ if (!hasFullInit && !hasSession) {
1011
+ console.error("Error: Not initialized. Run 'ralph-lisa init' first.");
1012
+ process.exit(1);
1013
+ }
1014
+ // Initialize task
1015
+ if (task) {
1016
+ console.log(`Task: ${task}`);
1017
+ cmdInit(task.split(" "));
1018
+ console.log("");
1019
+ }
1020
+ const sessionName = "ralph-lisa-auto";
1021
+ const dir = (0, state_js_1.stateDir)(projectDir);
1022
+ fs.mkdirSync(dir, { recursive: true });
1023
+ // Archive pane logs from previous runs (for transcript preservation)
1024
+ const logsDir = path.join(dir, "logs");
1025
+ fs.mkdirSync(logsDir, { recursive: true });
1026
+ for (const f of ["pane0.log", "pane1.log"]) {
1027
+ const src = path.join(dir, f);
1028
+ if (fs.existsSync(src) && fs.statSync(src).size > 0) {
1029
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1030
+ fs.renameSync(src, path.join(logsDir, `${f.replace(".log", "")}-${ts}.log`));
1031
+ }
1032
+ else {
1033
+ try {
1034
+ fs.unlinkSync(src);
1035
+ }
1036
+ catch { }
1037
+ }
1038
+ }
1039
+ // Clean event accelerator flag
1040
+ try {
1041
+ fs.unlinkSync(path.join(dir, ".turn_changed"));
1042
+ }
1043
+ catch { }
1044
+ // Create watcher script
1045
+ const watcherScript = path.join(dir, "watcher.sh");
1046
+ let watcherContent = `#!/bin/bash
1047
+ # Turn watcher v2 - reliable agent triggering with health checks
1048
+ # Architecture: polling main loop + optional event acceleration
1049
+
1050
+ STATE_DIR=".dual-agent"
1051
+ SESSION="${sessionName}"
1052
+ SCRIPT_PATH="\$(cd "\$(dirname "\$0")" && pwd)/watcher.sh"
1053
+ SEEN_TURN=""
1054
+ ACKED_TURN=""
1055
+ FAIL_COUNT=0
1056
+ ACCEL_PID=""
1057
+
1058
+ PANE0_LOG="\${STATE_DIR}/pane0.log"
1059
+ PANE1_LOG="\${STATE_DIR}/pane1.log"
1060
+ PID_FILE="\${STATE_DIR}/watcher.pid"
1061
+
1062
+ # Interactive prompt patterns (do NOT send "go" if matched)
1063
+ INTERACTIVE_RE='[Pp]assword[: ]|[Pp]assphrase|[Uu]sername[: ]|[Tt]oken[: ]|[Ll]ogin[: ]|\\(y/[Nn]\\)|\\(Y/[Nn]\\)|\\[y/[Nn]\\]|\\[Y/[Nn]\\]|Are you sure|Continue\\?|[Pp]ress [Ee]nter|MFA|2FA|one-time|OTP'
1064
+
1065
+ # Pause state per pane: 0=active, consecutive hit count
1066
+ PANE0_PROMPT_HITS=0
1067
+ PANE1_PROMPT_HITS=0
1068
+ PANE0_PAUSED=0
1069
+ PANE1_PAUSED=0
1070
+ PANE0_PAUSE_SIZE=0
1071
+ PANE1_PAUSE_SIZE=0
1072
+
1073
+ # ─── PID singleton ───────────────────────────────
1074
+
1075
+ if [[ -f "\$PID_FILE" ]]; then
1076
+ old_pid=\$(cat "\$PID_FILE" 2>/dev/null)
1077
+ if [[ -n "\$old_pid" ]] && kill -0 "\$old_pid" 2>/dev/null; then
1078
+ old_args=\$(ps -p "\$old_pid" -o args= 2>/dev/null || echo "")
1079
+ if echo "\$old_args" | grep -qF "\$SCRIPT_PATH"; then
1080
+ echo "[Watcher] Killing old watcher (PID \$old_pid)"
1081
+ kill "\$old_pid" 2>/dev/null || true
1082
+ sleep 1
1083
+ fi
1084
+ fi
1085
+ fi
1086
+
1087
+ echo \$\$ > "\$PID_FILE"
1088
+
1089
+ # ─── Cleanup trap ────────────────────────────────
1090
+
1091
+ cleanup() {
1092
+ echo "[Watcher] Shutting down..."
1093
+ # Stop pipe-pane capture
1094
+ tmux pipe-pane -t "\${SESSION}:0.0" 2>/dev/null || true
1095
+ tmux pipe-pane -t "\${SESSION}:0.1" 2>/dev/null || true
1096
+ # Kill event accelerator
1097
+ if [[ -n "\$ACCEL_PID" ]] && kill -0 "\$ACCEL_PID" 2>/dev/null; then
1098
+ kill "\$ACCEL_PID" 2>/dev/null || true
1099
+ fi
1100
+ # Clean up PID and flag files
1101
+ rm -f "\$PID_FILE" "\${STATE_DIR}/.turn_changed"
1102
+ # Archive pane logs (not delete) so transcripts are preserved
1103
+ local logs_dir="\${STATE_DIR}/logs"
1104
+ mkdir -p "\$logs_dir"
1105
+ local archive_ts
1106
+ archive_ts=\$(date "+%Y-%m-%dT%H-%M-%S")
1107
+ for lf in "\$PANE0_LOG" "\$PANE1_LOG"; do
1108
+ if [[ -f "\$lf" && -s "\$lf" ]]; then
1109
+ local base
1110
+ base=\$(basename "\$lf" .log)
1111
+ mv "\$lf" "\${logs_dir}/\${base}-\${archive_ts}.log" 2>/dev/null || true
1112
+ fi
1113
+ done
1114
+ exit 0
1115
+ }
1116
+ trap cleanup EXIT INT TERM
1117
+
1118
+ # ─── Set up pipe-pane ────────────────────────────
1119
+
1120
+ touch "\$PANE0_LOG" "\$PANE1_LOG"
1121
+ tmux pipe-pane -o -t "\${SESSION}:0.0" "cat >> \\"\$PANE0_LOG\\"" 2>/dev/null || true
1122
+ tmux pipe-pane -o -t "\${SESSION}:0.1" "cat >> \\"\$PANE1_LOG\\"" 2>/dev/null || true
1123
+
1124
+ # ─── Helper functions ────────────────────────────
1125
+
1126
+ check_session_alive() {
1127
+ if ! tmux has-session -t "\${SESSION}" 2>/dev/null; then
1128
+ echo "[Watcher] ERROR: tmux session '\${SESSION}' no longer exists. Exiting."
1129
+ exit 1
1130
+ fi
1131
+ }
1132
+
1133
+ # Returns 0 if agent appears dead (pane shows bare shell 3 consecutive times)
1134
+ check_agent_alive() {
1135
+ local pane="\$1"
1136
+ local agent_name="\$2"
1137
+ local dead_count=0
1138
+ local i
1139
+ for i in 1 2 3; do
1140
+ local pane_cmd
1141
+ pane_cmd=\$(tmux list-panes -t "\${SESSION}" -F '#{pane_index} #{pane_current_command}' 2>/dev/null | grep "^\${pane##*.} " | awk '{print \$2}')
1142
+ if [[ "\$pane_cmd" == "bash" || "\$pane_cmd" == "zsh" || "\$pane_cmd" == "sh" || "\$pane_cmd" == "fish" ]]; then
1143
+ dead_count=\$((dead_count + 1))
1144
+ else
1145
+ return 0 # Agent alive
1146
+ fi
1147
+ [[ \$i -lt 3 ]] && sleep 2
1148
+ done
1149
+ if (( dead_count >= 3 )); then
1150
+ echo "[Watcher] ALERT: \$agent_name appears to have exited (pane shows shell 3 consecutive times)"
1151
+ return 1 # Agent dead
1152
+ fi
1153
+ return 0
1154
+ }
1155
+
1156
+ # Returns 0 if pane output has been stable for at least N seconds
1157
+ check_output_stable() {
1158
+ local log_file="\$1"
1159
+ local stable_seconds="\${2:-5}"
1160
+
1161
+ if [[ ! -f "\$log_file" ]]; then
1162
+ return 0
1163
+ fi
1164
+
1165
+ local mtime_epoch now_epoch elapsed
1166
+ if [[ "\$(uname)" == "Darwin" ]]; then
1167
+ mtime_epoch=\$(stat -f %m "\$log_file" 2>/dev/null || echo 0)
1168
+ else
1169
+ mtime_epoch=\$(stat -c %Y "\$log_file" 2>/dev/null || echo 0)
1170
+ fi
1171
+ now_epoch=\$(date +%s)
1172
+ elapsed=\$(( now_epoch - mtime_epoch ))
1173
+
1174
+ if (( elapsed >= stable_seconds )); then
1175
+ return 0 # Stable
1176
+ fi
1177
+ return 1 # Still producing output
1178
+ }
1179
+
1180
+ # Returns 0 if interactive prompt detected (do NOT send go)
1181
+ check_for_interactive_prompt() {
1182
+ local pane="\$1"
1183
+ local pane_content
1184
+ pane_content=\$(tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | tail -5)
1185
+ if echo "\$pane_content" | grep -Eq "\$INTERACTIVE_RE"; then
1186
+ return 0 # IS interactive
1187
+ fi
1188
+ return 1 # Not interactive
1189
+ }
1190
+
1191
+ # Truncate log file safely: unbind pipe → truncate → rebind
1192
+ truncate_log_if_needed() {
1193
+ local pane="\$1"
1194
+ local log_file="\$2"
1195
+ local max_bytes=1048576 # 1MB
1196
+
1197
+ if [[ ! -f "\$log_file" ]]; then return; fi
1198
+ local size
1199
+ size=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ')
1200
+ if (( size > max_bytes )); then
1201
+ echo "[Watcher] Truncating \$log_file (\${size} bytes > 1MB)"
1202
+ tmux pipe-pane -t "\${SESSION}:\${pane}" 2>/dev/null || true
1203
+ tail -c 102400 "\$log_file" > "\${log_file}.tmp" && mv "\${log_file}.tmp" "\$log_file"
1204
+ tmux pipe-pane -o -t "\${SESSION}:\${pane}" "cat >> \\"\$log_file\\"" 2>/dev/null || true
1205
+ fi
1206
+ }
1207
+
1208
+ # ─── send_go_to_pane ─────────────────────────────
1209
+
1210
+ send_go_to_pane() {
1211
+ local pane="\$1"
1212
+ local agent_name="\$2"
1213
+ local log_file="\$3"
1214
+ local go_msg="\${4:-go}"
1215
+ local max_retries=3
1216
+ local attempt=0
1217
+
1218
+ # 1. Agent alive?
1219
+ if ! check_agent_alive "\$pane" "\$agent_name"; then
1220
+ echo "[Watcher] Skipping \$agent_name - agent not running"
1221
+ return 1
1222
+ fi
1223
+
1224
+ # 2. Interactive prompt?
1225
+ if check_for_interactive_prompt "\$pane"; then
1226
+ echo "[Watcher] Skipping \$agent_name - interactive prompt detected"
1227
+ return 1
1228
+ fi
1229
+
1230
+ # 3. Wait for output to stabilize (max 60s, then FAIL — not continue)
1231
+ local wait_count=0
1232
+ while ! check_output_stable "\$log_file" 5; do
1233
+ wait_count=\$((wait_count + 1))
1234
+ if (( wait_count > 30 )); then
1235
+ echo "[Watcher] WARNING: \$agent_name output not stabilizing after 60s, returning failure"
1236
+ return 1
1237
+ fi
1238
+ sleep 2
1239
+ done
1240
+
1241
+ # 4. Double-confirm stability
1242
+ sleep 2
1243
+ if ! check_output_stable "\$log_file" 2; then
1244
+ echo "[Watcher] \$agent_name output resumed during confirmation wait, returning failure"
1245
+ return 1
1246
+ fi
1247
+
1248
+ # 5. Re-check interactive prompt
1249
+ if check_for_interactive_prompt "\$pane"; then
1250
+ echo "[Watcher] Skipping \$agent_name - interactive prompt detected (post-wait)"
1251
+ return 1
1252
+ fi
1253
+
1254
+ # 6. Record log size before sending
1255
+ local pre_size
1256
+ pre_size=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ' || echo 0)
1257
+
1258
+ # 7. Send trigger message + Enter with retry
1259
+ # Use first 20 chars as detection marker (long messages wrap in narrow panes)
1260
+ local detect_marker="\${go_msg:0:20}"
1261
+ while (( attempt < max_retries )); do
1262
+ tmux send-keys -t "\${SESSION}:\${pane}" -l "\$go_msg" 2>/dev/null || true
1263
+ sleep 1
1264
+ tmux send-keys -t "\${SESSION}:\${pane}" Enter 2>/dev/null || true
1265
+ sleep 3
1266
+
1267
+ # Check if message is stuck in input line (not submitted)
1268
+ local pane_content
1269
+ pane_content=\$(tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | tail -5)
1270
+ if echo "\$pane_content" | grep -qF "\$detect_marker"; then
1271
+ attempt=\$((attempt + 1))
1272
+ echo "[Watcher] Retry \$attempt: Enter not registered for \$agent_name"
1273
+ tmux send-keys -t "\${SESSION}:\${pane}" C-u 2>/dev/null || true
1274
+ sleep 1
1275
+ else
1276
+ break
1277
+ fi
1278
+ done
1279
+
1280
+ # 8. Verify delivery: did log file grow?
1281
+ sleep 5
1282
+ local post_size
1283
+ post_size=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ' || echo 0)
1284
+ if (( post_size <= pre_size )); then
1285
+ echo "[Watcher] WARNING: No new output from \$agent_name after sending 'go'"
1286
+ return 1
1287
+ fi
1288
+
1289
+ echo "[Watcher] OK: \$agent_name is working (output \$pre_size -> \$post_size)"
1290
+ return 0
1291
+ }
1292
+
1293
+ # ─── trigger_agent ───────────────────────────────
1294
+
1295
+ trigger_agent() {
1296
+ local turn="\$1"
1297
+ if [[ "\$turn" == "ralph" ]]; then
1298
+ # Check pause state
1299
+ if (( PANE0_PAUSED )); then
1300
+ local cur_size
1301
+ cur_size=\$(wc -c < "\$PANE0_LOG" 2>/dev/null | tr -d ' ' || echo 0)
1302
+ if (( cur_size != PANE0_PAUSE_SIZE )) && ! check_for_interactive_prompt "0.0"; then
1303
+ echo "[Watcher] Ralph pane resumed (output changed + prompt gone)"
1304
+ PANE0_PAUSED=0
1305
+ PANE0_PROMPT_HITS=0
1306
+ else
1307
+ echo "[Watcher] Ralph pane still paused (waiting for user)"
1308
+ return 1
1309
+ fi
1310
+ fi
1311
+ local ralph_msg="Your turn. Lisa's feedback is ready — run: ralph-lisa read review.md"
1312
+ send_go_to_pane "0.0" "Ralph" "\$PANE0_LOG" "\$ralph_msg"
1313
+ local rc=\$?
1314
+ if (( rc != 0 )); then
1315
+ # Track interactive prompt hits for pause
1316
+ if check_for_interactive_prompt "0.0"; then
1317
+ PANE0_PROMPT_HITS=\$((PANE0_PROMPT_HITS + 1))
1318
+ if (( PANE0_PROMPT_HITS >= 3 )); then
1319
+ PANE0_PAUSED=1
1320
+ PANE0_PAUSE_SIZE=\$(wc -c < "\$PANE0_LOG" 2>/dev/null | tr -d ' ' || echo 0)
1321
+ echo "[Watcher] PAUSED: Ralph pane waiting for user input (hit \$PANE0_PROMPT_HITS times)"
1322
+ fi
1323
+ fi
1324
+ else
1325
+ PANE0_PROMPT_HITS=0
1326
+ fi
1327
+ return \$rc
1328
+ elif [[ "\$turn" == "lisa" ]]; then
1329
+ if (( PANE1_PAUSED )); then
1330
+ local cur_size
1331
+ cur_size=\$(wc -c < "\$PANE1_LOG" 2>/dev/null | tr -d ' ' || echo 0)
1332
+ if (( cur_size != PANE1_PAUSE_SIZE )) && ! check_for_interactive_prompt "0.1"; then
1333
+ echo "[Watcher] Lisa pane resumed (output changed + prompt gone)"
1334
+ PANE1_PAUSED=0
1335
+ PANE1_PROMPT_HITS=0
1336
+ else
1337
+ echo "[Watcher] Lisa pane still paused (waiting for user)"
1338
+ return 1
1339
+ fi
1340
+ fi
1341
+ local lisa_msg="Your turn. Ralph's work is ready — run: ralph-lisa read work.md"
1342
+ send_go_to_pane "0.1" "Lisa" "\$PANE1_LOG" "\$lisa_msg"
1343
+ local rc=\$?
1344
+ if (( rc != 0 )); then
1345
+ if check_for_interactive_prompt "0.1"; then
1346
+ PANE1_PROMPT_HITS=\$((PANE1_PROMPT_HITS + 1))
1347
+ if (( PANE1_PROMPT_HITS >= 3 )); then
1348
+ PANE1_PAUSED=1
1349
+ PANE1_PAUSE_SIZE=\$(wc -c < "\$PANE1_LOG" 2>/dev/null | tr -d ' ' || echo 0)
1350
+ echo "[Watcher] PAUSED: Lisa pane waiting for user input (hit \$PANE1_PROMPT_HITS times)"
1351
+ fi
1352
+ fi
1353
+ else
1354
+ PANE1_PROMPT_HITS=0
1355
+ fi
1356
+ return \$rc
1357
+ fi
1358
+ return 1
1359
+ }
1360
+
1361
+ # ─── check_and_trigger (state machine) ───────────
1362
+
1363
+ check_and_trigger() {
1364
+ check_session_alive
1365
+
1366
+ # Truncate logs if too large
1367
+ truncate_log_if_needed "0.0" "\$PANE0_LOG"
1368
+ truncate_log_if_needed "0.1" "\$PANE1_LOG"
1369
+
1370
+ if [[ -f "\$STATE_DIR/turn.txt" ]]; then
1371
+ CURRENT_TURN=\$(cat "\$STATE_DIR/turn.txt" 2>/dev/null || echo "")
1372
+
1373
+ # Detect new turn change (reset fail count)
1374
+ if [[ -n "\$CURRENT_TURN" && "\$CURRENT_TURN" != "\$SEEN_TURN" ]]; then
1375
+ echo "[Watcher] Turn changed: \$SEEN_TURN -> \$CURRENT_TURN"
1376
+ SEEN_TURN="\$CURRENT_TURN"
1377
+ FAIL_COUNT=0
1378
+
1379
+ # Write round separator to pane logs for transcript tracking
1380
+ local round_ts
1381
+ round_ts=\$(date "+%Y-%m-%d %H:%M:%S")
1382
+ local round_marker="\\n\\n===== [Turn -> \$CURRENT_TURN] \$round_ts =====\\n\\n"
1383
+ echo -e "\$round_marker" >> "\$PANE0_LOG" 2>/dev/null || true
1384
+ echo -e "\$round_marker" >> "\$PANE1_LOG" 2>/dev/null || true
1385
+ fi
1386
+
1387
+ # Need to deliver? (seen but not yet acked)
1388
+ if [[ -n "\$SEEN_TURN" && "\$SEEN_TURN" != "\$ACKED_TURN" ]]; then
1389
+ # Backoff on repeated failures
1390
+ if (( FAIL_COUNT >= 30 )); then
1391
+ echo "[Watcher] ALERT: \$FAIL_COUNT consecutive failures. Manual intervention needed."
1392
+ sleep 30
1393
+ elif (( FAIL_COUNT >= 10 )); then
1394
+ echo "[Watcher] DEGRADED: \$FAIL_COUNT consecutive failures, slowing down..."
1395
+ sleep 30
1396
+ fi
1397
+
1398
+ if trigger_agent "\$SEEN_TURN"; then
1399
+ ACKED_TURN="\$SEEN_TURN"
1400
+ FAIL_COUNT=0
1401
+ echo "[Watcher] Turn acknowledged: \$SEEN_TURN"
1402
+ else
1403
+ FAIL_COUNT=\$((FAIL_COUNT + 1))
1404
+ echo "[Watcher] Trigger failed (fail_count=\$FAIL_COUNT), will retry next cycle"
1405
+ fi
1406
+ fi
1407
+ fi
1408
+ }
1409
+
1410
+ # ─── Main ────────────────────────────────────────
1411
+
1412
+ echo "[Watcher] Starting v2... (Ctrl+C to stop)"
1413
+ echo "[Watcher] Monitoring \$STATE_DIR/turn.txt"
1414
+ echo "[Watcher] Pane logs: \$PANE0_LOG, \$PANE1_LOG"
1415
+ echo "[Watcher] PID: \$\$"
1416
+
1417
+ sleep 5
1418
+ check_and_trigger
1419
+
1420
+ `;
1421
+ // Event accelerator (optional background subprocess)
1422
+ if (watcher === "fswatch") {
1423
+ watcherContent += `# Event accelerator: fswatch touches flag file to wake main loop faster
1424
+ fswatch -o "\$STATE_DIR/turn.txt" 2>/dev/null | while read; do
1425
+ touch "\${STATE_DIR}/.turn_changed"
1426
+ done &
1427
+ ACCEL_PID=\$!
1428
+ echo "[Watcher] Event accelerator started (fswatch, PID \$ACCEL_PID)"
1429
+
1430
+ `;
1431
+ }
1432
+ else if (watcher === "inotifywait") {
1433
+ watcherContent += `# Event accelerator: inotifywait touches flag file to wake main loop faster
1434
+ while inotifywait -e modify "\$STATE_DIR/turn.txt" 2>/dev/null; do
1435
+ touch "\${STATE_DIR}/.turn_changed"
1436
+ done &
1437
+ ACCEL_PID=\$!
1438
+ echo "[Watcher] Event accelerator started (inotifywait, PID \$ACCEL_PID)"
1439
+
1440
+ `;
1441
+ }
1442
+ // Main polling loop (always runs, event accelerator just speeds it up)
1443
+ watcherContent += `# Main loop: polling + optional event acceleration
1444
+ while true; do
1445
+ check_and_trigger
1446
+ # If event accelerator touched the flag, skip sleep
1447
+ if [[ -f "\${STATE_DIR}/.turn_changed" ]]; then
1448
+ rm -f "\${STATE_DIR}/.turn_changed"
1449
+ else
1450
+ sleep 2
1451
+ fi
1452
+ done
1453
+ `;
1454
+ (0, state_js_1.writeFile)(watcherScript, watcherContent);
1455
+ fs.chmodSync(watcherScript, 0o755);
1456
+ // Launch tmux session
1457
+ // Layout: Ralph (left) | Lisa (right), Watcher runs in background
1458
+ execSync(`tmux kill-session -t "${sessionName}" 2>/dev/null || true`);
1459
+ // Pane 0: Ralph (left), Pane 1: Lisa (right)
1460
+ execSync(`tmux new-session -d -s "${sessionName}" -n "main" -c "${projectDir}"`);
1461
+ execSync(`tmux split-window -h -t "${sessionName}" -c "${projectDir}"`);
1462
+ // Pane 0 = Ralph (left), Pane 1 = Lisa (right)
1463
+ execSync(`tmux send-keys -t "${sessionName}:0.0" "echo '=== Ralph (Claude Code) ===' && ${claudeCmd}" Enter`);
1464
+ execSync(`tmux send-keys -t "${sessionName}:0.1" "echo '=== Lisa (Codex) ===' && ${codexCmd}" Enter`);
1465
+ execSync(`tmux select-pane -t "${sessionName}:0.0"`);
1466
+ // Watcher runs in background (logs to .dual-agent/watcher.log)
1467
+ const watcherLog = path.join(dir, "watcher.log");
1468
+ execSync(`bash -c 'nohup "${watcherScript}" > "${watcherLog}" 2>&1 &'`);
1469
+ console.log("");
1470
+ console.log(line());
1471
+ console.log("Auto Mode Started!");
1472
+ console.log(line());
1473
+ console.log("");
1474
+ console.log("Layout:");
1475
+ console.log(" +-----------+-----------+");
1476
+ console.log(" | Ralph | Lisa |");
1477
+ console.log(" | (Claude) | (Codex) |");
1478
+ console.log(" +-----------+-----------+");
1479
+ console.log(" Watcher runs in background (log: .dual-agent/watcher.log)");
1480
+ console.log(" Pane output captured: .dual-agent/pane0.log, .dual-agent/pane1.log");
1481
+ console.log("");
1482
+ console.log("Attaching to session...");
1483
+ console.log(line());
1484
+ execSync(`tmux attach-session -t "${sessionName}"`, { stdio: "inherit" });
1485
+ }
1486
+ // ─── policy ──────────────────────────────────────
1487
+ function cmdPolicy(args) {
1488
+ const sub = args[0];
1489
+ if (sub === "check-consensus") {
1490
+ cmdPolicyCheckConsensus();
1491
+ return;
1492
+ }
1493
+ if (sub === "check-next-step") {
1494
+ cmdPolicyCheckNextStep();
1495
+ return;
1496
+ }
1497
+ if (sub !== "check") {
1498
+ console.error("Usage:");
1499
+ console.error(" ralph-lisa policy check <ralph|lisa>");
1500
+ console.error(" ralph-lisa policy check-consensus");
1501
+ console.error(" ralph-lisa policy check-next-step");
1502
+ process.exit(1);
1503
+ }
1504
+ const role = args[1];
1505
+ if (role !== "ralph" && role !== "lisa") {
1506
+ console.error("Usage: ralph-lisa policy check <ralph|lisa>");
1507
+ process.exit(1);
1508
+ }
1509
+ (0, state_js_1.checkSession)();
1510
+ const dir = (0, state_js_1.stateDir)();
1511
+ const file = role === "ralph" ? "work.md" : "review.md";
1512
+ const raw = (0, state_js_1.readFile)(path.join(dir, file));
1513
+ if (!raw) {
1514
+ console.log("No submission to check.");
1515
+ return;
1516
+ }
1517
+ const content = extractSubmissionContent(raw);
1518
+ if (!content) {
1519
+ console.log("No submission content found.");
1520
+ return;
1521
+ }
1522
+ const tag = (0, state_js_1.extractTag)(content);
1523
+ if (!tag) {
1524
+ console.log("No valid tag found in submission.");
1525
+ return;
1526
+ }
1527
+ const violations = role === "ralph" ? (0, policy_js_1.checkRalph)(tag, content) : (0, policy_js_1.checkLisa)(tag, content);
1528
+ if (violations.length === 0) {
1529
+ console.log("Policy check passed.");
1530
+ return;
1531
+ }
1532
+ console.error("");
1533
+ console.error("⚠️ Policy violations:");
1534
+ for (const v of violations) {
1535
+ console.error(` - ${v.message}`);
1536
+ }
1537
+ console.error("");
1538
+ // Standalone policy check always exits non-zero on violations,
1539
+ // regardless of RL_POLICY_MODE. This is a hard gate for use in
1540
+ // scripts/hooks. RL_POLICY_MODE only affects inline checks during submit.
1541
+ process.exit(1);
1542
+ }
1543
+ /**
1544
+ * Check if the most recent round has both agents submitting [CONSENSUS].
1545
+ */
1546
+ function cmdPolicyCheckConsensus() {
1547
+ (0, state_js_1.checkSession)();
1548
+ const dir = (0, state_js_1.stateDir)();
1549
+ const workRaw = (0, state_js_1.readFile)(path.join(dir, "work.md"));
1550
+ const reviewRaw = (0, state_js_1.readFile)(path.join(dir, "review.md"));
1551
+ const workContent = extractSubmissionContent(workRaw);
1552
+ const reviewContent = extractSubmissionContent(reviewRaw);
1553
+ const workTag = workContent ? (0, state_js_1.extractTag)(workContent) : "";
1554
+ const reviewTag = reviewContent ? (0, state_js_1.extractTag)(reviewContent) : "";
1555
+ const issues = [];
1556
+ if (workTag !== "CONSENSUS") {
1557
+ issues.push(`Ralph's latest submission is [${workTag || "none"}], not [CONSENSUS].`);
1558
+ }
1559
+ if (reviewTag !== "CONSENSUS") {
1560
+ issues.push(`Lisa's latest submission is [${reviewTag || "none"}], not [CONSENSUS].`);
1561
+ }
1562
+ if (issues.length === 0) {
1563
+ console.log("Consensus reached: both agents submitted [CONSENSUS].");
1564
+ return;
1565
+ }
1566
+ console.error("Consensus NOT reached:");
1567
+ for (const issue of issues) {
1568
+ console.error(` - ${issue}`);
1569
+ }
1570
+ process.exit(1);
1571
+ }
1572
+ /**
1573
+ * Comprehensive check for proceeding to the next step:
1574
+ * 1. Both agents have submitted [CONSENSUS]
1575
+ * 2. Ralph's submission passes policy checks
1576
+ * 3. Lisa's submission passes policy checks
1577
+ */
1578
+ function cmdPolicyCheckNextStep() {
1579
+ (0, state_js_1.checkSession)();
1580
+ const dir = (0, state_js_1.stateDir)();
1581
+ const workRaw = (0, state_js_1.readFile)(path.join(dir, "work.md"));
1582
+ const reviewRaw = (0, state_js_1.readFile)(path.join(dir, "review.md"));
1583
+ const workContent = extractSubmissionContent(workRaw);
1584
+ const reviewContent = extractSubmissionContent(reviewRaw);
1585
+ const workTag = workContent ? (0, state_js_1.extractTag)(workContent) : "";
1586
+ const reviewTag = reviewContent ? (0, state_js_1.extractTag)(reviewContent) : "";
1587
+ const allIssues = [];
1588
+ // 1. Consensus check
1589
+ if (workTag !== "CONSENSUS") {
1590
+ allIssues.push(`Ralph's latest is [${workTag || "none"}], not [CONSENSUS].`);
1591
+ }
1592
+ if (reviewTag !== "CONSENSUS") {
1593
+ allIssues.push(`Lisa's latest is [${reviewTag || "none"}], not [CONSENSUS].`);
1594
+ }
1595
+ // 2. Policy checks on latest submissions (if content exists)
1596
+ if (workContent && workTag) {
1597
+ const rv = (0, policy_js_1.checkRalph)(workTag, workContent);
1598
+ for (const v of rv)
1599
+ allIssues.push(`Ralph: ${v.message}`);
1600
+ }
1601
+ if (reviewContent && reviewTag) {
1602
+ const lv = (0, policy_js_1.checkLisa)(reviewTag, reviewContent);
1603
+ for (const v of lv)
1604
+ allIssues.push(`Lisa: ${v.message}`);
1605
+ }
1606
+ if (allIssues.length === 0) {
1607
+ console.log("Ready to proceed: consensus reached and all checks pass.");
1608
+ return;
1609
+ }
1610
+ console.error("Not ready to proceed:");
1611
+ for (const issue of allIssues) {
1612
+ console.error(` - ${issue}`);
1613
+ }
1614
+ process.exit(1);
1615
+ }
1616
+ /**
1617
+ * Extract the actual submission content from work.md/review.md.
1618
+ * The file has metadata headers; the submission content is the part
1619
+ * that starts with a [TAG] line.
1620
+ */
1621
+ function extractSubmissionContent(raw) {
1622
+ const lines = raw.split("\n");
1623
+ for (let i = 0; i < lines.length; i++) {
1624
+ if ((0, state_js_1.extractTag)(lines[i])) {
1625
+ return lines.slice(i).join("\n");
1626
+ }
1627
+ }
1628
+ return "";
1629
+ }
1630
+ // ─── logs ────────────────────────────────────────
1631
+ function cmdLogs(args) {
1632
+ const dir = (0, state_js_1.stateDir)();
1633
+ const logsDir = path.join(dir, "logs");
1634
+ // Also include live pane logs
1635
+ const liveFiles = [];
1636
+ for (const f of ["pane0.log", "pane1.log"]) {
1637
+ const p = path.join(dir, f);
1638
+ if (fs.existsSync(p) && fs.statSync(p).size > 0)
1639
+ liveFiles.push(p);
1640
+ }
1641
+ const archivedFiles = [];
1642
+ if (fs.existsSync(logsDir)) {
1643
+ archivedFiles.push(...fs.readdirSync(logsDir)
1644
+ .filter((f) => f.endsWith(".log"))
1645
+ .sort()
1646
+ .map((f) => path.join(logsDir, f)));
1647
+ }
1648
+ const sub = args[0] || "";
1649
+ if (sub === "cat" || sub === "view") {
1650
+ // ralph-lisa logs cat [filename] — view a specific log or latest
1651
+ const target = args[1];
1652
+ let file;
1653
+ if (target) {
1654
+ // Try exact match in logs/ or as pane name
1655
+ file = [...archivedFiles, ...liveFiles].find((f) => path.basename(f) === target || f.endsWith(target));
1656
+ }
1657
+ else {
1658
+ // Default: show live pane logs
1659
+ if (liveFiles.length > 0) {
1660
+ for (const f of liveFiles) {
1661
+ console.log(`\n${"=".repeat(60)}`);
1662
+ console.log(` ${path.basename(f)} (live)`);
1663
+ console.log(`${"=".repeat(60)}\n`);
1664
+ console.log(fs.readFileSync(f, "utf-8"));
1665
+ }
1666
+ return;
1667
+ }
1668
+ console.log("No live pane logs. Use 'ralph-lisa logs cat <filename>' to view archived logs.");
1669
+ return;
1670
+ }
1671
+ if (file && fs.existsSync(file)) {
1672
+ console.log(fs.readFileSync(file, "utf-8"));
1673
+ }
1674
+ else {
1675
+ console.error(`Log not found: ${target}`);
1676
+ process.exit(1);
1677
+ }
1678
+ return;
1679
+ }
1680
+ // Default: list all logs
1681
+ console.log("Transcript Logs");
1682
+ console.log("===============\n");
1683
+ if (liveFiles.length > 0) {
1684
+ console.log("Live (current session):");
1685
+ for (const f of liveFiles) {
1686
+ const stat = fs.statSync(f);
1687
+ const size = stat.size > 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
1688
+ console.log(` ${path.basename(f)} ${size} ${stat.mtime.toISOString().slice(0, 19)}`);
1689
+ }
1690
+ console.log("");
1691
+ }
1692
+ if (archivedFiles.length > 0) {
1693
+ console.log("Archived (previous sessions):");
1694
+ for (const f of archivedFiles) {
1695
+ const stat = fs.statSync(f);
1696
+ const size = stat.size > 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
1697
+ console.log(` ${path.basename(f)} ${size} ${stat.mtime.toISOString().slice(0, 19)}`);
1698
+ }
1699
+ console.log("");
1700
+ }
1701
+ if (liveFiles.length === 0 && archivedFiles.length === 0) {
1702
+ console.log("No transcript logs found. Logs are created during auto mode sessions.");
1703
+ }
1704
+ console.log("\nUsage:");
1705
+ console.log(" ralph-lisa logs List all logs");
1706
+ console.log(" ralph-lisa logs cat View live pane logs");
1707
+ console.log(" ralph-lisa logs cat <file> View specific log file");
1708
+ }
1709
+ // ─── doctor ──────────────────────────────────────
1710
+ function cmdDoctor(args) {
1711
+ const strict = args.includes("--strict");
1712
+ const { execSync } = require("node:child_process");
1713
+ console.log(line());
1714
+ console.log("Ralph-Lisa Loop - Dependency Check");
1715
+ console.log(line());
1716
+ console.log("");
1717
+ let allOk = true;
1718
+ let hasWatcher = false;
1719
+ const checks = [
1720
+ {
1721
+ name: "tmux",
1722
+ cmd: "which tmux",
1723
+ versionCmd: "tmux -V",
1724
+ required: true,
1725
+ installHint: "brew install tmux (macOS) / apt install tmux (Linux)",
1726
+ },
1727
+ {
1728
+ name: "claude (Claude Code CLI)",
1729
+ cmd: "which claude",
1730
+ versionCmd: "claude --version",
1731
+ required: true,
1732
+ installHint: "npm install -g @anthropic-ai/claude-code",
1733
+ },
1734
+ {
1735
+ name: "codex (OpenAI Codex CLI)",
1736
+ cmd: "which codex",
1737
+ versionCmd: "codex --version",
1738
+ required: true,
1739
+ installHint: "npm install -g @openai/codex",
1740
+ },
1741
+ {
1742
+ name: "fswatch (file watcher)",
1743
+ cmd: "which fswatch",
1744
+ required: false,
1745
+ installHint: "brew install fswatch (macOS)",
1746
+ },
1747
+ {
1748
+ name: "inotifywait (file watcher)",
1749
+ cmd: "which inotifywait",
1750
+ required: false,
1751
+ installHint: "apt install inotify-tools (Linux)",
1752
+ },
1753
+ ];
1754
+ for (const check of checks) {
1755
+ try {
1756
+ execSync(check.cmd, { stdio: "pipe" });
1757
+ let version = "";
1758
+ if (check.versionCmd) {
1759
+ try {
1760
+ version = execSync(check.versionCmd, {
1761
+ stdio: "pipe",
1762
+ encoding: "utf-8",
1763
+ timeout: 5000,
1764
+ })
1765
+ .trim()
1766
+ .split("\n")[0];
1767
+ }
1768
+ catch { }
1769
+ }
1770
+ console.log(` OK ${check.name}${version ? ` (${version})` : ""}`);
1771
+ if (check.name.includes("fswatch") || check.name.includes("inotifywait")) {
1772
+ hasWatcher = true;
1773
+ }
1774
+ }
1775
+ catch {
1776
+ if (check.required) {
1777
+ console.log(` MISSING ${check.name}`);
1778
+ console.log(` Install: ${check.installHint}`);
1779
+ allOk = false;
1780
+ }
1781
+ else {
1782
+ console.log(` -- ${check.name} (optional)`);
1783
+ }
1784
+ }
1785
+ }
1786
+ if (!hasWatcher) {
1787
+ console.log("");
1788
+ console.log(" Note: No file watcher found. Auto mode will use polling (slower).");
1789
+ console.log(" Install fswatch or inotify-tools for event-driven turn detection.");
1790
+ }
1791
+ // Node version
1792
+ const nodeVersion = process.version;
1793
+ const majorVersion = parseInt(nodeVersion.slice(1), 10);
1794
+ if (majorVersion >= 18) {
1795
+ console.log(` OK Node.js ${nodeVersion}`);
1796
+ }
1797
+ else {
1798
+ console.log(` WARNING Node.js ${nodeVersion} (requires >= 18)`);
1799
+ allOk = false;
1800
+ }
1801
+ console.log("");
1802
+ if (allOk) {
1803
+ console.log("All required dependencies satisfied.");
1804
+ }
1805
+ else {
1806
+ console.log("Some required dependencies missing. Install them and re-run 'ralph-lisa doctor'.");
1807
+ }
1808
+ console.log(line());
1809
+ if (strict && !allOk) {
1810
+ process.exit(1);
1811
+ }
1812
+ }