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.
- package/README.md +226 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +121 -0
- package/dist/commands.d.ts +19 -0
- package/dist/commands.js +1029 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/policy.d.ts +28 -0
- package/dist/policy.js +101 -0
- package/dist/state.d.ts +24 -0
- package/dist/state.js +150 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +134 -0
- package/dist/test/policy.test.d.ts +1 -0
- package/dist/test/policy.test.js +107 -0
- package/dist/test/state.test.d.ts +1 -0
- package/dist/test/state.test.js +82 -0
- package/package.json +44 -0
- package/templates/claude-commands/check-turn.md +18 -0
- package/templates/claude-commands/next-step.md +23 -0
- package/templates/claude-commands/read-review.md +11 -0
- package/templates/claude-commands/submit-work.md +39 -0
- package/templates/claude-commands/view-status.md +18 -0
- package/templates/codex-skills/check-turn.md +16 -0
- package/templates/codex-skills/read-work.md +9 -0
- package/templates/codex-skills/submit-review.md +36 -0
- package/templates/codex-skills/view-status.md +16 -0
- package/templates/ralph-prompt.md +59 -0
- package/templates/roles/lisa.md +115 -0
- package/templates/roles/ralph.md +117 -0
- package/templates/skill.json +27 -0
package/dist/commands.js
ADDED
|
@@ -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
|
+
}
|