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,594 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const node_test_1 = require("node:test");
37
+ const assert = __importStar(require("node:assert"));
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const node_child_process_1 = require("node:child_process");
41
+ const CLI = path.join(__dirname, "..", "cli.js");
42
+ const TMP = path.join(__dirname, "..", "..", ".test-tmp");
43
+ function run(...args) {
44
+ try {
45
+ const stdout = (0, node_child_process_1.execFileSync)(process.execPath, [CLI, ...args], {
46
+ cwd: TMP,
47
+ encoding: "utf-8",
48
+ env: { ...process.env, RL_POLICY_MODE: "off" },
49
+ });
50
+ return { stdout, exitCode: 0 };
51
+ }
52
+ catch (e) {
53
+ return { stdout: (e.stdout || "") + (e.stderr || ""), exitCode: e.status };
54
+ }
55
+ }
56
+ (0, node_test_1.describe)("CLI: policy check-consensus", () => {
57
+ (0, node_test_1.beforeEach)(() => {
58
+ fs.rmSync(TMP, { recursive: true, force: true });
59
+ fs.mkdirSync(TMP, { recursive: true });
60
+ run("init", "--minimal");
61
+ });
62
+ (0, node_test_1.afterEach)(() => {
63
+ fs.rmSync(TMP, { recursive: true, force: true });
64
+ });
65
+ (0, node_test_1.it)("passes when both agents submit CONSENSUS", () => {
66
+ run("submit-ralph", "[CONSENSUS] Agreed on plan");
67
+ run("submit-lisa", "[CONSENSUS] Agreed\n\n- All good");
68
+ const r = run("policy", "check-consensus");
69
+ assert.strictEqual(r.exitCode, 0);
70
+ assert.ok(r.stdout.includes("Consensus reached"));
71
+ });
72
+ (0, node_test_1.it)("fails when neither agent has CONSENSUS", () => {
73
+ run("submit-ralph", "[CODE] Done\n\nTest Results\n- pass");
74
+ run("submit-lisa", "[PASS] OK\n\n- Clean code");
75
+ const r = run("policy", "check-consensus");
76
+ assert.strictEqual(r.exitCode, 1);
77
+ assert.ok(r.stdout.includes("NOT reached"));
78
+ });
79
+ (0, node_test_1.it)("fails when only Ralph has CONSENSUS", () => {
80
+ run("submit-ralph", "[CONSENSUS] I agree");
81
+ run("submit-lisa", "[PASS] OK\n\n- Looks good");
82
+ const r = run("policy", "check-consensus");
83
+ assert.strictEqual(r.exitCode, 1);
84
+ assert.ok(r.stdout.includes("Lisa"));
85
+ });
86
+ });
87
+ (0, node_test_1.describe)("CLI: policy check-next-step", () => {
88
+ (0, node_test_1.beforeEach)(() => {
89
+ fs.rmSync(TMP, { recursive: true, force: true });
90
+ fs.mkdirSync(TMP, { recursive: true });
91
+ run("init", "--minimal");
92
+ });
93
+ (0, node_test_1.afterEach)(() => {
94
+ fs.rmSync(TMP, { recursive: true, force: true });
95
+ });
96
+ (0, node_test_1.it)("passes when both CONSENSUS and policy OK", () => {
97
+ run("submit-ralph", "[CONSENSUS] Agreed on approach");
98
+ run("submit-lisa", "[CONSENSUS] Confirmed\n\n- Sound plan");
99
+ const r = run("policy", "check-next-step");
100
+ assert.strictEqual(r.exitCode, 0);
101
+ assert.ok(r.stdout.includes("Ready to proceed"));
102
+ });
103
+ (0, node_test_1.it)("fails with comprehensive issues", () => {
104
+ run("submit-ralph", "[CODE] Done");
105
+ run("submit-lisa", "[PASS] OK");
106
+ const r = run("policy", "check-next-step");
107
+ assert.strictEqual(r.exitCode, 1);
108
+ // Should report: no consensus + missing test results + missing reason
109
+ assert.ok(r.stdout.includes("not [CONSENSUS]"));
110
+ });
111
+ });
112
+ (0, node_test_1.describe)("CLI: doctor", () => {
113
+ (0, node_test_1.beforeEach)(() => {
114
+ fs.rmSync(TMP, { recursive: true, force: true });
115
+ fs.mkdirSync(TMP, { recursive: true });
116
+ });
117
+ (0, node_test_1.afterEach)(() => {
118
+ fs.rmSync(TMP, { recursive: true, force: true });
119
+ });
120
+ (0, node_test_1.it)("runs and outputs dependency check header", () => {
121
+ const r = run("doctor");
122
+ assert.strictEqual(r.exitCode, 0);
123
+ assert.ok(r.stdout.includes("Dependency Check"));
124
+ });
125
+ (0, node_test_1.it)("outputs at least one status line", () => {
126
+ const r = run("doctor");
127
+ assert.strictEqual(r.exitCode, 0);
128
+ // Should have at least one OK, MISSING, or -- line
129
+ assert.ok(r.stdout.includes("OK") ||
130
+ r.stdout.includes("MISSING") ||
131
+ r.stdout.includes("--"));
132
+ });
133
+ (0, node_test_1.it)("outputs Node.js version", () => {
134
+ const r = run("doctor");
135
+ assert.ok(r.stdout.includes("Node.js"));
136
+ });
137
+ });
138
+ (0, node_test_1.describe)("CLI: logs", () => {
139
+ (0, node_test_1.beforeEach)(() => {
140
+ fs.rmSync(TMP, { recursive: true, force: true });
141
+ fs.mkdirSync(TMP, { recursive: true });
142
+ run("init", "--minimal");
143
+ });
144
+ (0, node_test_1.afterEach)(() => {
145
+ fs.rmSync(TMP, { recursive: true, force: true });
146
+ });
147
+ (0, node_test_1.it)("lists no logs when none exist", () => {
148
+ const r = run("logs");
149
+ assert.strictEqual(r.exitCode, 0);
150
+ assert.ok(r.stdout.includes("No transcript logs found"));
151
+ });
152
+ (0, node_test_1.it)("lists live pane logs when present", () => {
153
+ const logFile = path.join(TMP, ".dual-agent", "pane0.log");
154
+ fs.writeFileSync(logFile, "some output from ralph\n");
155
+ const r = run("logs");
156
+ assert.strictEqual(r.exitCode, 0);
157
+ assert.ok(r.stdout.includes("Live (current session)"));
158
+ assert.ok(r.stdout.includes("pane0.log"));
159
+ });
160
+ (0, node_test_1.it)("lists archived logs", () => {
161
+ const logsDir = path.join(TMP, ".dual-agent", "logs");
162
+ fs.mkdirSync(logsDir, { recursive: true });
163
+ fs.writeFileSync(path.join(logsDir, "pane0-2026-01-01T12-00-00.log"), "archived output\n");
164
+ const r = run("logs");
165
+ assert.strictEqual(r.exitCode, 0);
166
+ assert.ok(r.stdout.includes("Archived (previous sessions)"));
167
+ assert.ok(r.stdout.includes("pane0-2026-01-01T12-00-00.log"));
168
+ });
169
+ (0, node_test_1.it)("cat shows live pane log content", () => {
170
+ const logFile = path.join(TMP, ".dual-agent", "pane0.log");
171
+ fs.writeFileSync(logFile, "hello from ralph pane\n");
172
+ const r = run("logs", "cat");
173
+ assert.strictEqual(r.exitCode, 0);
174
+ assert.ok(r.stdout.includes("hello from ralph pane"));
175
+ });
176
+ (0, node_test_1.it)("cat shows specific archived log", () => {
177
+ const logsDir = path.join(TMP, ".dual-agent", "logs");
178
+ fs.mkdirSync(logsDir, { recursive: true });
179
+ const archiveFile = "pane1-2026-01-01T12-00-00.log";
180
+ fs.writeFileSync(path.join(logsDir, archiveFile), "archived lisa output\n");
181
+ const r = run("logs", "cat", archiveFile);
182
+ assert.strictEqual(r.exitCode, 0);
183
+ assert.ok(r.stdout.includes("archived lisa output"));
184
+ });
185
+ (0, node_test_1.it)("ignores empty pane logs in listing", () => {
186
+ const logFile = path.join(TMP, ".dual-agent", "pane0.log");
187
+ fs.writeFileSync(logFile, ""); // empty
188
+ const r = run("logs");
189
+ assert.strictEqual(r.exitCode, 0);
190
+ assert.ok(!r.stdout.includes("pane0.log"));
191
+ });
192
+ });
193
+ (0, node_test_1.describe)("CLI: init --minimal", () => {
194
+ (0, node_test_1.beforeEach)(() => {
195
+ fs.rmSync(TMP, { recursive: true, force: true });
196
+ fs.mkdirSync(TMP, { recursive: true });
197
+ });
198
+ (0, node_test_1.afterEach)(() => {
199
+ fs.rmSync(TMP, { recursive: true, force: true });
200
+ });
201
+ (0, node_test_1.it)("creates only .dual-agent/, no project files", () => {
202
+ run("init", "--minimal");
203
+ assert.ok(fs.existsSync(path.join(TMP, ".dual-agent", "turn.txt")));
204
+ assert.ok(!fs.existsSync(path.join(TMP, "CLAUDE.md")));
205
+ assert.ok(!fs.existsSync(path.join(TMP, "CODEX.md")));
206
+ assert.ok(!fs.existsSync(path.join(TMP, ".claude")));
207
+ assert.ok(!fs.existsSync(path.join(TMP, ".codex")));
208
+ });
209
+ (0, node_test_1.it)("allows submit after minimal init", () => {
210
+ run("init", "--minimal");
211
+ const r = run("submit-ralph", "[PLAN] Test plan");
212
+ assert.strictEqual(r.exitCode, 0);
213
+ assert.ok(r.stdout.includes("Submitted"));
214
+ });
215
+ });
216
+ (0, node_test_1.describe)("CLI: submit --file", () => {
217
+ (0, node_test_1.beforeEach)(() => {
218
+ fs.rmSync(TMP, { recursive: true, force: true });
219
+ fs.mkdirSync(TMP, { recursive: true });
220
+ run("init", "--minimal");
221
+ });
222
+ (0, node_test_1.afterEach)(() => {
223
+ fs.rmSync(TMP, { recursive: true, force: true });
224
+ });
225
+ (0, node_test_1.it)("ralph submits content from file", () => {
226
+ const contentFile = path.join(TMP, "submission.md");
227
+ fs.writeFileSync(contentFile, "[PLAN] My plan from file\n\nDetailed plan content here.");
228
+ const r = run("submit-ralph", "--file", contentFile);
229
+ assert.strictEqual(r.exitCode, 0);
230
+ assert.ok(r.stdout.includes("Submitted"));
231
+ assert.ok(r.stdout.includes("[PLAN]"));
232
+ // Verify work.md contains the file content
233
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
234
+ assert.ok(work.includes("Detailed plan content here"));
235
+ });
236
+ (0, node_test_1.it)("lisa submits content from file", () => {
237
+ // Ralph submits first to give Lisa the turn
238
+ run("submit-ralph", "[PLAN] Setup");
239
+ const contentFile = path.join(TMP, "review.md");
240
+ fs.writeFileSync(contentFile, "[PASS] Approved from file\n\nLooks good to me.");
241
+ const r = run("submit-lisa", "--file", contentFile);
242
+ assert.strictEqual(r.exitCode, 0);
243
+ assert.ok(r.stdout.includes("Submitted"));
244
+ assert.ok(r.stdout.includes("[PASS]"));
245
+ });
246
+ (0, node_test_1.it)("fails when --file has no path", () => {
247
+ const r = run("submit-ralph", "--file");
248
+ assert.notStrictEqual(r.exitCode, 0);
249
+ assert.ok(r.stdout.includes("--file requires a file path"));
250
+ });
251
+ (0, node_test_1.it)("fails when file does not exist", () => {
252
+ const r = run("submit-ralph", "--file", "/nonexistent/path.md");
253
+ assert.notStrictEqual(r.exitCode, 0);
254
+ assert.ok(r.stdout.includes("File not found"));
255
+ });
256
+ (0, node_test_1.it)("handles file with special characters in content", () => {
257
+ const contentFile = path.join(TMP, "special.md");
258
+ fs.writeFileSync(contentFile, '[PLAN] Plan with "quotes" & $pecial chars\n\nContent with `backticks` and $(subshell)');
259
+ const r = run("submit-ralph", "--file", contentFile);
260
+ assert.strictEqual(r.exitCode, 0);
261
+ assert.ok(r.stdout.includes("Submitted"));
262
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
263
+ assert.ok(work.includes("$(subshell)"));
264
+ });
265
+ (0, node_test_1.it)("--file history gets summary only, not full content", () => {
266
+ const contentFile = path.join(TMP, "submission.md");
267
+ fs.writeFileSync(contentFile, "[PLAN] File plan\n\nThis detailed body should NOT be in history.");
268
+ run("submit-ralph", "--file", contentFile);
269
+ const history = fs.readFileSync(path.join(TMP, ".dual-agent", "history.md"), "utf-8");
270
+ // History should have summary reference, not full body
271
+ assert.ok(history.includes("File plan"));
272
+ assert.ok(history.includes("(Full content in work.md)"));
273
+ assert.ok(!history.includes("This detailed body should NOT be in history"));
274
+ });
275
+ (0, node_test_1.it)("--file full content is still in work.md", () => {
276
+ const contentFile = path.join(TMP, "submission.md");
277
+ fs.writeFileSync(contentFile, "[PLAN] File plan\n\nFull body lives in work.md.");
278
+ run("submit-ralph", "--file", contentFile);
279
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
280
+ assert.ok(work.includes("Full body lives in work.md"));
281
+ });
282
+ (0, node_test_1.it)("inline args still write full content to history", () => {
283
+ const r = run("submit-ralph", "[PLAN] Inline plan\n\nInline body in history.");
284
+ assert.strictEqual(r.exitCode, 0);
285
+ const history = fs.readFileSync(path.join(TMP, ".dual-agent", "history.md"), "utf-8");
286
+ assert.ok(history.includes("Inline body in history"));
287
+ assert.ok(!history.includes("(Full content in work.md)"));
288
+ });
289
+ });
290
+ (0, node_test_1.describe)("CLI: submit files_changed", () => {
291
+ (0, node_test_1.beforeEach)(() => {
292
+ fs.rmSync(TMP, { recursive: true, force: true });
293
+ fs.mkdirSync(TMP, { recursive: true });
294
+ // Set up a git repo so git diff works
295
+ (0, node_child_process_1.execFileSync)("git", ["init"], { cwd: TMP, stdio: "pipe" });
296
+ (0, node_child_process_1.execFileSync)("git", ["config", "user.email", "test@test.com"], { cwd: TMP, stdio: "pipe" });
297
+ (0, node_child_process_1.execFileSync)("git", ["config", "user.name", "Test"], { cwd: TMP, stdio: "pipe" });
298
+ // Create and commit an initial file
299
+ fs.writeFileSync(path.join(TMP, "app.ts"), "const x = 1;\n");
300
+ (0, node_child_process_1.execFileSync)("git", ["add", "app.ts"], { cwd: TMP, stdio: "pipe" });
301
+ (0, node_child_process_1.execFileSync)("git", ["commit", "-m", "initial"], { cwd: TMP, stdio: "pipe" });
302
+ run("init", "--minimal");
303
+ });
304
+ (0, node_test_1.afterEach)(() => {
305
+ fs.rmSync(TMP, { recursive: true, force: true });
306
+ });
307
+ (0, node_test_1.it)("attaches files_changed to work.md for CODE tag", () => {
308
+ // Modify a tracked file
309
+ fs.writeFileSync(path.join(TMP, "app.ts"), "const x = 2;\n");
310
+ const r = run("submit-ralph", "[CODE] Updated app\n\nTest Results\n- pass");
311
+ assert.strictEqual(r.exitCode, 0);
312
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
313
+ assert.ok(work.includes("**Files Changed**:"));
314
+ assert.ok(work.includes("- app.ts"));
315
+ });
316
+ (0, node_test_1.it)("attaches files_changed to work.md for FIX tag", () => {
317
+ fs.writeFileSync(path.join(TMP, "app.ts"), "const x = 3;\n");
318
+ const r = run("submit-ralph", "[FIX] Fixed app\n\nTest Results\n- pass");
319
+ assert.strictEqual(r.exitCode, 0);
320
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
321
+ assert.ok(work.includes("**Files Changed**:"));
322
+ assert.ok(work.includes("- app.ts"));
323
+ });
324
+ (0, node_test_1.it)("does NOT attach files_changed for PLAN tag", () => {
325
+ fs.writeFileSync(path.join(TMP, "app.ts"), "const x = 4;\n");
326
+ const r = run("submit-ralph", "[PLAN] Just a plan");
327
+ assert.strictEqual(r.exitCode, 0);
328
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
329
+ assert.ok(!work.includes("**Files Changed**:"));
330
+ });
331
+ (0, node_test_1.it)("no files_changed section when no files changed", () => {
332
+ // No modifications — git diff should return empty
333
+ const r = run("submit-ralph", "[CODE] No changes\n\nTest Results\n- pass");
334
+ assert.strictEqual(r.exitCode, 0);
335
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
336
+ assert.ok(!work.includes("**Files Changed**:"));
337
+ });
338
+ });
339
+ (0, node_test_1.describe)("CLI: submit --stdin", () => {
340
+ (0, node_test_1.beforeEach)(() => {
341
+ fs.rmSync(TMP, { recursive: true, force: true });
342
+ fs.mkdirSync(TMP, { recursive: true });
343
+ run("init", "--minimal");
344
+ });
345
+ (0, node_test_1.afterEach)(() => {
346
+ fs.rmSync(TMP, { recursive: true, force: true });
347
+ });
348
+ function runWithStdin(input, ...args) {
349
+ try {
350
+ const stdout = (0, node_child_process_1.execFileSync)(process.execPath, [CLI, ...args], {
351
+ cwd: TMP,
352
+ encoding: "utf-8",
353
+ input,
354
+ env: { ...process.env, RL_POLICY_MODE: "off" },
355
+ });
356
+ return { stdout, exitCode: 0 };
357
+ }
358
+ catch (e) {
359
+ return { stdout: (e.stdout || "") + (e.stderr || ""), exitCode: e.status };
360
+ }
361
+ }
362
+ (0, node_test_1.it)("ralph submits content from stdin", () => {
363
+ const r = runWithStdin("[PLAN] Plan from stdin\n\nStdin content here.", "submit-ralph", "--stdin");
364
+ assert.strictEqual(r.exitCode, 0);
365
+ assert.ok(r.stdout.includes("Submitted"));
366
+ assert.ok(r.stdout.includes("[PLAN]"));
367
+ const work = fs.readFileSync(path.join(TMP, ".dual-agent", "work.md"), "utf-8");
368
+ assert.ok(work.includes("Stdin content here"));
369
+ });
370
+ (0, node_test_1.it)("lisa submits content from stdin", () => {
371
+ run("submit-ralph", "[PLAN] Setup");
372
+ const r = runWithStdin("[PASS] Approved from stdin\n\nAll good.", "submit-lisa", "--stdin");
373
+ assert.strictEqual(r.exitCode, 0);
374
+ assert.ok(r.stdout.includes("Submitted"));
375
+ assert.ok(r.stdout.includes("[PASS]"));
376
+ });
377
+ (0, node_test_1.it)("--stdin history gets summary only, not full content", () => {
378
+ runWithStdin("[PLAN] Stdin plan\n\nDetailed stdin body not in history.", "submit-ralph", "--stdin");
379
+ const history = fs.readFileSync(path.join(TMP, ".dual-agent", "history.md"), "utf-8");
380
+ assert.ok(history.includes("Stdin plan"));
381
+ assert.ok(history.includes("(Full content in work.md)"));
382
+ assert.ok(!history.includes("Detailed stdin body not in history"));
383
+ });
384
+ });
385
+ (0, node_test_1.describe)("CLI: recap", () => {
386
+ (0, node_test_1.beforeEach)(() => {
387
+ fs.rmSync(TMP, { recursive: true, force: true });
388
+ fs.mkdirSync(TMP, { recursive: true });
389
+ run("init", "--minimal");
390
+ });
391
+ (0, node_test_1.afterEach)(() => {
392
+ fs.rmSync(TMP, { recursive: true, force: true });
393
+ });
394
+ (0, node_test_1.it)("shows current step and round", () => {
395
+ const r = run("recap");
396
+ assert.strictEqual(r.exitCode, 0);
397
+ assert.ok(r.stdout.includes("RECAP"));
398
+ assert.ok(r.stdout.includes("Step: planning"));
399
+ assert.ok(r.stdout.includes("Round: 1"));
400
+ });
401
+ (0, node_test_1.it)("shows recent actions after submissions", () => {
402
+ run("submit-ralph", "[PLAN] First plan");
403
+ run("submit-lisa", "[PASS] Plan approved\n\n- Looks good");
404
+ run("submit-ralph", "[CONSENSUS] Agreed");
405
+ const r = run("recap");
406
+ assert.strictEqual(r.exitCode, 0);
407
+ assert.ok(r.stdout.includes("Recent actions:"));
408
+ assert.ok(r.stdout.includes("Ralph [PLAN] First plan"));
409
+ assert.ok(r.stdout.includes("Lisa [PASS] Plan approved"));
410
+ assert.ok(r.stdout.includes("Ralph [CONSENSUS] Agreed"));
411
+ assert.ok(r.stdout.includes("Actions in this step: 3"));
412
+ });
413
+ (0, node_test_1.it)("shows only last 3 actions when more exist", () => {
414
+ run("submit-ralph", "[PLAN] Plan A");
415
+ run("submit-lisa", "[NEEDS_WORK] Fix it\n\n- Issue found");
416
+ run("submit-ralph", "[FIX] Fixed\n\nTest Results\n- pass");
417
+ run("submit-lisa", "[PASS] Now good\n\n- All clear");
418
+ const r = run("recap");
419
+ assert.strictEqual(r.exitCode, 0);
420
+ // Should show last 3, not the first PLAN
421
+ assert.ok(!r.stdout.includes("[PLAN] Plan A"));
422
+ assert.ok(r.stdout.includes("[NEEDS_WORK] Fix it"));
423
+ assert.ok(r.stdout.includes("[FIX] Fixed"));
424
+ assert.ok(r.stdout.includes("[PASS] Now good"));
425
+ });
426
+ (0, node_test_1.it)("shows unresolved NEEDS_WORK", () => {
427
+ run("submit-ralph", "[CODE] Some code\n\nTest Results\n- pass");
428
+ run("submit-lisa", "[NEEDS_WORK] Missing edge case\n\n- Need tests");
429
+ const r = run("recap");
430
+ assert.strictEqual(r.exitCode, 0);
431
+ assert.ok(r.stdout.includes("Unresolved NEEDS_WORK:"));
432
+ assert.ok(r.stdout.includes("Missing edge case"));
433
+ });
434
+ (0, node_test_1.it)("does not show resolved NEEDS_WORK", () => {
435
+ run("submit-ralph", "[CODE] Some code\n\nTest Results\n- pass");
436
+ run("submit-lisa", "[NEEDS_WORK] Missing edge case\n\n- Need tests");
437
+ run("submit-ralph", "[FIX] Added edge case tests\n\nTest Results\n- pass");
438
+ const r = run("recap");
439
+ assert.strictEqual(r.exitCode, 0);
440
+ assert.ok(!r.stdout.includes("Unresolved NEEDS_WORK:"));
441
+ });
442
+ (0, node_test_1.it)("shows no actions when step is fresh", () => {
443
+ const r = run("recap");
444
+ assert.strictEqual(r.exitCode, 0);
445
+ assert.ok(r.stdout.includes("Actions in this step: 0"));
446
+ assert.ok(r.stdout.includes("Recent actions: (none)"));
447
+ });
448
+ });
449
+ (0, node_test_1.describe)("CLI: review-history", () => {
450
+ (0, node_test_1.beforeEach)(() => {
451
+ fs.rmSync(TMP, { recursive: true, force: true });
452
+ fs.mkdirSync(TMP, { recursive: true });
453
+ run("init", "--minimal");
454
+ });
455
+ (0, node_test_1.afterEach)(() => {
456
+ fs.rmSync(TMP, { recursive: true, force: true });
457
+ });
458
+ (0, node_test_1.it)("review.md keeps last 3 entries after 4 submissions", () => {
459
+ // Round 1
460
+ run("submit-ralph", "[PLAN] Plan A");
461
+ run("submit-lisa", "[NEEDS_WORK] Fix plan\n\n- Issue 1");
462
+ // Round 2
463
+ run("submit-ralph", "[FIX] Fixed plan\n\nTest Results\n- pass");
464
+ run("submit-lisa", "[NEEDS_WORK] Still wrong\n\n- Issue 2");
465
+ // Round 3
466
+ run("submit-ralph", "[FIX] Fixed again\n\nTest Results\n- pass");
467
+ run("submit-lisa", "[PASS] Now good\n\n- All clear");
468
+ // Round 4
469
+ run("submit-ralph", "[CONSENSUS] Agreed");
470
+ run("submit-lisa", "[CONSENSUS] Confirmed\n\n- Done");
471
+ const review = fs.readFileSync(path.join(TMP, ".dual-agent", "review.md"), "utf-8");
472
+ // Should have exactly 3 entries (rounds 2, 3, 4), not round 1
473
+ assert.ok(!review.includes("Fix plan"));
474
+ assert.ok(review.includes("Still wrong"));
475
+ assert.ok(review.includes("Now good"));
476
+ assert.ok(review.includes("Confirmed"));
477
+ });
478
+ (0, node_test_1.it)("review.md contains separator between entries", () => {
479
+ run("submit-ralph", "[PLAN] Plan");
480
+ run("submit-lisa", "[PASS] OK\n\n- Good");
481
+ run("submit-ralph", "[CONSENSUS] Agreed");
482
+ run("submit-lisa", "[CONSENSUS] Done\n\n- Confirmed");
483
+ const review = fs.readFileSync(path.join(TMP, ".dual-agent", "review.md"), "utf-8");
484
+ assert.ok(review.includes("---"));
485
+ });
486
+ (0, node_test_1.it)("read review --round N returns specific round from history", () => {
487
+ run("submit-ralph", "[PLAN] Plan A");
488
+ run("submit-lisa", "[NEEDS_WORK] Fix plan\n\n- Issue found");
489
+ run("submit-ralph", "[FIX] Fixed\n\nTest Results\n- pass");
490
+ run("submit-lisa", "[PASS] Approved\n\n- Looks good");
491
+ const r = run("read", "review", "--round", "1");
492
+ assert.strictEqual(r.exitCode, 0);
493
+ assert.ok(r.stdout.includes("Fix plan"));
494
+ assert.ok(r.stdout.includes("Issue found"));
495
+ });
496
+ (0, node_test_1.it)("read review --round returns correct round when multiple exist", () => {
497
+ run("submit-ralph", "[PLAN] Plan");
498
+ run("submit-lisa", "[NEEDS_WORK] Round 1 review\n\n- R1 issue");
499
+ run("submit-ralph", "[FIX] Fix\n\nTest Results\n- pass");
500
+ run("submit-lisa", "[PASS] Round 2 review\n\n- R2 good");
501
+ const r2 = run("read", "review", "--round", "2");
502
+ assert.strictEqual(r2.exitCode, 0);
503
+ assert.ok(r2.stdout.includes("Round 2 review"));
504
+ assert.ok(r2.stdout.includes("R2 good"));
505
+ });
506
+ (0, node_test_1.it)("read review --round with invalid round shows error", () => {
507
+ const r = run("read", "review", "--round", "abc");
508
+ assert.notStrictEqual(r.exitCode, 0);
509
+ assert.ok(r.stdout.includes("--round requires a positive integer"));
510
+ });
511
+ (0, node_test_1.it)("read review --round with out-of-range round shows not found", () => {
512
+ run("submit-ralph", "[PLAN] Plan");
513
+ run("submit-lisa", "[PASS] OK\n\n- Good");
514
+ const r = run("read", "review", "--round", "99");
515
+ assert.strictEqual(r.exitCode, 0);
516
+ assert.ok(r.stdout.includes("No review found for round 99"));
517
+ });
518
+ (0, node_test_1.it)("read review.md without --round still shows current file", () => {
519
+ run("submit-ralph", "[PLAN] Plan");
520
+ run("submit-lisa", "[PASS] Latest review\n\n- Current");
521
+ const r = run("read", "review.md");
522
+ assert.strictEqual(r.exitCode, 0);
523
+ assert.ok(r.stdout.includes("Latest review"));
524
+ });
525
+ });
526
+ (0, node_test_1.describe)("CLI: step consensus check", () => {
527
+ (0, node_test_1.beforeEach)(() => {
528
+ fs.rmSync(TMP, { recursive: true, force: true });
529
+ fs.mkdirSync(TMP, { recursive: true });
530
+ run("init", "--minimal");
531
+ });
532
+ (0, node_test_1.afterEach)(() => {
533
+ fs.rmSync(TMP, { recursive: true, force: true });
534
+ });
535
+ (0, node_test_1.it)("allows step when both CONSENSUS", () => {
536
+ run("submit-ralph", "[CONSENSUS] Agreed");
537
+ run("submit-lisa", "[CONSENSUS] Confirmed\n\n- Done");
538
+ const r = run("step", "next-feature");
539
+ assert.strictEqual(r.exitCode, 0);
540
+ assert.ok(r.stdout.includes("Entered step: next-feature"));
541
+ });
542
+ (0, node_test_1.it)("allows step when CONSENSUS + PASS", () => {
543
+ // Ralph submits CONSENSUS, Lisa responds with PASS
544
+ // work.md last tag = CONSENSUS, review.md last tag = PASS
545
+ run("submit-ralph", "[CONSENSUS] I agree");
546
+ run("submit-lisa", "[PASS] Approved\n\n- Looks good");
547
+ const r = run("step", "next-feature");
548
+ assert.strictEqual(r.exitCode, 0);
549
+ assert.ok(r.stdout.includes("Entered step: next-feature"));
550
+ });
551
+ (0, node_test_1.it)("allows step when PASS + CONSENSUS", () => {
552
+ // Lisa gives PASS, Ralph submits CONSENSUS
553
+ // After Ralph's CONSENSUS, work.md has CONSENSUS, review.md still has PASS
554
+ run("submit-ralph", "[PLAN] Plan");
555
+ run("submit-lisa", "[PASS] Approved\n\n- Good");
556
+ run("submit-ralph", "[CONSENSUS] Agreed");
557
+ // work.md = CONSENSUS, review.md = PASS → should allow
558
+ const r = run("step", "next-feature");
559
+ assert.strictEqual(r.exitCode, 0);
560
+ assert.ok(r.stdout.includes("Entered step: next-feature"));
561
+ });
562
+ (0, node_test_1.it)("blocks step when no consensus", () => {
563
+ run("submit-ralph", "[CODE] Some code\n\nTest Results\n- pass");
564
+ run("submit-lisa", "[PASS] Looks good\n\n- Clean");
565
+ const r = run("step", "next-feature");
566
+ assert.notStrictEqual(r.exitCode, 0);
567
+ assert.ok(r.stdout.includes("Consensus not reached"));
568
+ });
569
+ (0, node_test_1.it)("--force bypasses consensus check", () => {
570
+ run("submit-ralph", "[CODE] Some code\n\nTest Results\n- pass");
571
+ run("submit-lisa", "[NEEDS_WORK] Fix it\n\n- Issue");
572
+ const r = run("step", "--force", "next-feature");
573
+ assert.strictEqual(r.exitCode, 0);
574
+ assert.ok(r.stdout.includes("Entered step: next-feature"));
575
+ });
576
+ (0, node_test_1.it)("shows both tags in error when blocked", () => {
577
+ run("submit-ralph", "[CODE] Code\n\nTest Results\n- pass");
578
+ run("submit-lisa", "[NEEDS_WORK] Problems\n\n- Bugs");
579
+ const r = run("step", "next-feature");
580
+ assert.notStrictEqual(r.exitCode, 0);
581
+ assert.ok(r.stdout.includes("[CODE]"));
582
+ assert.ok(r.stdout.includes("[NEEDS_WORK]"));
583
+ });
584
+ (0, node_test_1.it)("body text with ## [CONSENSUS] does NOT spoof consensus", () => {
585
+ // Ralph submits CODE with a ## [CONSENSUS] heading in the body text
586
+ run("submit-ralph", "[CODE] My code\n\nTest Results\n- pass\n\n## [CONSENSUS] fake heading in body");
587
+ run("submit-lisa", "[PASS] OK\n\n- Clean code");
588
+ const r = run("step", "next-feature");
589
+ // Should block: work.md metadata tag is CODE, not CONSENSUS
590
+ assert.notStrictEqual(r.exitCode, 0);
591
+ assert.ok(r.stdout.includes("Consensus not reached"));
592
+ assert.ok(r.stdout.includes("[CODE]"));
593
+ });
594
+ });
@@ -0,0 +1 @@
1
+ export {};