ralph-lisa-loop 3.0.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,1029 @@
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.cmdStep = cmdStep;
47
+ exports.cmdHistory = cmdHistory;
48
+ exports.cmdArchive = cmdArchive;
49
+ exports.cmdClean = cmdClean;
50
+ exports.cmdUninit = cmdUninit;
51
+ exports.cmdInitProject = cmdInitProject;
52
+ exports.cmdStart = cmdStart;
53
+ exports.cmdAuto = cmdAuto;
54
+ exports.cmdPolicy = cmdPolicy;
55
+ const fs = __importStar(require("node:fs"));
56
+ const path = __importStar(require("node:path"));
57
+ const state_js_1 = require("./state.js");
58
+ const policy_js_1 = require("./policy.js");
59
+ function line(ch = "=", len = 40) {
60
+ return ch.repeat(len);
61
+ }
62
+ // ─── init ────────────────────────────────────────
63
+ function cmdInit(args) {
64
+ const task = args.join(" ");
65
+ if (!task) {
66
+ console.error('Usage: ralph-lisa init "task description"');
67
+ process.exit(1);
68
+ }
69
+ const dir = (0, state_js_1.stateDir)();
70
+ if (fs.existsSync(dir)) {
71
+ console.log("Warning: Existing session will be overwritten");
72
+ }
73
+ fs.rmSync(dir, { recursive: true, force: true });
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ const ts = (0, state_js_1.timestamp)();
76
+ (0, state_js_1.writeFile)(path.join(dir, "task.md"), `# Task\n\n${task}\n\n---\nCreated: ${ts}\n`);
77
+ (0, state_js_1.writeFile)(path.join(dir, "round.txt"), "1");
78
+ (0, state_js_1.writeFile)(path.join(dir, "step.txt"), "planning");
79
+ (0, state_js_1.writeFile)(path.join(dir, "turn.txt"), "ralph");
80
+ (0, state_js_1.writeFile)(path.join(dir, "last_action.txt"), "(No action yet)");
81
+ (0, state_js_1.writeFile)(path.join(dir, "plan.md"), "# Plan\n\n(To be drafted by Ralph and reviewed by Lisa)\n");
82
+ (0, state_js_1.writeFile)(path.join(dir, "work.md"), "# Ralph Work\n\n(Waiting for Ralph to submit)\n");
83
+ (0, state_js_1.writeFile)(path.join(dir, "review.md"), "# Lisa Review\n\n(Waiting for Lisa to respond)\n");
84
+ (0, state_js_1.writeFile)(path.join(dir, "history.md"), `# Collaboration History\n\n**Task**: ${task}\n**Started**: ${ts}\n`);
85
+ console.log(line());
86
+ console.log("Session Initialized");
87
+ console.log(line());
88
+ console.log(`Task: ${task}`);
89
+ console.log("Turn: ralph");
90
+ console.log("");
91
+ console.log('Ralph should start with: ralph-lisa submit-ralph "[PLAN] summary..."');
92
+ console.log(line());
93
+ }
94
+ // ─── whose-turn ──────────────────────────────────
95
+ function cmdWhoseTurn() {
96
+ (0, state_js_1.checkSession)();
97
+ console.log((0, state_js_1.getTurn)());
98
+ }
99
+ // ─── submit-ralph ────────────────────────────────
100
+ function cmdSubmitRalph(args) {
101
+ (0, state_js_1.checkSession)();
102
+ const content = args.join(" ");
103
+ if (!content) {
104
+ console.error('Usage: ralph-lisa submit-ralph "[TAG] summary\\n\\ndetails..."');
105
+ console.error("");
106
+ console.error("Valid tags: PLAN, RESEARCH, CODE, FIX, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
107
+ process.exit(1);
108
+ }
109
+ const turn = (0, state_js_1.getTurn)();
110
+ if (turn !== "ralph") {
111
+ console.error("Error: It's Lisa's turn. Wait for her response.");
112
+ console.error("Run: ralph-lisa whose-turn");
113
+ process.exit(1);
114
+ }
115
+ const tag = (0, state_js_1.extractTag)(content);
116
+ if (!tag) {
117
+ console.error("Error: Content must start with a valid tag.");
118
+ console.error("Format: [TAG] One line summary");
119
+ console.error("");
120
+ console.error("Valid tags: PLAN, RESEARCH, CODE, FIX, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
121
+ process.exit(1);
122
+ }
123
+ // Policy check
124
+ if (!(0, policy_js_1.runPolicyCheck)("ralph", tag, content)) {
125
+ process.exit(1);
126
+ }
127
+ const round = (0, state_js_1.getRound)();
128
+ const step = (0, state_js_1.getStep)();
129
+ const ts = (0, state_js_1.timestamp)();
130
+ const summary = (0, state_js_1.extractSummary)(content);
131
+ const dir = (0, state_js_1.stateDir)();
132
+ (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\n${content}\n`);
133
+ (0, state_js_1.appendHistory)("Ralph", content);
134
+ (0, state_js_1.updateLastAction)("Ralph", content);
135
+ (0, state_js_1.setTurn)("lisa");
136
+ console.log(line());
137
+ console.log(`Submitted: [${tag}] ${summary}`);
138
+ console.log("Turn passed to: Lisa");
139
+ console.log(line());
140
+ console.log("");
141
+ console.log("Now wait for Lisa. Check with: ralph-lisa whose-turn");
142
+ }
143
+ // ─── submit-lisa ─────────────────────────────────
144
+ function cmdSubmitLisa(args) {
145
+ (0, state_js_1.checkSession)();
146
+ const content = args.join(" ");
147
+ if (!content) {
148
+ console.error('Usage: ralph-lisa submit-lisa "[TAG] summary\\n\\ndetails..."');
149
+ console.error("");
150
+ console.error("Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
151
+ process.exit(1);
152
+ }
153
+ const turn = (0, state_js_1.getTurn)();
154
+ if (turn !== "lisa") {
155
+ console.error("Error: It's Ralph's turn. Wait for his submission.");
156
+ console.error("Run: ralph-lisa whose-turn");
157
+ process.exit(1);
158
+ }
159
+ const tag = (0, state_js_1.extractTag)(content);
160
+ if (!tag) {
161
+ console.error("Error: Content must start with a valid tag.");
162
+ console.error("Format: [TAG] One line summary");
163
+ console.error("");
164
+ console.error("Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS");
165
+ process.exit(1);
166
+ }
167
+ // Policy check
168
+ if (!(0, policy_js_1.runPolicyCheck)("lisa", tag, content)) {
169
+ process.exit(1);
170
+ }
171
+ const round = (0, state_js_1.getRound)();
172
+ const step = (0, state_js_1.getStep)();
173
+ const ts = (0, state_js_1.timestamp)();
174
+ const summary = (0, state_js_1.extractSummary)(content);
175
+ const dir = (0, state_js_1.stateDir)();
176
+ (0, state_js_1.writeFile)(path.join(dir, "review.md"), `# Lisa Review\n\n## [${tag}] Round ${round} | Step: ${step}\n**Updated**: ${ts}\n**Summary**: ${summary}\n\n${content}\n`);
177
+ (0, state_js_1.appendHistory)("Lisa", content);
178
+ (0, state_js_1.updateLastAction)("Lisa", content);
179
+ (0, state_js_1.setTurn)("ralph");
180
+ // Increment round
181
+ const nextRound = (parseInt(round, 10) || 0) + 1;
182
+ (0, state_js_1.setRound)(nextRound);
183
+ console.log(line());
184
+ console.log(`Submitted: [${tag}] ${summary}`);
185
+ console.log("Turn passed to: Ralph");
186
+ console.log(`Round: ${round} -> ${nextRound}`);
187
+ console.log(line());
188
+ console.log("");
189
+ console.log("Now wait for Ralph. Check with: ralph-lisa whose-turn");
190
+ }
191
+ // ─── status ──────────────────────────────────────
192
+ function cmdStatus() {
193
+ const dir = (0, state_js_1.stateDir)();
194
+ if (!fs.existsSync(dir)) {
195
+ console.log("Status: Not initialized");
196
+ return;
197
+ }
198
+ const turn = (0, state_js_1.getTurn)();
199
+ const round = (0, state_js_1.getRound)();
200
+ const step = (0, state_js_1.getStep)();
201
+ const last = (0, state_js_1.readFile)(path.join(dir, "last_action.txt")) || "None";
202
+ const taskFile = (0, state_js_1.readFile)(path.join(dir, "task.md"));
203
+ const taskLine = taskFile.split("\n")[2] || "Unknown";
204
+ console.log(line());
205
+ console.log("Ralph Lisa Dual-Agent Loop");
206
+ console.log(line());
207
+ console.log(`Task: ${taskLine}`);
208
+ console.log(`Round: ${round} | Step: ${step}`);
209
+ console.log("");
210
+ console.log(`>>> Turn: ${turn} <<<`);
211
+ console.log(`Last: ${last}`);
212
+ console.log(line());
213
+ }
214
+ // ─── read ────────────────────────────────────────
215
+ function cmdRead(args) {
216
+ (0, state_js_1.checkSession)();
217
+ const file = args[0];
218
+ if (!file) {
219
+ console.error("Usage: ralph-lisa read <file>");
220
+ console.error(" work.md - Ralph's work");
221
+ console.error(" review.md - Lisa's feedback");
222
+ process.exit(1);
223
+ }
224
+ const filePath = path.join((0, state_js_1.stateDir)(), file);
225
+ if (fs.existsSync(filePath)) {
226
+ console.log(fs.readFileSync(filePath, "utf-8"));
227
+ }
228
+ else {
229
+ console.log(`(File ${file} does not exist)`);
230
+ }
231
+ }
232
+ // ─── step ────────────────────────────────────────
233
+ function cmdStep(args) {
234
+ (0, state_js_1.checkSession)();
235
+ const stepName = args.join(" ");
236
+ if (!stepName) {
237
+ console.error('Usage: ralph-lisa step "step name"');
238
+ process.exit(1);
239
+ }
240
+ (0, state_js_1.setStep)(stepName);
241
+ (0, state_js_1.setRound)(1);
242
+ const dir = (0, state_js_1.stateDir)();
243
+ const ts = (0, state_js_1.timestamp)();
244
+ const entry = `\n---\n\n# Step: ${stepName}\n\nStarted: ${ts}\n\n`;
245
+ fs.appendFileSync(path.join(dir, "history.md"), entry, "utf-8");
246
+ console.log(`Entered step: ${stepName} (round reset to 1)`);
247
+ }
248
+ // ─── history ─────────────────────────────────────
249
+ function cmdHistory() {
250
+ (0, state_js_1.checkSession)();
251
+ const filePath = path.join((0, state_js_1.stateDir)(), "history.md");
252
+ if (fs.existsSync(filePath)) {
253
+ console.log(fs.readFileSync(filePath, "utf-8"));
254
+ }
255
+ }
256
+ // ─── archive ─────────────────────────────────────
257
+ function cmdArchive(args) {
258
+ (0, state_js_1.checkSession)();
259
+ const name = args[0] || new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
260
+ const archiveDir = path.join(process.cwd(), state_js_1.ARCHIVE_DIR);
261
+ const dest = path.join(archiveDir, name);
262
+ fs.mkdirSync(dest, { recursive: true });
263
+ fs.cpSync((0, state_js_1.stateDir)(), dest, { recursive: true });
264
+ console.log(`Archived: ${state_js_1.ARCHIVE_DIR}/${name}/`);
265
+ }
266
+ // ─── clean ───────────────────────────────────────
267
+ function cmdClean() {
268
+ const dir = (0, state_js_1.stateDir)();
269
+ if (fs.existsSync(dir)) {
270
+ fs.rmSync(dir, { recursive: true, force: true });
271
+ console.log("Session cleaned");
272
+ }
273
+ }
274
+ // ─── uninit ──────────────────────────────────────
275
+ const MARKER = "RALPH-LISA-LOOP";
276
+ function cmdUninit() {
277
+ const projectDir = process.cwd();
278
+ // Remove .dual-agent/
279
+ const dualAgentDir = path.join(projectDir, state_js_1.STATE_DIR);
280
+ if (fs.existsSync(dualAgentDir)) {
281
+ fs.rmSync(dualAgentDir, { recursive: true, force: true });
282
+ console.log("Removed: .dual-agent/");
283
+ }
284
+ // Clean CODEX.md marker block (same logic as CLAUDE.md — preserve pre-existing content)
285
+ const codexMd = path.join(projectDir, "CODEX.md");
286
+ if (fs.existsSync(codexMd)) {
287
+ const content = fs.readFileSync(codexMd, "utf-8");
288
+ if (content.includes(MARKER)) {
289
+ const markerIdx = content.indexOf(`<!-- ${MARKER} -->`);
290
+ if (markerIdx >= 0) {
291
+ const before = content.slice(0, markerIdx).trimEnd();
292
+ if (before) {
293
+ fs.writeFileSync(codexMd, before + "\n", "utf-8");
294
+ console.log("Cleaned: CODEX.md (removed Ralph-Lisa-Loop section)");
295
+ }
296
+ else {
297
+ fs.unlinkSync(codexMd);
298
+ console.log("Removed: CODEX.md (was entirely Ralph-Lisa-Loop content)");
299
+ }
300
+ }
301
+ }
302
+ }
303
+ // Clean CLAUDE.md marker block
304
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
305
+ if (fs.existsSync(claudeMd)) {
306
+ const content = fs.readFileSync(claudeMd, "utf-8");
307
+ if (content.includes(MARKER)) {
308
+ // Remove everything from <!-- RALPH-LISA-LOOP --> to end of file
309
+ // or to next <!-- end --> marker
310
+ const markerIdx = content.indexOf(`<!-- ${MARKER} -->`);
311
+ if (markerIdx >= 0) {
312
+ const before = content.slice(0, markerIdx).trimEnd();
313
+ if (before) {
314
+ fs.writeFileSync(claudeMd, before + "\n", "utf-8");
315
+ console.log("Cleaned: CLAUDE.md (removed Ralph-Lisa-Loop section)");
316
+ }
317
+ else {
318
+ fs.unlinkSync(claudeMd);
319
+ console.log("Removed: CLAUDE.md (was entirely Ralph-Lisa-Loop content)");
320
+ }
321
+ }
322
+ }
323
+ }
324
+ // Remove .claude/commands/ (only our files)
325
+ const claudeCmdDir = path.join(projectDir, ".claude", "commands");
326
+ const ourCommands = [
327
+ "check-turn.md",
328
+ "next-step.md",
329
+ "read-review.md",
330
+ "submit-work.md",
331
+ "view-status.md",
332
+ ];
333
+ if (fs.existsSync(claudeCmdDir)) {
334
+ for (const cmd of ourCommands) {
335
+ const cmdPath = path.join(claudeCmdDir, cmd);
336
+ if (fs.existsSync(cmdPath)) {
337
+ fs.unlinkSync(cmdPath);
338
+ }
339
+ }
340
+ // Remove directory if empty
341
+ try {
342
+ const remaining = fs.readdirSync(claudeCmdDir);
343
+ if (remaining.length === 0) {
344
+ fs.rmdirSync(claudeCmdDir);
345
+ // Also remove .claude/ if empty
346
+ const claudeDir = path.join(projectDir, ".claude");
347
+ const claudeRemaining = fs.readdirSync(claudeDir);
348
+ if (claudeRemaining.length === 0) {
349
+ fs.rmdirSync(claudeDir);
350
+ }
351
+ }
352
+ }
353
+ catch {
354
+ // ignore
355
+ }
356
+ console.log("Cleaned: .claude/commands/");
357
+ }
358
+ // Remove only our skill from .codex/ (preserve other content)
359
+ const codexSkillDir = path.join(projectDir, ".codex", "skills", "ralph-lisa-loop");
360
+ if (fs.existsSync(codexSkillDir)) {
361
+ fs.rmSync(codexSkillDir, { recursive: true, force: true });
362
+ console.log("Removed: .codex/skills/ralph-lisa-loop/");
363
+ // Clean up empty parent dirs
364
+ try {
365
+ const skillsDir = path.join(projectDir, ".codex", "skills");
366
+ if (fs.readdirSync(skillsDir).length === 0) {
367
+ fs.rmdirSync(skillsDir);
368
+ }
369
+ }
370
+ catch {
371
+ // ignore
372
+ }
373
+ }
374
+ // Remove .codex/config.toml only if it has our marker
375
+ const codexConfig = path.join(projectDir, ".codex", "config.toml");
376
+ if (fs.existsSync(codexConfig)) {
377
+ const configContent = fs.readFileSync(codexConfig, "utf-8");
378
+ if (configContent.includes(MARKER)) {
379
+ fs.unlinkSync(codexConfig);
380
+ console.log("Removed: .codex/config.toml");
381
+ }
382
+ }
383
+ // Remove .codex/ if empty
384
+ try {
385
+ const codexDir = path.join(projectDir, ".codex");
386
+ if (fs.existsSync(codexDir) && fs.readdirSync(codexDir).length === 0) {
387
+ fs.rmdirSync(codexDir);
388
+ console.log("Removed: .codex/ (empty)");
389
+ }
390
+ }
391
+ catch {
392
+ // ignore
393
+ }
394
+ // Remove io.sh if it exists
395
+ const ioSh = path.join(projectDir, "io.sh");
396
+ if (fs.existsSync(ioSh)) {
397
+ fs.unlinkSync(ioSh);
398
+ console.log("Removed: io.sh");
399
+ }
400
+ console.log("");
401
+ console.log("Ralph-Lisa Loop removed from this project.");
402
+ }
403
+ // ─── init (project setup) ────────────────────────
404
+ function cmdInitProject(args) {
405
+ // Parse --minimal flag
406
+ const minimal = args.includes("--minimal");
407
+ const filteredArgs = args.filter((a) => a !== "--minimal");
408
+ const projectDir = filteredArgs[0] || process.cwd();
409
+ const resolvedDir = path.resolve(projectDir);
410
+ if (!fs.existsSync(resolvedDir)) {
411
+ console.error(`Error: Directory does not exist: ${resolvedDir}`);
412
+ process.exit(1);
413
+ }
414
+ console.log(line());
415
+ console.log(`Ralph-Lisa Loop - Init${minimal ? " (minimal)" : ""}`);
416
+ console.log(line());
417
+ console.log(`Project: ${resolvedDir}`);
418
+ console.log("");
419
+ if (minimal) {
420
+ // Minimal mode: only create .dual-agent/ session state.
421
+ // Use this when Claude Code plugin + Codex global config are installed.
422
+ console.log("[Session] Initializing .dual-agent/ (minimal mode)...");
423
+ const origCwd = process.cwd();
424
+ process.chdir(resolvedDir);
425
+ cmdInit(["Waiting for task assignment"]);
426
+ process.chdir(origCwd);
427
+ console.log("");
428
+ console.log(line());
429
+ console.log("Minimal Init Complete");
430
+ console.log(line());
431
+ console.log("");
432
+ console.log("Files created:");
433
+ console.log(" - .dual-agent/ (session state only)");
434
+ console.log("");
435
+ console.log("No project-level role/command files written.");
436
+ console.log("Requires: Claude Code plugin + Codex global config.");
437
+ console.log(line());
438
+ return;
439
+ }
440
+ // Find templates directory (shipped inside npm package)
441
+ const templatesDir = findTemplatesDir();
442
+ // 1. Append Ralph role to CLAUDE.md
443
+ const claudeMd = path.join(resolvedDir, "CLAUDE.md");
444
+ if (fs.existsSync(claudeMd) && (0, state_js_1.readFile)(claudeMd).includes(MARKER)) {
445
+ console.log("[Claude] Ralph role already in CLAUDE.md, skipping...");
446
+ }
447
+ else {
448
+ console.log("[Claude] Appending Ralph role to CLAUDE.md...");
449
+ const ralphRole = (0, state_js_1.readFile)(path.join(templatesDir, "roles", "ralph.md"));
450
+ if (fs.existsSync(claudeMd)) {
451
+ fs.appendFileSync(claudeMd, "\n\n", "utf-8");
452
+ }
453
+ fs.appendFileSync(claudeMd, ralphRole, "utf-8");
454
+ console.log("[Claude] Done.");
455
+ }
456
+ // 2. Create/update CODEX.md with Lisa role
457
+ const codexMd = path.join(resolvedDir, "CODEX.md");
458
+ if (fs.existsSync(codexMd) && (0, state_js_1.readFile)(codexMd).includes(MARKER)) {
459
+ console.log("[Codex] Lisa role already in CODEX.md, skipping...");
460
+ }
461
+ else {
462
+ console.log("[Codex] Creating CODEX.md with Lisa role...");
463
+ const lisaRole = (0, state_js_1.readFile)(path.join(templatesDir, "roles", "lisa.md"));
464
+ if (fs.existsSync(codexMd)) {
465
+ fs.appendFileSync(codexMd, "\n\n", "utf-8");
466
+ }
467
+ fs.appendFileSync(codexMd, lisaRole, "utf-8");
468
+ console.log("[Codex] Done.");
469
+ }
470
+ // 3. Copy Claude commands
471
+ console.log("[Claude] Copying commands to .claude/commands/...");
472
+ const claudeCmdDir = path.join(resolvedDir, ".claude", "commands");
473
+ fs.mkdirSync(claudeCmdDir, { recursive: true });
474
+ const cmdSrc = path.join(templatesDir, "claude-commands");
475
+ if (fs.existsSync(cmdSrc)) {
476
+ for (const f of fs.readdirSync(cmdSrc)) {
477
+ if (f.endsWith(".md")) {
478
+ fs.copyFileSync(path.join(cmdSrc, f), path.join(claudeCmdDir, f));
479
+ }
480
+ }
481
+ }
482
+ console.log("[Claude] Commands copied.");
483
+ // 4. Copy Codex skills
484
+ console.log("[Codex] Setting up skills in .codex/skills/ralph-lisa-loop/...");
485
+ const codexSkillDir = path.join(resolvedDir, ".codex", "skills", "ralph-lisa-loop");
486
+ fs.mkdirSync(codexSkillDir, { recursive: true });
487
+ const skillContent = `---
488
+ name: ralph-lisa-loop
489
+ description: Lisa review commands for Ralph-Lisa dual-agent collaboration
490
+ ---
491
+
492
+ # Ralph-Lisa Loop - Lisa Skills
493
+
494
+ This skill provides Lisa's review commands for the Ralph-Lisa collaboration.
495
+
496
+ ## Available Commands
497
+
498
+ ### Check Turn
499
+ \`\`\`bash
500
+ ralph-lisa whose-turn
501
+ \`\`\`
502
+ Check if it's your turn before taking action.
503
+
504
+ ### Submit Review
505
+ \`\`\`bash
506
+ ralph-lisa submit-lisa "[TAG] summary
507
+
508
+ detailed content..."
509
+ \`\`\`
510
+ Submit your review. Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS
511
+
512
+ ### View Status
513
+ \`\`\`bash
514
+ ralph-lisa status
515
+ \`\`\`
516
+ View current task, turn, and last action.
517
+
518
+ ### Read Ralph's Work
519
+ \`\`\`bash
520
+ ralph-lisa read work.md
521
+ \`\`\`
522
+ Read Ralph's latest submission.
523
+ `;
524
+ (0, state_js_1.writeFile)(path.join(codexSkillDir, "SKILL.md"), skillContent);
525
+ // Create .codex/config.toml (with marker for safe uninit)
526
+ // Codex reads AGENTS.md by default; fallback to CODEX.md for our setup
527
+ const codexConfig = `# ${MARKER} - managed by ralph-lisa-loop
528
+ project_doc_fallback_filenames = ["CODEX.md"]
529
+
530
+ [skills]
531
+ enabled = true
532
+ path = ".codex/skills"
533
+ `;
534
+ (0, state_js_1.writeFile)(path.join(resolvedDir, ".codex", "config.toml"), codexConfig);
535
+ console.log(`[Codex] Skill created at ${codexSkillDir}/`);
536
+ console.log(`[Codex] Config created at ${path.join(resolvedDir, ".codex", "config.toml")}`);
537
+ // 5. Initialize session state
538
+ console.log("[Session] Initializing .dual-agent/...");
539
+ const origCwd = process.cwd();
540
+ process.chdir(resolvedDir);
541
+ cmdInit(["Waiting for task assignment"]);
542
+ process.chdir(origCwd);
543
+ console.log("");
544
+ console.log(line());
545
+ console.log("Initialization Complete");
546
+ console.log(line());
547
+ console.log("");
548
+ console.log("Files created/updated:");
549
+ console.log(" - CLAUDE.md (Ralph role)");
550
+ console.log(" - CODEX.md (Lisa role)");
551
+ console.log(" - .claude/commands/ (Claude slash commands)");
552
+ console.log(" - .codex/skills/ (Codex skills)");
553
+ console.log(" - .dual-agent/");
554
+ console.log("");
555
+ console.log("Start agents:");
556
+ console.log(" Terminal 1: claude");
557
+ console.log(" Terminal 2: codex");
558
+ console.log("");
559
+ console.log('Or run: ralph-lisa start "your task"');
560
+ console.log(line());
561
+ }
562
+ function findTemplatesDir() {
563
+ // Look for templates relative to the CLI package
564
+ const candidates = [
565
+ // When installed via npm (templates shipped in package)
566
+ path.join(__dirname, "..", "templates"),
567
+ // When running from repo
568
+ path.join(__dirname, "..", "..", "templates"),
569
+ // Repo root
570
+ path.join(__dirname, "..", "..", "..", "templates"),
571
+ ];
572
+ for (const c of candidates) {
573
+ if (fs.existsSync(path.join(c, "roles", "ralph.md"))) {
574
+ return c;
575
+ }
576
+ }
577
+ console.error("Error: Templates directory not found. Reinstall ralph-lisa-loop.");
578
+ process.exit(1);
579
+ }
580
+ // ─── start ───────────────────────────────────────
581
+ function cmdStart(args) {
582
+ const projectDir = process.cwd();
583
+ const fullAuto = args.includes("--full-auto");
584
+ const filteredArgs = args.filter((a) => a !== "--full-auto");
585
+ const task = filteredArgs.join(" ");
586
+ const claudeCmd = fullAuto ? "claude --dangerously-skip-permissions" : "claude";
587
+ const codexCmd = fullAuto ? "codex --full-auto" : "codex";
588
+ console.log(line());
589
+ console.log("Ralph-Lisa Loop - Start");
590
+ console.log(line());
591
+ console.log(`Project: ${projectDir}`);
592
+ if (fullAuto)
593
+ console.log("Mode: FULL AUTO (no permission prompts)");
594
+ console.log("");
595
+ // Check prerequisites
596
+ const { execSync } = require("node:child_process");
597
+ try {
598
+ execSync("which claude", { stdio: "pipe" });
599
+ }
600
+ catch {
601
+ console.error("Error: 'claude' command not found. Install Claude Code first.");
602
+ process.exit(1);
603
+ }
604
+ try {
605
+ execSync("which codex", { stdio: "pipe" });
606
+ }
607
+ catch {
608
+ console.error("Error: 'codex' command not found. Install Codex CLI first.");
609
+ process.exit(1);
610
+ }
611
+ // Check if initialized (full init has CLAUDE.md marker, minimal has .dual-agent/)
612
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
613
+ const hasFullInit = fs.existsSync(claudeMd) && (0, state_js_1.readFile)(claudeMd).includes(MARKER);
614
+ const hasSession = fs.existsSync(path.join(projectDir, state_js_1.STATE_DIR));
615
+ if (!hasFullInit && !hasSession) {
616
+ console.error("Error: Not initialized. Run 'ralph-lisa init' first.");
617
+ process.exit(1);
618
+ }
619
+ // Initialize task if provided
620
+ if (task) {
621
+ console.log(`Task: ${task}`);
622
+ cmdInit(task.split(" "));
623
+ console.log("");
624
+ }
625
+ // Detect terminal and launch
626
+ const platform = process.platform;
627
+ const ralphCmd = `cd '${projectDir}' && echo '=== Ralph (Claude Code) ===' && echo 'Commands: /check-turn, /submit-work, /view-status' && echo 'First: /check-turn' && echo '' && ${claudeCmd}`;
628
+ const lisaCmd = `cd '${projectDir}' && echo '=== Lisa (Codex) ===' && echo 'First: ralph-lisa whose-turn' && echo '' && ${codexCmd}`;
629
+ if (platform === "darwin") {
630
+ try {
631
+ // Try iTerm2 first
632
+ execSync("pgrep -x iTerm2", { stdio: "pipe" });
633
+ console.log("Launching with iTerm2...");
634
+ execSync(`osascript -e 'tell application "iTerm"
635
+ activate
636
+ set ralphWindow to (create window with default profile)
637
+ tell current session of ralphWindow
638
+ write text "${ralphCmd.replace(/"/g, '\\"')}"
639
+ set name to "Ralph (Claude)"
640
+ end tell
641
+ tell current window
642
+ set lisaTab to (create tab with default profile)
643
+ tell current session of lisaTab
644
+ write text "${lisaCmd.replace(/"/g, '\\"')}"
645
+ set name to "Lisa (Codex)"
646
+ end tell
647
+ end tell
648
+ end tell'`, { stdio: "pipe" });
649
+ }
650
+ catch {
651
+ // Fall back to Terminal.app
652
+ console.log("Launching with macOS Terminal...");
653
+ try {
654
+ execSync(`osascript -e 'tell application "Terminal"
655
+ activate
656
+ do script "${ralphCmd.replace(/"/g, '\\"')}"
657
+ end tell'`, { stdio: "pipe" });
658
+ execSync("sleep 1");
659
+ execSync(`osascript -e 'tell application "Terminal"
660
+ activate
661
+ do script "${lisaCmd.replace(/"/g, '\\"')}"
662
+ end tell'`, { stdio: "pipe" });
663
+ }
664
+ catch {
665
+ launchGeneric(projectDir);
666
+ return;
667
+ }
668
+ }
669
+ }
670
+ else {
671
+ // Try tmux
672
+ try {
673
+ execSync("which tmux", { stdio: "pipe" });
674
+ console.log("Launching with tmux...");
675
+ const sessionName = "ralph-lisa";
676
+ execSync(`tmux kill-session -t "${sessionName}" 2>/dev/null || true`);
677
+ execSync(`tmux new-session -d -s "${sessionName}" -n "Ralph" "bash -c '${ralphCmd}; exec bash'"`);
678
+ execSync(`tmux split-window -h -t "${sessionName}" "bash -c '${lisaCmd}; exec bash'"`);
679
+ execSync(`tmux attach-session -t "${sessionName}"`, { stdio: "inherit" });
680
+ }
681
+ catch {
682
+ launchGeneric(projectDir);
683
+ return;
684
+ }
685
+ }
686
+ console.log("");
687
+ console.log(line());
688
+ console.log("Both agents launched!");
689
+ console.log(line());
690
+ const currentTurn = (0, state_js_1.readFile)(path.join(projectDir, state_js_1.STATE_DIR, "turn.txt")) || "ralph";
691
+ console.log(`Current turn: ${currentTurn}`);
692
+ console.log(line());
693
+ }
694
+ function launchGeneric(projectDir) {
695
+ console.log("Please manually open two terminals:");
696
+ console.log("");
697
+ console.log("Terminal 1 (Ralph):");
698
+ console.log(` cd ${projectDir} && claude`);
699
+ console.log("");
700
+ console.log("Terminal 2 (Lisa):");
701
+ console.log(` cd ${projectDir} && codex`);
702
+ }
703
+ // ─── auto ────────────────────────────────────────
704
+ function cmdAuto(args) {
705
+ const projectDir = process.cwd();
706
+ const fullAuto = args.includes("--full-auto");
707
+ const filteredArgs = args.filter((a) => a !== "--full-auto");
708
+ const task = filteredArgs.join(" ");
709
+ const claudeCmd = fullAuto ? "claude --dangerously-skip-permissions" : "claude";
710
+ const codexCmd = fullAuto ? "codex --full-auto" : "codex";
711
+ console.log(line());
712
+ console.log("Ralph-Lisa Loop - Auto Mode");
713
+ console.log(line());
714
+ console.log(`Project: ${projectDir}`);
715
+ if (fullAuto)
716
+ console.log("Mode: FULL AUTO (no permission prompts)");
717
+ console.log("");
718
+ const { execSync } = require("node:child_process");
719
+ // Check prerequisites
720
+ try {
721
+ execSync("which tmux", { stdio: "pipe" });
722
+ }
723
+ catch {
724
+ console.error("Error: tmux is required for auto mode.");
725
+ console.error("Install: brew install tmux (macOS) or apt install tmux (Linux)");
726
+ process.exit(1);
727
+ }
728
+ try {
729
+ execSync("which claude", { stdio: "pipe" });
730
+ }
731
+ catch {
732
+ console.error("Error: 'claude' command not found.");
733
+ process.exit(1);
734
+ }
735
+ try {
736
+ execSync("which codex", { stdio: "pipe" });
737
+ }
738
+ catch {
739
+ console.error("Error: 'codex' command not found.");
740
+ process.exit(1);
741
+ }
742
+ // Check file watcher
743
+ let watcher = "";
744
+ try {
745
+ execSync("which fswatch", { stdio: "pipe" });
746
+ watcher = "fswatch";
747
+ }
748
+ catch {
749
+ try {
750
+ execSync("which inotifywait", { stdio: "pipe" });
751
+ watcher = "inotifywait";
752
+ }
753
+ catch {
754
+ console.error("Error: File watcher required.");
755
+ console.error("Install: brew install fswatch (macOS) or apt install inotify-tools (Linux)");
756
+ process.exit(1);
757
+ }
758
+ }
759
+ // Check if initialized (full init has CLAUDE.md marker, minimal has .dual-agent/)
760
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
761
+ const hasFullInit = fs.existsSync(claudeMd) && (0, state_js_1.readFile)(claudeMd).includes(MARKER);
762
+ const hasSession = fs.existsSync(path.join(projectDir, state_js_1.STATE_DIR));
763
+ if (!hasFullInit && !hasSession) {
764
+ console.error("Error: Not initialized. Run 'ralph-lisa init' first.");
765
+ process.exit(1);
766
+ }
767
+ // Initialize task
768
+ if (task) {
769
+ console.log(`Task: ${task}`);
770
+ cmdInit(task.split(" "));
771
+ console.log("");
772
+ }
773
+ const sessionName = "ralph-lisa-auto";
774
+ const dir = (0, state_js_1.stateDir)(projectDir);
775
+ fs.mkdirSync(dir, { recursive: true });
776
+ // Create watcher script
777
+ const watcherScript = path.join(dir, "watcher.sh");
778
+ let watcherContent = `#!/bin/bash
779
+ # Turn watcher - triggers agents on turn change
780
+
781
+ STATE_DIR=".dual-agent"
782
+ LAST_TURN=""
783
+
784
+ send_go_to_pane() {
785
+ local pane="$1"
786
+ local max_retries=3
787
+ local attempt=0
788
+ while (( attempt < max_retries )); do
789
+ tmux send-keys -t ${sessionName}:\${pane} -l "go" 2>/dev/null || true
790
+ sleep 3
791
+ tmux send-keys -t ${sessionName}:\${pane} Enter 2>/dev/null || true
792
+ sleep 2
793
+ # Check if "go" is still sitting in the input (Enter didn't register)
794
+ local pane_content
795
+ pane_content=$(tmux capture-pane -t ${sessionName}:\${pane} -p 2>/dev/null | tail -3)
796
+ if echo "$pane_content" | grep -Eq "^(> |❯ |› )go$"; then
797
+ attempt=$((attempt + 1))
798
+ echo "[Watcher] Retry $attempt: Enter not registered on pane \${pane}"
799
+ # Clear the stuck "go" text and retry
800
+ tmux send-keys -t ${sessionName}:\${pane} C-u 2>/dev/null || true
801
+ sleep 1
802
+ else
803
+ break
804
+ fi
805
+ done
806
+ }
807
+
808
+ trigger_agent() {
809
+ local turn="$1"
810
+ if [[ "$turn" == "ralph" ]]; then
811
+ send_go_to_pane "0.0"
812
+ elif [[ "$turn" == "lisa" ]]; then
813
+ send_go_to_pane "0.1"
814
+ fi
815
+ }
816
+
817
+ check_and_trigger() {
818
+ if [[ -f "$STATE_DIR/turn.txt" ]]; then
819
+ CURRENT_TURN=$(cat "$STATE_DIR/turn.txt" 2>/dev/null || echo "")
820
+ if [[ -n "$CURRENT_TURN" && "$CURRENT_TURN" != "$LAST_TURN" ]]; then
821
+ echo "[Watcher] Turn changed: $LAST_TURN -> $CURRENT_TURN"
822
+ LAST_TURN="$CURRENT_TURN"
823
+ sleep 5
824
+ trigger_agent "$CURRENT_TURN"
825
+ fi
826
+ fi
827
+ }
828
+
829
+ echo "[Watcher] Starting... (Ctrl+C to stop)"
830
+ echo "[Watcher] Monitoring $STATE_DIR/turn.txt"
831
+
832
+ sleep 2
833
+ check_and_trigger
834
+
835
+ `;
836
+ if (watcher === "fswatch") {
837
+ watcherContent += `fswatch -o "$STATE_DIR/turn.txt" 2>/dev/null | while read; do
838
+ check_and_trigger
839
+ done
840
+ `;
841
+ }
842
+ else if (watcher === "inotifywait") {
843
+ watcherContent += `while inotifywait -e modify "$STATE_DIR/turn.txt" 2>/dev/null; do
844
+ check_and_trigger
845
+ done
846
+ `;
847
+ }
848
+ else {
849
+ watcherContent += `while true; do
850
+ check_and_trigger
851
+ sleep 2
852
+ done
853
+ `;
854
+ }
855
+ (0, state_js_1.writeFile)(watcherScript, watcherContent);
856
+ fs.chmodSync(watcherScript, 0o755);
857
+ // Launch tmux session
858
+ // Layout: Ralph (left) | Lisa (right), Watcher runs in background
859
+ execSync(`tmux kill-session -t "${sessionName}" 2>/dev/null || true`);
860
+ // Pane 0: Ralph (left), Pane 1: Lisa (right)
861
+ execSync(`tmux new-session -d -s "${sessionName}" -n "main" -c "${projectDir}"`);
862
+ execSync(`tmux split-window -h -t "${sessionName}" -c "${projectDir}"`);
863
+ // Pane 0 = Ralph (left), Pane 1 = Lisa (right)
864
+ execSync(`tmux send-keys -t "${sessionName}:0.0" "echo '=== Ralph (Claude Code) ===' && ${claudeCmd}" Enter`);
865
+ execSync(`tmux send-keys -t "${sessionName}:0.1" "echo '=== Lisa (Codex) ===' && ${codexCmd}" Enter`);
866
+ execSync(`tmux select-pane -t "${sessionName}:0.0"`);
867
+ // Watcher runs in background (logs to .dual-agent/watcher.log)
868
+ const watcherLog = path.join(dir, "watcher.log");
869
+ execSync(`bash -c 'nohup "${watcherScript}" > "${watcherLog}" 2>&1 &'`);
870
+ console.log("");
871
+ console.log(line());
872
+ console.log("Auto Mode Started!");
873
+ console.log(line());
874
+ console.log("");
875
+ console.log("Layout:");
876
+ console.log(" +-----------+-----------+");
877
+ console.log(" | Ralph | Lisa |");
878
+ console.log(" | (Claude) | (Codex) |");
879
+ console.log(" +-----------+-----------+");
880
+ console.log(" Watcher runs in background (log: .dual-agent/watcher.log)");
881
+ console.log("");
882
+ console.log("Attaching to session...");
883
+ console.log(line());
884
+ execSync(`tmux attach-session -t "${sessionName}"`, { stdio: "inherit" });
885
+ }
886
+ // ─── policy ──────────────────────────────────────
887
+ function cmdPolicy(args) {
888
+ const sub = args[0];
889
+ if (sub === "check-consensus") {
890
+ cmdPolicyCheckConsensus();
891
+ return;
892
+ }
893
+ if (sub === "check-next-step") {
894
+ cmdPolicyCheckNextStep();
895
+ return;
896
+ }
897
+ if (sub !== "check") {
898
+ console.error("Usage:");
899
+ console.error(" ralph-lisa policy check <ralph|lisa>");
900
+ console.error(" ralph-lisa policy check-consensus");
901
+ console.error(" ralph-lisa policy check-next-step");
902
+ process.exit(1);
903
+ }
904
+ const role = args[1];
905
+ if (role !== "ralph" && role !== "lisa") {
906
+ console.error("Usage: ralph-lisa policy check <ralph|lisa>");
907
+ process.exit(1);
908
+ }
909
+ (0, state_js_1.checkSession)();
910
+ const dir = (0, state_js_1.stateDir)();
911
+ const file = role === "ralph" ? "work.md" : "review.md";
912
+ const raw = (0, state_js_1.readFile)(path.join(dir, file));
913
+ if (!raw) {
914
+ console.log("No submission to check.");
915
+ return;
916
+ }
917
+ const content = extractSubmissionContent(raw);
918
+ if (!content) {
919
+ console.log("No submission content found.");
920
+ return;
921
+ }
922
+ const tag = (0, state_js_1.extractTag)(content);
923
+ if (!tag) {
924
+ console.log("No valid tag found in submission.");
925
+ return;
926
+ }
927
+ const violations = role === "ralph" ? (0, policy_js_1.checkRalph)(tag, content) : (0, policy_js_1.checkLisa)(tag, content);
928
+ if (violations.length === 0) {
929
+ console.log("Policy check passed.");
930
+ return;
931
+ }
932
+ console.error("");
933
+ console.error("⚠️ Policy violations:");
934
+ for (const v of violations) {
935
+ console.error(` - ${v.message}`);
936
+ }
937
+ console.error("");
938
+ // Standalone policy check always exits non-zero on violations,
939
+ // regardless of RL_POLICY_MODE. This is a hard gate for use in
940
+ // scripts/hooks. RL_POLICY_MODE only affects inline checks during submit.
941
+ process.exit(1);
942
+ }
943
+ /**
944
+ * Check if the most recent round has both agents submitting [CONSENSUS].
945
+ */
946
+ function cmdPolicyCheckConsensus() {
947
+ (0, state_js_1.checkSession)();
948
+ const dir = (0, state_js_1.stateDir)();
949
+ const workRaw = (0, state_js_1.readFile)(path.join(dir, "work.md"));
950
+ const reviewRaw = (0, state_js_1.readFile)(path.join(dir, "review.md"));
951
+ const workContent = extractSubmissionContent(workRaw);
952
+ const reviewContent = extractSubmissionContent(reviewRaw);
953
+ const workTag = workContent ? (0, state_js_1.extractTag)(workContent) : "";
954
+ const reviewTag = reviewContent ? (0, state_js_1.extractTag)(reviewContent) : "";
955
+ const issues = [];
956
+ if (workTag !== "CONSENSUS") {
957
+ issues.push(`Ralph's latest submission is [${workTag || "none"}], not [CONSENSUS].`);
958
+ }
959
+ if (reviewTag !== "CONSENSUS") {
960
+ issues.push(`Lisa's latest submission is [${reviewTag || "none"}], not [CONSENSUS].`);
961
+ }
962
+ if (issues.length === 0) {
963
+ console.log("Consensus reached: both agents submitted [CONSENSUS].");
964
+ return;
965
+ }
966
+ console.error("Consensus NOT reached:");
967
+ for (const issue of issues) {
968
+ console.error(` - ${issue}`);
969
+ }
970
+ process.exit(1);
971
+ }
972
+ /**
973
+ * Comprehensive check for proceeding to the next step:
974
+ * 1. Both agents have submitted [CONSENSUS]
975
+ * 2. Ralph's submission passes policy checks
976
+ * 3. Lisa's submission passes policy checks
977
+ */
978
+ function cmdPolicyCheckNextStep() {
979
+ (0, state_js_1.checkSession)();
980
+ const dir = (0, state_js_1.stateDir)();
981
+ const workRaw = (0, state_js_1.readFile)(path.join(dir, "work.md"));
982
+ const reviewRaw = (0, state_js_1.readFile)(path.join(dir, "review.md"));
983
+ const workContent = extractSubmissionContent(workRaw);
984
+ const reviewContent = extractSubmissionContent(reviewRaw);
985
+ const workTag = workContent ? (0, state_js_1.extractTag)(workContent) : "";
986
+ const reviewTag = reviewContent ? (0, state_js_1.extractTag)(reviewContent) : "";
987
+ const allIssues = [];
988
+ // 1. Consensus check
989
+ if (workTag !== "CONSENSUS") {
990
+ allIssues.push(`Ralph's latest is [${workTag || "none"}], not [CONSENSUS].`);
991
+ }
992
+ if (reviewTag !== "CONSENSUS") {
993
+ allIssues.push(`Lisa's latest is [${reviewTag || "none"}], not [CONSENSUS].`);
994
+ }
995
+ // 2. Policy checks on latest submissions (if content exists)
996
+ if (workContent && workTag) {
997
+ const rv = (0, policy_js_1.checkRalph)(workTag, workContent);
998
+ for (const v of rv)
999
+ allIssues.push(`Ralph: ${v.message}`);
1000
+ }
1001
+ if (reviewContent && reviewTag) {
1002
+ const lv = (0, policy_js_1.checkLisa)(reviewTag, reviewContent);
1003
+ for (const v of lv)
1004
+ allIssues.push(`Lisa: ${v.message}`);
1005
+ }
1006
+ if (allIssues.length === 0) {
1007
+ console.log("Ready to proceed: consensus reached and all checks pass.");
1008
+ return;
1009
+ }
1010
+ console.error("Not ready to proceed:");
1011
+ for (const issue of allIssues) {
1012
+ console.error(` - ${issue}`);
1013
+ }
1014
+ process.exit(1);
1015
+ }
1016
+ /**
1017
+ * Extract the actual submission content from work.md/review.md.
1018
+ * The file has metadata headers; the submission content is the part
1019
+ * that starts with a [TAG] line.
1020
+ */
1021
+ function extractSubmissionContent(raw) {
1022
+ const lines = raw.split("\n");
1023
+ for (let i = 0; i < lines.length; i++) {
1024
+ if ((0, state_js_1.extractTag)(lines[i])) {
1025
+ return lines.slice(i).join("\n");
1026
+ }
1027
+ }
1028
+ return "";
1029
+ }