ralph-lisa-loop 0.3.11 → 0.3.13
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 +1 -1
- package/dist/cli.js +20 -0
- package/dist/commands.d.ts +17 -0
- package/dist/commands.js +420 -76
- package/dist/policy.js +24 -0
- package/package.json +2 -1
- package/templates/claude-commands/check-turn.md +1 -1
- package/templates/claude-commands/submit-work.md +1 -1
- package/templates/codex-skills/check-turn.md +2 -2
- package/templates/roles/lisa.md +18 -10
- package/templates/roles/ralph.md +34 -12
- package/templates/skill.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Ralph writes → Lisa reviews → Consensus → Next step
|
|
|
40
40
|
- **Round 1 Mandatory Plan** — Ralph must submit `[PLAN]` first for Lisa to verify understanding
|
|
41
41
|
- **Goal Guardian** — Lisa checks for direction drift before every review
|
|
42
42
|
- **Mid-Session Task Update** — Change direction without restarting
|
|
43
|
-
- **Deadlock
|
|
43
|
+
- **Deadlock Detection** — After 8 consecutive `[NEEDS_WORK]` rounds, watcher auto-pauses for user intervention
|
|
44
44
|
- **Minimal Init** — Zero-intrusion mode with plugin/global config architecture
|
|
45
45
|
|
|
46
46
|
## Essential Commands
|
package/dist/cli.js
CHANGED
|
@@ -92,6 +92,18 @@ switch (cmd) {
|
|
|
92
92
|
case "stop":
|
|
93
93
|
(0, commands_js_1.cmdStop)(rest);
|
|
94
94
|
break;
|
|
95
|
+
case "emergency-msg":
|
|
96
|
+
(0, commands_js_1.cmdEmergencyMsg)(rest);
|
|
97
|
+
break;
|
|
98
|
+
case "notify":
|
|
99
|
+
(0, commands_js_1.cmdNotify)(rest);
|
|
100
|
+
break;
|
|
101
|
+
case "smoke-test":
|
|
102
|
+
(0, commands_js_1.cmdSmokeTest)(rest);
|
|
103
|
+
break;
|
|
104
|
+
case "smoke-check":
|
|
105
|
+
(0, commands_js_1.cmdSmokeCheck)(rest);
|
|
106
|
+
break;
|
|
95
107
|
case "help":
|
|
96
108
|
case "--help":
|
|
97
109
|
case "-h":
|
|
@@ -173,6 +185,14 @@ function showHelp() {
|
|
|
173
185
|
console.log(" ralph-lisa remote --auth user:pass Enable basic auth");
|
|
174
186
|
console.log(" ralph-lisa remote --stop Stop ttyd server");
|
|
175
187
|
console.log("");
|
|
188
|
+
console.log("Emergency & Notifications:");
|
|
189
|
+
console.log(' ralph-lisa emergency-msg <agent> "msg" Send emergency message to agent pane');
|
|
190
|
+
console.log(' ralph-lisa notify "message" Send notification via RL_NOTIFY_CMD');
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log("Smoke Testing:");
|
|
193
|
+
console.log(" ralph-lisa smoke-test Run smoke test (uses RL_SMOKE_CMD)");
|
|
194
|
+
console.log(" ralph-lisa smoke-check Check smoke test environment");
|
|
195
|
+
console.log("");
|
|
176
196
|
console.log("Diagnostics:");
|
|
177
197
|
console.log(" ralph-lisa state-dir Show state directory resolution");
|
|
178
198
|
console.log(" ralph-lisa state-dir /path Set state directory (tmux env)");
|
package/dist/commands.d.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* CLI commands for Ralph-Lisa Loop.
|
|
3
3
|
* Direct port of io.sh logic to Node/TS.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Send a notification to the user via RL_NOTIFY_CMD.
|
|
7
|
+
* Notification failure must not block main flow.
|
|
8
|
+
*/
|
|
9
|
+
export declare function notifyUser(message: string): void;
|
|
5
10
|
/**
|
|
6
11
|
* Generate a project-specific tmux session name to avoid conflicts
|
|
7
12
|
* when running multiple projects simultaneously.
|
|
@@ -51,4 +56,16 @@ export declare function cmdRemote(args: string[]): void;
|
|
|
51
56
|
export declare function cmdStateDir(args: string[]): void;
|
|
52
57
|
export declare function cmdAddContext(args: string[]): void;
|
|
53
58
|
export declare function cmdDoctor(args: string[]): void;
|
|
59
|
+
export declare function cmdEmergencyMsg(args: string[]): void;
|
|
60
|
+
export declare function cmdNotify(args: string[]): void;
|
|
61
|
+
/**
|
|
62
|
+
* Run smoke test command and persist results.
|
|
63
|
+
* Called automatically during step transition or manually via `ralph-lisa smoke-test`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runSmokeTest(dir: string): {
|
|
66
|
+
passed: boolean;
|
|
67
|
+
output: string;
|
|
68
|
+
};
|
|
69
|
+
export declare function cmdSmokeTest(_args: string[]): void;
|
|
70
|
+
export declare function cmdSmokeCheck(_args: string[]): void;
|
|
54
71
|
export {};
|
package/dist/commands.js
CHANGED
|
@@ -37,6 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
};
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.notifyUser = notifyUser;
|
|
40
41
|
exports.generateSessionName = generateSessionName;
|
|
41
42
|
exports.runGate = runGate;
|
|
42
43
|
exports.cmdInit = cmdInit;
|
|
@@ -66,6 +67,11 @@ exports.cmdRemote = cmdRemote;
|
|
|
66
67
|
exports.cmdStateDir = cmdStateDir;
|
|
67
68
|
exports.cmdAddContext = cmdAddContext;
|
|
68
69
|
exports.cmdDoctor = cmdDoctor;
|
|
70
|
+
exports.cmdEmergencyMsg = cmdEmergencyMsg;
|
|
71
|
+
exports.cmdNotify = cmdNotify;
|
|
72
|
+
exports.runSmokeTest = runSmokeTest;
|
|
73
|
+
exports.cmdSmokeTest = cmdSmokeTest;
|
|
74
|
+
exports.cmdSmokeCheck = cmdSmokeCheck;
|
|
69
75
|
const fs = __importStar(require("node:fs"));
|
|
70
76
|
const path = __importStar(require("node:path"));
|
|
71
77
|
const crypto = __importStar(require("node:crypto"));
|
|
@@ -75,6 +81,29 @@ const policy_js_1 = require("./policy.js");
|
|
|
75
81
|
function line(ch = "=", len = 40) {
|
|
76
82
|
return ch.repeat(len);
|
|
77
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Send a notification to the user via RL_NOTIFY_CMD.
|
|
86
|
+
* Notification failure must not block main flow.
|
|
87
|
+
*/
|
|
88
|
+
function notifyUser(message) {
|
|
89
|
+
const cmd = process.env.RL_NOTIFY_CMD;
|
|
90
|
+
if (!cmd)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
const child = (0, node_child_process_1.spawn)("sh", ["-c", cmd], {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
96
|
+
});
|
|
97
|
+
if (child.stdin) {
|
|
98
|
+
child.stdin.write(message);
|
|
99
|
+
child.stdin.end();
|
|
100
|
+
}
|
|
101
|
+
child.unref();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Notification failure must not block main flow
|
|
105
|
+
}
|
|
106
|
+
}
|
|
78
107
|
/**
|
|
79
108
|
* Generate a project-specific tmux session name to avoid conflicts
|
|
80
109
|
* when running multiple projects simultaneously.
|
|
@@ -210,6 +239,7 @@ function cmdInit(args) {
|
|
|
210
239
|
(0, state_js_1.writeFile)(path.join(dir, "round.txt"), "1");
|
|
211
240
|
(0, state_js_1.writeFile)(path.join(dir, "step.txt"), "planning");
|
|
212
241
|
(0, state_js_1.writeFile)(path.join(dir, "turn.txt"), "ralph");
|
|
242
|
+
(0, state_js_1.writeFile)(path.join(dir, ".project_root"), process.cwd()); // persist project root for smoke-test cwd
|
|
213
243
|
(0, state_js_1.writeFile)(path.join(dir, "last_action.txt"), "(No action yet)");
|
|
214
244
|
(0, state_js_1.writeFile)(path.join(dir, "plan.md"), "# Plan\n\n(To be drafted by Ralph and reviewed by Lisa)\n");
|
|
215
245
|
(0, state_js_1.writeFile)(path.join(dir, "work.md"), "# Ralph Work\n\n(Waiting for Ralph to submit)\n");
|
|
@@ -374,6 +404,17 @@ function cmdSubmitRalph(args) {
|
|
|
374
404
|
console.log(line());
|
|
375
405
|
console.log("");
|
|
376
406
|
console.log("Now wait for Lisa. Check with: ralph-lisa whose-turn");
|
|
407
|
+
// Notify on step completion (consensus reached)
|
|
408
|
+
const latestWork = (0, state_js_1.readFile)(path.join(dir, "work.md"));
|
|
409
|
+
const latestReview = (0, state_js_1.readFile)(path.join(dir, "review.md"));
|
|
410
|
+
const wTag = extractLastTag(latestWork);
|
|
411
|
+
const rTag = extractLastTag(latestReview);
|
|
412
|
+
if ((wTag === "CONSENSUS" && rTag === "CONSENSUS") ||
|
|
413
|
+
(wTag === "CONSENSUS" && rTag === "PASS") ||
|
|
414
|
+
(wTag === "PASS" && rTag === "CONSENSUS")) {
|
|
415
|
+
const stepName = (0, state_js_1.getStep)();
|
|
416
|
+
notifyUser(`[RLL] Step "${stepName}" complete — consensus reached.`);
|
|
417
|
+
}
|
|
377
418
|
}
|
|
378
419
|
// ─── submit-lisa ─────────────────────────────────
|
|
379
420
|
function cmdSubmitLisa(args) {
|
|
@@ -448,7 +489,8 @@ function cmdSubmitLisa(args) {
|
|
|
448
489
|
const currentCount = parseInt((0, state_js_1.readFile)(nwCountPath) || "0", 10);
|
|
449
490
|
const newCount = currentCount + 1;
|
|
450
491
|
(0, state_js_1.writeFile)(nwCountPath, String(newCount));
|
|
451
|
-
|
|
492
|
+
const deadlockThreshold = parseInt(process.env.RL_DEADLOCK_THRESHOLD || "8", 10);
|
|
493
|
+
if (newCount >= deadlockThreshold) {
|
|
452
494
|
// Trigger deadlock — write flag for watcher to detect
|
|
453
495
|
const deadlockPath = path.join(dir, "deadlock.txt");
|
|
454
496
|
(0, state_js_1.writeFile)(deadlockPath, `DEADLOCK at round ${round}: ${newCount} consecutive NEEDS_WORK rounds\nTimestamp: ${ts}\nAction: Watcher will pause. User intervention required.`);
|
|
@@ -458,6 +500,7 @@ function cmdSubmitLisa(args) {
|
|
|
458
500
|
console.log("Watcher will pause for user intervention.");
|
|
459
501
|
console.log("To resolve: ralph-lisa scope-update or ralph-lisa force-turn");
|
|
460
502
|
console.log(line("!", 40));
|
|
503
|
+
notifyUser(`[RLL] DEADLOCK: ${newCount} consecutive NEEDS_WORK rounds. User intervention needed.`);
|
|
461
504
|
}
|
|
462
505
|
}
|
|
463
506
|
else {
|
|
@@ -498,6 +541,17 @@ function cmdSubmitLisa(args) {
|
|
|
498
541
|
console.log(line());
|
|
499
542
|
console.log("");
|
|
500
543
|
console.log("Now wait for Ralph. Check with: ralph-lisa whose-turn");
|
|
544
|
+
// Notify on step completion (consensus reached)
|
|
545
|
+
const latestWork = (0, state_js_1.readFile)(path.join(dir, "work.md"));
|
|
546
|
+
const latestReview = (0, state_js_1.readFile)(path.join(dir, "review.md"));
|
|
547
|
+
const wTag = extractLastTag(latestWork);
|
|
548
|
+
const rTag = extractLastTag(latestReview);
|
|
549
|
+
if ((wTag === "CONSENSUS" && rTag === "CONSENSUS") ||
|
|
550
|
+
(wTag === "CONSENSUS" && rTag === "PASS") ||
|
|
551
|
+
(wTag === "PASS" && rTag === "CONSENSUS")) {
|
|
552
|
+
const stepName = (0, state_js_1.getStep)();
|
|
553
|
+
notifyUser(`[RLL] Step "${stepName}" complete — consensus reached.`);
|
|
554
|
+
}
|
|
501
555
|
}
|
|
502
556
|
// ─── status ──────────────────────────────────────
|
|
503
557
|
function cmdStatus() {
|
|
@@ -725,6 +779,26 @@ function cmdStep(args) {
|
|
|
725
779
|
process.exit(1);
|
|
726
780
|
}
|
|
727
781
|
}
|
|
782
|
+
// Auto smoke test: run if current step had CODE/FIX submissions (step50)
|
|
783
|
+
if (process.env.RL_SMOKE_AUTO !== "false") {
|
|
784
|
+
const history = (0, state_js_1.readFile)(path.join(dir, "history.md"));
|
|
785
|
+
const currentStep = (0, state_js_1.getStep)();
|
|
786
|
+
const escaped = currentStep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
787
|
+
const stepBlockRe = new RegExp(`^# Step: ${escaped}\\n\\nStarted:`, "gm");
|
|
788
|
+
let lastMatch = null;
|
|
789
|
+
let m;
|
|
790
|
+
while ((m = stepBlockRe.exec(history)) !== null) {
|
|
791
|
+
lastMatch = m;
|
|
792
|
+
}
|
|
793
|
+
const stepSection = lastMatch ? history.slice(lastMatch.index) : "";
|
|
794
|
+
if (/\[CODE\]|\[FIX\]/.test(stepSection)) {
|
|
795
|
+
console.log(`[Smoke] Development step detected (CODE/FIX found). Running smoke test...`);
|
|
796
|
+
runSmokeTest(dir);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
console.log(`[Smoke] Planning step (no CODE/FIX). Skipping smoke test.`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
728
802
|
(0, state_js_1.setStep)(stepName);
|
|
729
803
|
(0, state_js_1.setRound)(1);
|
|
730
804
|
(0, state_js_1.setTurn)("ralph");
|
|
@@ -1382,6 +1456,12 @@ description: Lisa review commands for Ralph-Lisa dual-agent collaboration
|
|
|
1382
1456
|
|
|
1383
1457
|
This skill provides Lisa's review commands for the Ralph-Lisa collaboration.
|
|
1384
1458
|
|
|
1459
|
+
## Turn Rules
|
|
1460
|
+
|
|
1461
|
+
When it's not your turn, do not submit work. You may use subagents for preparatory tasks.
|
|
1462
|
+
If triggered by the user but it's not your turn, suggest checking watcher status:
|
|
1463
|
+
\`cat .dual-agent/.watcher_heartbeat\` and \`ralph-lisa status\`.
|
|
1464
|
+
|
|
1385
1465
|
## Available Commands
|
|
1386
1466
|
|
|
1387
1467
|
### Check Turn
|
|
@@ -1392,9 +1472,7 @@ Check if it's your turn before taking action.
|
|
|
1392
1472
|
|
|
1393
1473
|
### Submit Review
|
|
1394
1474
|
\`\`\`bash
|
|
1395
|
-
ralph-lisa submit-lisa
|
|
1396
|
-
|
|
1397
|
-
detailed content..."
|
|
1475
|
+
ralph-lisa submit-lisa --file .dual-agent/submit.md
|
|
1398
1476
|
\`\`\`
|
|
1399
1477
|
Submit your review. Valid tags: PASS, NEEDS_WORK, CHALLENGE, DISCUSS, QUESTION, CONSENSUS
|
|
1400
1478
|
|
|
@@ -1409,6 +1487,13 @@ View current task, turn, and last action.
|
|
|
1409
1487
|
ralph-lisa read work.md
|
|
1410
1488
|
\`\`\`
|
|
1411
1489
|
Read Ralph's latest submission.
|
|
1490
|
+
|
|
1491
|
+
## Review Requirements
|
|
1492
|
+
|
|
1493
|
+
For [CODE]/[FIX] reviews:
|
|
1494
|
+
- Verify Test Results match the test plan from [PLAN] phase
|
|
1495
|
+
- Re-run the test command yourself to verify results
|
|
1496
|
+
- Check for exit code or pass/fail count (or explicit Skipped: with justification)
|
|
1412
1497
|
`;
|
|
1413
1498
|
(0, state_js_1.writeFile)(path.join(codexSkillDir, "SKILL.md"), skillContent);
|
|
1414
1499
|
// Create .codex/config.toml (with marker for safe uninit)
|
|
@@ -1735,8 +1820,9 @@ function cmdAuto(args) {
|
|
|
1735
1820
|
// Create watcher script
|
|
1736
1821
|
const watcherScript = path.join(dir, "watcher.sh");
|
|
1737
1822
|
let watcherContent = `#!/bin/bash
|
|
1738
|
-
# Turn watcher
|
|
1823
|
+
# Turn watcher v5 - decoupled delivery + send caps + capture-pane monitoring
|
|
1739
1824
|
# Architecture: polling main loop + optional event acceleration
|
|
1825
|
+
# v5: Fixes message flooding and stall bugs from v4 (step41)
|
|
1740
1826
|
# v4: Round-based detection fixes double-flip deadlock (step39)
|
|
1741
1827
|
|
|
1742
1828
|
STATE_DIR=".dual-agent"
|
|
@@ -1759,14 +1845,29 @@ DEADLOCK_REMIND_TIME=0
|
|
|
1759
1845
|
CLEANUP_DONE=0
|
|
1760
1846
|
|
|
1761
1847
|
# Per-turn escalation state (step38: anti-flooding + stuck-agent detection)
|
|
1848
|
+
# step43: configurable escalation timing via env vars (default: 5m/15m/30m)
|
|
1762
1849
|
NOTIFY_SENT_AT=0 # epoch when first notification was sent this turn
|
|
1763
1850
|
REMINDER_LEVEL=0 # 0=initial, 1=REMINDER sent, 2=slash sent, 3=user notified
|
|
1764
1851
|
CURRENT_TURN_HASH="" # hash of turn.txt content for change detection
|
|
1852
|
+
ESCALATION_L1=\${RL_ESCALATION_L1:-300} # L1 REMINDER (default 5 min)
|
|
1853
|
+
ESCALATION_L2=\${RL_ESCALATION_L2:-900} # L2 /check-turn (default 15 min)
|
|
1854
|
+
ESCALATION_L3=\${RL_ESCALATION_L3:-1800} # L3 STUCK notify (default 30 min)
|
|
1855
|
+
|
|
1856
|
+
# v5: Per-round send cap (P0-2: prevents message flooding)
|
|
1857
|
+
SEND_COUNT_THIS_ROUND=0
|
|
1858
|
+
MAX_SENDS_PER_ROUND=2 # initial + 1 retry max
|
|
1765
1859
|
|
|
1766
1860
|
PANE0_LOG="\${STATE_DIR}/pane0.log"
|
|
1767
1861
|
PANE1_LOG="\${STATE_DIR}/pane1.log"
|
|
1768
1862
|
PID_FILE="\${STATE_DIR}/watcher.pid"
|
|
1769
1863
|
|
|
1864
|
+
# User notification hook (step47)
|
|
1865
|
+
notify_user() {
|
|
1866
|
+
if [[ -n "\$RL_NOTIFY_CMD" ]]; then
|
|
1867
|
+
echo "\$1" | eval "\$RL_NOTIFY_CMD" 2>/dev/null &
|
|
1868
|
+
fi
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1770
1871
|
# Interactive prompt patterns (do NOT send "go" if matched)
|
|
1771
1872
|
# Covers: passwords, confirmations, Claude Code permission prompts, Codex approval prompts
|
|
1772
1873
|
# NOTE: patterns must be specific enough to avoid false positives in normal agent output
|
|
@@ -1913,27 +2014,23 @@ check_agent_alive() {
|
|
|
1913
2014
|
}
|
|
1914
2015
|
|
|
1915
2016
|
# Returns 0 if pane output has been stable for at least N seconds
|
|
2017
|
+
# v5 (P0-3): Uses capture-pane diff instead of pipe-pane log mtime.
|
|
2018
|
+
# The old log-mtime approach failed silently when pipe-pane died,
|
|
2019
|
+
# causing false-idle detection and message injection into active agents.
|
|
1916
2020
|
check_output_stable() {
|
|
1917
|
-
local
|
|
2021
|
+
local pane="\$1"
|
|
1918
2022
|
local stable_seconds="\${2:-5}"
|
|
1919
2023
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
if [[ "\$(uname)" == "Darwin" ]]; then
|
|
1926
|
-
mtime_epoch=\$(stat -f %m "\$log_file" 2>/dev/null || echo 0)
|
|
1927
|
-
else
|
|
1928
|
-
mtime_epoch=\$(stat -c %Y "\$log_file" 2>/dev/null || echo 0)
|
|
1929
|
-
fi
|
|
1930
|
-
now_epoch=\$(date +%s)
|
|
1931
|
-
elapsed=\$(( now_epoch - mtime_epoch ))
|
|
2024
|
+
# Capture current pane content hash
|
|
2025
|
+
local hash1 hash2
|
|
2026
|
+
hash1=\$(tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5sum 2>/dev/null || tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5)
|
|
2027
|
+
sleep "\$stable_seconds"
|
|
2028
|
+
hash2=\$(tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5sum 2>/dev/null || tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5)
|
|
1932
2029
|
|
|
1933
|
-
if
|
|
1934
|
-
return 0 # Stable
|
|
2030
|
+
if [[ "\$hash1" == "\$hash2" ]]; then
|
|
2031
|
+
return 0 # Stable — pane content unchanged
|
|
1935
2032
|
fi
|
|
1936
|
-
return 1 # Still producing output
|
|
2033
|
+
return 1 # Still producing output — pane content changed
|
|
1937
2034
|
}
|
|
1938
2035
|
|
|
1939
2036
|
# Returns 0 if interactive prompt detected (do NOT send go)
|
|
@@ -1991,14 +2088,14 @@ send_go_to_pane() {
|
|
|
1991
2088
|
|
|
1992
2089
|
# 3. Wait for agent to be idle (output stable for 5s)
|
|
1993
2090
|
# Prevents injecting text while agent is mid-response
|
|
2091
|
+
# v5 (P0-3): uses capture-pane diff, not pipe-pane log mtime
|
|
1994
2092
|
local stable_wait=0
|
|
1995
2093
|
while (( stable_wait < 30 )); do
|
|
1996
|
-
if check_output_stable "\$
|
|
2094
|
+
if check_output_stable "\$pane" 5; then
|
|
1997
2095
|
break
|
|
1998
2096
|
fi
|
|
1999
2097
|
echo "[Watcher] Waiting for \$agent_name to finish output..."
|
|
2000
|
-
|
|
2001
|
-
stable_wait=\$((stable_wait + 3))
|
|
2098
|
+
stable_wait=\$((stable_wait + 5))
|
|
2002
2099
|
done
|
|
2003
2100
|
if (( stable_wait >= 30 )); then
|
|
2004
2101
|
echo "[Watcher] \$agent_name still producing output after 30s, sending anyway"
|
|
@@ -2037,29 +2134,73 @@ send_go_to_pane() {
|
|
|
2037
2134
|
return 1
|
|
2038
2135
|
fi
|
|
2039
2136
|
|
|
2040
|
-
#
|
|
2041
|
-
#
|
|
2042
|
-
#
|
|
2043
|
-
|
|
2137
|
+
# v5 (P0-1): send-keys succeeded + message left input line = delivered.
|
|
2138
|
+
# Post-send response monitoring is now decoupled — handled by monitor_agent_response()
|
|
2139
|
+
# in the escalation path. This eliminates the flooding bug where pipe-pane failure
|
|
2140
|
+
# caused send_go_to_pane to return 1 despite successful delivery.
|
|
2141
|
+
echo "[Watcher] OK: Message delivered to \$agent_name (send-keys confirmed)"
|
|
2142
|
+
SEND_COUNT_THIS_ROUND=\$((SEND_COUNT_THIS_ROUND + 1))
|
|
2143
|
+
return 0
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
# v5 (P1-2): Passive post-send monitoring — checks if agent is responding
|
|
2147
|
+
# without sending any messages. Uses capture-pane diff + log growth cross-reference.
|
|
2148
|
+
# Called from escalation path, NOT from delivery path.
|
|
2149
|
+
monitor_agent_response() {
|
|
2150
|
+
local pane="\$1"
|
|
2151
|
+
local agent_name="\$2"
|
|
2152
|
+
local log_file="\$3"
|
|
2153
|
+
|
|
2154
|
+
# Record log size BEFORE sleep so we can measure real growth
|
|
2155
|
+
local log_size_before=0
|
|
2044
2156
|
if [[ -f "\$log_file" ]]; then
|
|
2045
|
-
|
|
2157
|
+
log_size_before=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ')
|
|
2046
2158
|
fi
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2159
|
+
|
|
2160
|
+
# Check 1: capture-pane diff (primary signal, works even if pipe-pane is dead)
|
|
2161
|
+
local hash_before hash_after
|
|
2162
|
+
hash_before=\$(tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5sum 2>/dev/null || tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5)
|
|
2163
|
+
sleep 5
|
|
2164
|
+
hash_after=\$(tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5sum 2>/dev/null || tmux capture-pane -t "\${SESSION}:\${pane}" -p 2>/dev/null | md5)
|
|
2165
|
+
|
|
2166
|
+
local pane_changed=0
|
|
2167
|
+
local log_grew=0
|
|
2168
|
+
|
|
2169
|
+
if [[ "\$hash_before" != "\$hash_after" ]]; then
|
|
2170
|
+
pane_changed=1
|
|
2171
|
+
fi
|
|
2172
|
+
|
|
2173
|
+
# Check 2: log file growth (secondary signal, depends on pipe-pane being alive)
|
|
2174
|
+
# size_before was recorded BEFORE the 5s sleep above
|
|
2175
|
+
if [[ -f "\$log_file" ]]; then
|
|
2176
|
+
local log_size_after
|
|
2177
|
+
log_size_after=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ')
|
|
2178
|
+
if (( log_size_after > log_size_before + 50 )); then
|
|
2179
|
+
log_grew=1
|
|
2058
2180
|
fi
|
|
2059
|
-
|
|
2181
|
+
fi
|
|
2060
2182
|
|
|
2061
|
-
|
|
2062
|
-
|
|
2183
|
+
# Cross-reference for pipe-pane health (P1-1)
|
|
2184
|
+
if (( pane_changed && !log_grew )); then
|
|
2185
|
+
echo "[Watcher] Pipe-pane appears dead (pane active but log stale), rebuilding for \$pane"
|
|
2186
|
+
tmux pipe-pane -t "\${SESSION}:\${pane}" 2>/dev/null || true
|
|
2187
|
+
tmux pipe-pane -o -t "\${SESSION}:\${pane}" "cat >> \\"\$log_file\\"" 2>/dev/null || true
|
|
2188
|
+
fi
|
|
2189
|
+
|
|
2190
|
+
if (( pane_changed )); then
|
|
2191
|
+
echo "[Watcher] Monitor: \$agent_name is active (pane output changing)"
|
|
2192
|
+
return 0 # Agent is working
|
|
2193
|
+
fi
|
|
2194
|
+
|
|
2195
|
+
# Check 3: turn.txt changed (ultimate signal — agent finished and submitted)
|
|
2196
|
+
local current_turn
|
|
2197
|
+
current_turn=\$(cat "\$STATE_DIR/turn.txt" 2>/dev/null || echo "")
|
|
2198
|
+
if [[ "\$current_turn" != "\$SEEN_TURN" ]]; then
|
|
2199
|
+
echo "[Watcher] Monitor: Turn changed to \$current_turn — agent completed work"
|
|
2200
|
+
return 0
|
|
2201
|
+
fi
|
|
2202
|
+
|
|
2203
|
+
return 1 # No activity detected
|
|
2063
2204
|
}
|
|
2064
2205
|
|
|
2065
2206
|
# ─── trigger_agent ───────────────────────────────
|
|
@@ -2067,6 +2208,12 @@ send_go_to_pane() {
|
|
|
2067
2208
|
trigger_agent() {
|
|
2068
2209
|
local turn="\$1"
|
|
2069
2210
|
|
|
2211
|
+
# v5 (P0-2): Check send cap before attempting delivery
|
|
2212
|
+
if (( SEND_COUNT_THIS_ROUND >= MAX_SENDS_PER_ROUND )); then
|
|
2213
|
+
echo "[Watcher] SEND_CAP: Max sends (\$MAX_SENDS_PER_ROUND) reached for round \$SEEN_ROUND, passive monitoring only"
|
|
2214
|
+
return 1
|
|
2215
|
+
fi
|
|
2216
|
+
|
|
2070
2217
|
# Read task context for trigger messages (last meaningful line = latest direction)
|
|
2071
2218
|
local task_ctx=""
|
|
2072
2219
|
if [[ -f "\$STATE_DIR/task.md" ]]; then
|
|
@@ -2268,6 +2415,8 @@ check_and_trigger() {
|
|
|
2268
2415
|
# Reset per-turn escalation state (step38)
|
|
2269
2416
|
NOTIFY_SENT_AT=0
|
|
2270
2417
|
REMINDER_LEVEL=0
|
|
2418
|
+
# v5 (P0-2): Reset send cap for new round
|
|
2419
|
+
SEND_COUNT_THIS_ROUND=0
|
|
2271
2420
|
|
|
2272
2421
|
# Mark delivery pending (step39: decouple ack from delivery)
|
|
2273
2422
|
DELIVERY_PENDING=1
|
|
@@ -2288,6 +2437,8 @@ check_and_trigger() {
|
|
|
2288
2437
|
LAST_ACK_TIME=0
|
|
2289
2438
|
NOTIFY_SENT_AT=0
|
|
2290
2439
|
REMINDER_LEVEL=0
|
|
2440
|
+
# v5 (P0-2): Reset send cap for new turn
|
|
2441
|
+
SEND_COUNT_THIS_ROUND=0
|
|
2291
2442
|
DELIVERY_PENDING=1
|
|
2292
2443
|
PENDING_TARGET="\$CURRENT_TURN"
|
|
2293
2444
|
save_watcher_state
|
|
@@ -2307,7 +2458,9 @@ check_and_trigger() {
|
|
|
2307
2458
|
|
|
2308
2459
|
# Consensus suppression (step38): suppress notifications when consensus reached
|
|
2309
2460
|
# step39: only suppress if round hasn't changed since consensus was detected
|
|
2310
|
-
if
|
|
2461
|
+
# step46: only suppress if delivery is NOT pending — if turn points to an agent
|
|
2462
|
+
# that hasn't responded yet, they need to be triggered to confirm consensus
|
|
2463
|
+
if check_consensus_reached && (( !DELIVERY_PENDING )); then
|
|
2311
2464
|
if [[ "\$CONSENSUS_AT_ROUND" == "" ]]; then
|
|
2312
2465
|
CONSENSUS_AT_ROUND="\$CURRENT_ROUND"
|
|
2313
2466
|
fi
|
|
@@ -2388,40 +2541,60 @@ check_and_trigger() {
|
|
|
2388
2541
|
target_pane="0.1"; target_name="Lisa"; target_log="\$PANE1_LOG"
|
|
2389
2542
|
fi
|
|
2390
2543
|
|
|
2391
|
-
#
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2544
|
+
# v5 (P1-2): Passive monitoring — check if agent is working before escalating
|
|
2545
|
+
# This also handles pipe-pane cross-reference rebuild (P1-1)
|
|
2546
|
+
if monitor_agent_response "\$target_pane" "\$target_name" "\$target_log"; then
|
|
2547
|
+
# Agent is active — reset escalation timer, no action needed
|
|
2548
|
+
NOTIFY_SENT_AT=\$(date +%s)
|
|
2549
|
+
REMINDER_LEVEL=0
|
|
2550
|
+
else
|
|
2551
|
+
# Agent not responding — proceed with escalation
|
|
2552
|
+
|
|
2553
|
+
# Check for context limit in pane output (unrecoverable — notify user immediately)
|
|
2554
|
+
local pane_tail
|
|
2555
|
+
pane_tail=\$(tmux capture-pane -t "\${SESSION}:\${target_pane}" -p 2>/dev/null | tail -10)
|
|
2556
|
+
if echo "\$pane_tail" | grep -qiE "context limit|conversation too long|token limit|context window"; then
|
|
2557
|
+
if (( REMINDER_LEVEL < 3 )); then
|
|
2558
|
+
echo "[Watcher] CONTEXT LIMIT detected for \$target_name. Manual intervention required."
|
|
2559
|
+
echo "[Watcher] Restart the agent session to continue."
|
|
2560
|
+
REMINDER_LEVEL=3
|
|
2561
|
+
notify_user "[RLL] CONTEXT LIMIT: \$target_name needs restart"
|
|
2562
|
+
fi
|
|
2403
2563
|
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
echo "[Watcher] STUCK: \$target_name has not responded for \${elapsed}s. Manual intervention needed."
|
|
2407
|
-
REMINDER_LEVEL=3
|
|
2564
|
+
# Time-based escalation: each level checked independently by elapsed time.
|
|
2565
|
+
# If L1/L2 delivery fails, time still advances, so L3 is always reachable.
|
|
2408
2566
|
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2567
|
+
# Level 3: notify user (default 30 min) — always reachable regardless of L1/L2 success
|
|
2568
|
+
elif (( elapsed >= ESCALATION_L3 && REMINDER_LEVEL < 3 )); then
|
|
2569
|
+
echo "[Watcher] STUCK: \$target_name has not responded for \${elapsed}s. Manual intervention needed."
|
|
2570
|
+
REMINDER_LEVEL=3
|
|
2571
|
+
notify_user "[RLL] STUCK: \$target_name not responding for \${elapsed}s"
|
|
2572
|
+
|
|
2573
|
+
# Level 2: slash command (default 15 min), with prompt guard
|
|
2574
|
+
# v5: escalation also respects send cap to prevent flooding
|
|
2575
|
+
elif (( elapsed >= ESCALATION_L2 && REMINDER_LEVEL < 2 )); then
|
|
2576
|
+
if (( SEND_COUNT_THIS_ROUND >= MAX_SENDS_PER_ROUND )); then
|
|
2577
|
+
echo "[Watcher] Escalation L2: Skipped — send cap reached for round \$SEEN_ROUND"
|
|
2578
|
+
elif ! check_for_interactive_prompt "\$target_pane"; then
|
|
2579
|
+
echo "[Watcher] Escalation L2: Sending /check-turn to \$target_name (no response for \${elapsed}s)"
|
|
2580
|
+
if send_go_to_pane "\$target_pane" "\$target_name" "\$target_log" "/check-turn"; then
|
|
2581
|
+
REMINDER_LEVEL=2
|
|
2582
|
+
fi
|
|
2583
|
+
else
|
|
2584
|
+
echo "[Watcher] Escalation L2: Skipped — interactive prompt detected for \$target_name"
|
|
2415
2585
|
fi
|
|
2416
|
-
else
|
|
2417
|
-
echo "[Watcher] Escalation L2: Skipped — interactive prompt detected for \$target_name"
|
|
2418
|
-
fi
|
|
2419
2586
|
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2587
|
+
# Level 1: REMINDER (default 5 min)
|
|
2588
|
+
# v5: escalation also respects send cap to prevent flooding
|
|
2589
|
+
elif (( elapsed >= ESCALATION_L1 && REMINDER_LEVEL < 1 )); then
|
|
2590
|
+
if (( SEND_COUNT_THIS_ROUND >= MAX_SENDS_PER_ROUND )); then
|
|
2591
|
+
echo "[Watcher] Escalation L1: Skipped — send cap reached for round \$SEEN_ROUND"
|
|
2592
|
+
else
|
|
2593
|
+
echo "[Watcher] Escalation L1: Sending REMINDER to \$target_name (no response for \${elapsed}s)"
|
|
2594
|
+
if send_go_to_pane "\$target_pane" "\$target_name" "\$target_log" "REMINDER: It is your turn. Please check turn and continue working."; then
|
|
2595
|
+
REMINDER_LEVEL=1
|
|
2596
|
+
fi
|
|
2597
|
+
fi
|
|
2425
2598
|
fi
|
|
2426
2599
|
fi
|
|
2427
2600
|
fi
|
|
@@ -2430,7 +2603,7 @@ check_and_trigger() {
|
|
|
2430
2603
|
|
|
2431
2604
|
# ─── Main ────────────────────────────────────────
|
|
2432
2605
|
|
|
2433
|
-
echo "[Watcher] Starting
|
|
2606
|
+
echo "[Watcher] Starting v5... (Ctrl+C to stop)"
|
|
2434
2607
|
echo "[Watcher] Monitoring \$STATE_DIR/turn.txt + round.txt"
|
|
2435
2608
|
echo "[Watcher] Pane logs: \$PANE0_LOG, \$PANE1_LOG"
|
|
2436
2609
|
if (( CHECKPOINT_ROUNDS > 0 )); then
|
|
@@ -2517,7 +2690,7 @@ done
|
|
|
2517
2690
|
}
|
|
2518
2691
|
// Watcher runs in background with session-guarded restart loop
|
|
2519
2692
|
const watcherLog = path.join(dir, "watcher.log");
|
|
2520
|
-
execSync(`bash -c 'nohup bash -c '"'"'while tmux has-session -t "${sessionName}" 2>/dev/null; do bash "${watcherScript}"; EXIT_CODE=$?; if ! tmux has-session -t "${sessionName}" 2>/dev/null; then echo "[Watcher] Session gone, not restarting." >> "${watcherLog}"; break; fi; echo "[Watcher] Exited ($EXIT_CODE), restarting in 5s..." >> "${watcherLog}"; sleep 5; done'"'"' > "${watcherLog}" 2>&1 & echo $! > "${wrapperPidFile}"'`);
|
|
2693
|
+
execSync(`bash -c 'nohup bash -c '"'"'while tmux has-session -t "${sessionName}" 2>/dev/null; do bash "${watcherScript}"; EXIT_CODE=$?; if ! tmux has-session -t "${sessionName}" 2>/dev/null; then echo "[Watcher] Session gone, not restarting." >> "${watcherLog}"; break; fi; echo "[Watcher] Exited ($EXIT_CODE), restarting in 5s..." >> "${watcherLog}"; if [[ -n "$RL_NOTIFY_CMD" ]]; then echo "[RLL] Watcher crashed (exit $EXIT_CODE), restarting..." | eval "$RL_NOTIFY_CMD" 2>/dev/null & fi; sleep 5; done'"'"' > "${watcherLog}" 2>&1 & echo $! > "${wrapperPidFile}"'`);
|
|
2521
2694
|
console.log("");
|
|
2522
2695
|
console.log(line());
|
|
2523
2696
|
console.log("Auto Mode Started!");
|
|
@@ -3119,3 +3292,174 @@ function cmdDoctor(args) {
|
|
|
3119
3292
|
process.exit(1);
|
|
3120
3293
|
}
|
|
3121
3294
|
}
|
|
3295
|
+
// ─── emergency-msg ───────────────────────────────
|
|
3296
|
+
function cmdEmergencyMsg(args) {
|
|
3297
|
+
if (args.length < 2) {
|
|
3298
|
+
console.error("Usage: ralph-lisa emergency-msg <ralph|lisa> \"message\"");
|
|
3299
|
+
process.exit(1);
|
|
3300
|
+
}
|
|
3301
|
+
const target = args[0];
|
|
3302
|
+
const message = args.slice(1).join(" ");
|
|
3303
|
+
if (target !== "ralph" && target !== "lisa") {
|
|
3304
|
+
console.error("Error: target must be 'ralph' or 'lisa'");
|
|
3305
|
+
process.exit(1);
|
|
3306
|
+
}
|
|
3307
|
+
// Use project root for session name (not cwd, which may be a subdirectory)
|
|
3308
|
+
const dir = (0, state_js_1.stateDir)();
|
|
3309
|
+
const projectRoot = path.resolve(dir, "..");
|
|
3310
|
+
const sessionName = generateSessionName(projectRoot);
|
|
3311
|
+
// Check tmux session exists
|
|
3312
|
+
try {
|
|
3313
|
+
(0, node_child_process_1.execSync)(`tmux has-session -t "${sessionName}" 2>/dev/null`);
|
|
3314
|
+
}
|
|
3315
|
+
catch {
|
|
3316
|
+
console.error(`Error: tmux session '${sessionName}' not found.`);
|
|
3317
|
+
process.exit(1);
|
|
3318
|
+
}
|
|
3319
|
+
// Check watcher health — only allow emergency-msg when watcher is unhealthy
|
|
3320
|
+
const heartbeatFile = path.join(dir, ".watcher_heartbeat");
|
|
3321
|
+
if (fs.existsSync(heartbeatFile)) {
|
|
3322
|
+
const heartbeat = parseInt((0, state_js_1.readFile)(heartbeatFile).trim(), 10);
|
|
3323
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3324
|
+
if (now - heartbeat < 300) { // 5 minutes
|
|
3325
|
+
console.error("Error: Watcher is healthy (heartbeat < 5min old). Use normal submit flow.");
|
|
3326
|
+
console.error("Emergency messaging is only available when watcher appears stuck.");
|
|
3327
|
+
process.exit(1);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
// Send via tmux — use temp file to avoid shell injection
|
|
3331
|
+
// (user message could contain $(), backticks, etc.)
|
|
3332
|
+
const pane = target === "ralph" ? "0.0" : "0.1";
|
|
3333
|
+
const emergencyMsg = `[EMERGENCY] ${message}`;
|
|
3334
|
+
const tmpMsgFile = path.join(dir, ".emergency_msg_tmp");
|
|
3335
|
+
try {
|
|
3336
|
+
(0, state_js_1.writeFile)(tmpMsgFile, emergencyMsg);
|
|
3337
|
+
(0, node_child_process_1.execSync)(`tmux load-buffer "${tmpMsgFile}" 2>/dev/null && tmux paste-buffer -t "${sessionName}:${pane}" 2>/dev/null`);
|
|
3338
|
+
(0, node_child_process_1.execSync)(`tmux send-keys -t "${sessionName}:${pane}" Enter 2>/dev/null`);
|
|
3339
|
+
try {
|
|
3340
|
+
fs.unlinkSync(tmpMsgFile);
|
|
3341
|
+
}
|
|
3342
|
+
catch { }
|
|
3343
|
+
}
|
|
3344
|
+
catch {
|
|
3345
|
+
console.error(`Error: Failed to send message to ${target}'s pane.`);
|
|
3346
|
+
process.exit(1);
|
|
3347
|
+
}
|
|
3348
|
+
// Log to emergency.log
|
|
3349
|
+
const ts = new Date().toISOString();
|
|
3350
|
+
const logEntry = `[${ts}] To ${target}: ${message}\n`;
|
|
3351
|
+
const logFile = path.join(dir, "emergency.log");
|
|
3352
|
+
fs.appendFileSync(logFile, logEntry);
|
|
3353
|
+
console.log(`Emergency message sent to ${target}: ${message}`);
|
|
3354
|
+
console.log(`Logged to ${logFile}`);
|
|
3355
|
+
}
|
|
3356
|
+
// ─── notify ──────────────────────────────────────
|
|
3357
|
+
function cmdNotify(args) {
|
|
3358
|
+
const message = args.join(" ");
|
|
3359
|
+
if (!message) {
|
|
3360
|
+
console.error("Usage: ralph-lisa notify \"message\"");
|
|
3361
|
+
process.exit(1);
|
|
3362
|
+
}
|
|
3363
|
+
if (!process.env.RL_NOTIFY_CMD) {
|
|
3364
|
+
console.error("Error: RL_NOTIFY_CMD not set. Configure it first:");
|
|
3365
|
+
console.error(' export RL_NOTIFY_CMD="cat >> /tmp/notify.txt"');
|
|
3366
|
+
process.exit(1);
|
|
3367
|
+
}
|
|
3368
|
+
notifyUser(message);
|
|
3369
|
+
console.log(`Notification sent: ${message}`);
|
|
3370
|
+
}
|
|
3371
|
+
// ─── smoke test ───────────────────────────────────
|
|
3372
|
+
/**
|
|
3373
|
+
* Run smoke test command and persist results.
|
|
3374
|
+
* Called automatically during step transition or manually via `ralph-lisa smoke-test`.
|
|
3375
|
+
*/
|
|
3376
|
+
function runSmokeTest(dir) {
|
|
3377
|
+
const cmd = process.env.RL_SMOKE_CMD;
|
|
3378
|
+
if (!cmd) {
|
|
3379
|
+
console.log("[Smoke] No RL_SMOKE_CMD configured. Skipping smoke test.");
|
|
3380
|
+
return { passed: true, output: "Skipped: RL_SMOKE_CMD not configured" };
|
|
3381
|
+
}
|
|
3382
|
+
const debug = process.env.RL_SMOKE_DEBUG === "true";
|
|
3383
|
+
console.log(`[Smoke] Running: ${cmd}`);
|
|
3384
|
+
const ts = new Date().toISOString();
|
|
3385
|
+
const startTime = Date.now();
|
|
3386
|
+
let exitCode = 0;
|
|
3387
|
+
let output = "";
|
|
3388
|
+
try {
|
|
3389
|
+
output = (0, node_child_process_1.execSync)(cmd, {
|
|
3390
|
+
cwd: (0, state_js_1.readFile)(path.join(dir, ".project_root")).trim() || (0, state_js_1.findProjectRoot)() || process.cwd(), // persisted root > upward search > cwd
|
|
3391
|
+
encoding: "utf-8",
|
|
3392
|
+
timeout: 300000, // 5 min
|
|
3393
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
catch (e) {
|
|
3397
|
+
exitCode = e.status || 1;
|
|
3398
|
+
output = (e.stdout || "") + "\n" + (e.stderr || "");
|
|
3399
|
+
}
|
|
3400
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
3401
|
+
const passed = exitCode === 0;
|
|
3402
|
+
// Persist results (append)
|
|
3403
|
+
const resultsFile = path.join(dir, "smoke-results.md");
|
|
3404
|
+
const last50 = output.split("\n").slice(-50).join("\n");
|
|
3405
|
+
const entry = `\n## Run: ${ts}\n- Command: ${cmd}\n- Exit code: ${exitCode}\n- Duration: ${duration}s\n- Result: ${passed ? "PASSED" : "FAILED"}\n\n### Output (last 50 lines)\n\`\`\`\n${last50}\n\`\`\`\n`;
|
|
3406
|
+
// Create header if file doesn't exist
|
|
3407
|
+
if (!fs.existsSync(resultsFile)) {
|
|
3408
|
+
fs.writeFileSync(resultsFile, "# Smoke Test Results\n");
|
|
3409
|
+
}
|
|
3410
|
+
fs.appendFileSync(resultsFile, entry);
|
|
3411
|
+
// Debug log (full output, overwrite)
|
|
3412
|
+
if (debug) {
|
|
3413
|
+
const debugFile = path.join(dir, "smoke-debug.log");
|
|
3414
|
+
fs.writeFileSync(debugFile, `# Smoke Debug Log\n# ${ts}\n# Command: ${cmd}\n# Exit code: ${exitCode}\n\n${output}`);
|
|
3415
|
+
console.log(`[Smoke] Debug log: ${debugFile}`);
|
|
3416
|
+
}
|
|
3417
|
+
if (passed) {
|
|
3418
|
+
console.log(`[Smoke] PASSED (${duration}s)`);
|
|
3419
|
+
}
|
|
3420
|
+
else {
|
|
3421
|
+
console.log(`[Smoke] FAILED (exit code ${exitCode}, ${duration}s)`);
|
|
3422
|
+
console.log("[Smoke] Results saved to .dual-agent/smoke-results.md");
|
|
3423
|
+
}
|
|
3424
|
+
return { passed, output: last50 };
|
|
3425
|
+
}
|
|
3426
|
+
function cmdSmokeTest(_args) {
|
|
3427
|
+
const dir = (0, state_js_1.stateDir)();
|
|
3428
|
+
const result = runSmokeTest(dir);
|
|
3429
|
+
process.exit(result.passed ? 0 : 1);
|
|
3430
|
+
}
|
|
3431
|
+
function cmdSmokeCheck(_args) {
|
|
3432
|
+
const cmd = process.env.RL_SMOKE_CMD;
|
|
3433
|
+
console.log(line());
|
|
3434
|
+
console.log("Smoke Test Environment Check");
|
|
3435
|
+
console.log(line());
|
|
3436
|
+
if (!cmd) {
|
|
3437
|
+
console.log(" RL_SMOKE_CMD: (not set)");
|
|
3438
|
+
console.log("");
|
|
3439
|
+
console.log(" Configure your project's smoke test command:");
|
|
3440
|
+
console.log(' export RL_SMOKE_CMD="npm run test:e2e"');
|
|
3441
|
+
console.log(' export RL_SMOKE_CMD="pytest tests/smoke/"');
|
|
3442
|
+
console.log(' export RL_SMOKE_CMD="flutter test integration_test/"');
|
|
3443
|
+
console.log("");
|
|
3444
|
+
console.log(" Then run: ralph-lisa smoke-check");
|
|
3445
|
+
process.exit(1);
|
|
3446
|
+
}
|
|
3447
|
+
console.log(` RL_SMOKE_CMD: ${cmd}`);
|
|
3448
|
+
// Check base command exists
|
|
3449
|
+
const baseCmd = cmd.split(/\s+/)[0];
|
|
3450
|
+
try {
|
|
3451
|
+
(0, node_child_process_1.execSync)(`command -v "${baseCmd}" 2>/dev/null`, { stdio: "pipe" });
|
|
3452
|
+
console.log(` Base command '${baseCmd}': OK`);
|
|
3453
|
+
}
|
|
3454
|
+
catch {
|
|
3455
|
+
console.log(` Base command '${baseCmd}': NOT FOUND`);
|
|
3456
|
+
console.log("");
|
|
3457
|
+
console.log(` Install '${baseCmd}' or adjust RL_SMOKE_CMD.`);
|
|
3458
|
+
process.exit(1);
|
|
3459
|
+
}
|
|
3460
|
+
console.log(` RL_SMOKE_AUTO: ${process.env.RL_SMOKE_AUTO || "true"} (auto-trigger on step transition)`);
|
|
3461
|
+
console.log(` RL_SMOKE_DEBUG: ${process.env.RL_SMOKE_DEBUG || "false"} (full output capture)`);
|
|
3462
|
+
console.log("");
|
|
3463
|
+
console.log(` Tip: run the full command manually to verify: ${cmd}`);
|
|
3464
|
+
console.log(line());
|
|
3465
|
+
}
|
package/dist/policy.js
CHANGED
|
@@ -25,6 +25,15 @@ function getPolicyMode() {
|
|
|
25
25
|
*/
|
|
26
26
|
function checkRalph(tag, content) {
|
|
27
27
|
const violations = [];
|
|
28
|
+
// [PLAN] must include test plan (step42: mandatory test execution)
|
|
29
|
+
if (tag === "PLAN") {
|
|
30
|
+
if (!content.match(/测试计划|[Tt]est [Pp]lan|测试命令|[Tt]est [Cc]ommand/)) {
|
|
31
|
+
violations.push({
|
|
32
|
+
rule: "plan-test-plan",
|
|
33
|
+
message: `[PLAN] submission missing test plan (test command + coverage scope).`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
// [CODE] or [FIX] must include Test Results and file:line references
|
|
29
38
|
if (tag === "CODE" || tag === "FIX") {
|
|
30
39
|
if (!content.includes("Test Results") &&
|
|
@@ -35,6 +44,21 @@ function checkRalph(tag, content) {
|
|
|
35
44
|
message: `[${tag}] submission missing "Test Results" section.`,
|
|
36
45
|
});
|
|
37
46
|
}
|
|
47
|
+
// step42: Test Results must include concrete execution evidence (exit code or pass/fail count)
|
|
48
|
+
// Exception: explicit "Skipped:" line inside the Test Results section only
|
|
49
|
+
// Section is bounded: from "Test Results" heading to next heading (## or blank-line-then-heading) or EOF
|
|
50
|
+
const testResultsMatch = content.match(/[Tt]est [Rr]esults[^\n]*\n([\s\S]*?)(?=\n##\s|\n\n[A-Z]|\n\n\*\*[A-Z]|$)/);
|
|
51
|
+
if (testResultsMatch) {
|
|
52
|
+
const testResultsBody = testResultsMatch[1];
|
|
53
|
+
const hasSkipLine = /^[\s\-*]*[Ss]kip(ped)?\s*:.*\S/m.test(testResultsBody);
|
|
54
|
+
const hasExecutionEvidence = /[Ee]xit code|退出码|\d+\/\d+\s*(pass|通过|passed)|(\d+)\s*tests?\s*pass/i.test(testResultsBody);
|
|
55
|
+
if (!hasSkipLine && !hasExecutionEvidence) {
|
|
56
|
+
violations.push({
|
|
57
|
+
rule: "test-results-detail",
|
|
58
|
+
message: `[${tag}] Test Results must include exit code or pass/fail count (e.g., "Exit code: 0" or "42/42 passed"), or explicit "Skipped:" with justification.`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
38
62
|
if (!/\w+\.\w+:\d+/.test(content)) {
|
|
39
63
|
violations.push({
|
|
40
64
|
rule: "file-line-ref",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralph-lisa-loop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "Turn-based dual-agent collaboration: Ralph codes, Lisa reviews, consensus required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ralph-lisa": "dist/cli.js"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"test": "node --test dist/test/*.js",
|
|
12
|
+
"test:smoke": "node --test dist/test/smoke.test.js",
|
|
12
13
|
"prepack": "npm run build"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
@@ -13,6 +13,6 @@ ralph-lisa whose-turn
|
|
|
13
13
|
## Rules
|
|
14
14
|
|
|
15
15
|
- If output is `ralph`: You can proceed with your work
|
|
16
|
-
- If output is `lisa`:
|
|
16
|
+
- If output is `lisa`: Wait for Lisa's feedback — do not take further action until your turn
|
|
17
17
|
|
|
18
18
|
**NEVER skip this check before working.**
|
|
@@ -11,6 +11,6 @@ Check whose turn it is before taking any action.
|
|
|
11
11
|
## Rules
|
|
12
12
|
|
|
13
13
|
- If output is `lisa`: You can proceed with your review
|
|
14
|
-
- If output is `ralph`:
|
|
14
|
+
- If output is `ralph`: Wait for Ralph's feedback — do not take further action until your turn
|
|
15
15
|
|
|
16
|
-
**NEVER skip this check
|
|
16
|
+
**NEVER skip this check. When it's not your turn, do not submit work. You may use subagents for preparatory tasks (research, environment checks). If triggered by the user but it's not your turn, suggest checking watcher status: `cat .dual-agent/.watcher_heartbeat` and `ralph-lisa status`.**
|
package/templates/roles/lisa.md
CHANGED
|
@@ -16,16 +16,16 @@ Then based on result:
|
|
|
16
16
|
```bash
|
|
17
17
|
ralph-lisa read work.md
|
|
18
18
|
```
|
|
19
|
-
- `ralph` → Say "Waiting for Ralph" and
|
|
19
|
+
- `ralph` → Say "Waiting for Ralph's feedback" and wait — do not take further action until your turn
|
|
20
20
|
|
|
21
21
|
**Do NOT wait for user to tell you to check. Check automatically.**
|
|
22
22
|
|
|
23
23
|
## CRITICAL: Turn-Based Rules
|
|
24
24
|
|
|
25
|
-
- Output `lisa` → You can review
|
|
26
|
-
- Output `ralph` →
|
|
25
|
+
- Output `lisa` → You can review. If it's your turn but you cannot complete work (missing input, environment error, etc.), tell the user the specific reason and wait — do not retry repeatedly.
|
|
26
|
+
- Output `ralph` → Tell user it's not your turn. You may use subagents for preparatory work, but do not submit until it is your turn.
|
|
27
27
|
|
|
28
|
-
**NEVER skip this check.
|
|
28
|
+
**NEVER skip this check. When it's not your turn, do not submit work. You may use subagents for preparatory tasks (research, environment checks). If triggered by the user but it's not your turn, suggest checking watcher status: `cat .dual-agent/.watcher_heartbeat` and `ralph-lisa status`.**
|
|
29
29
|
|
|
30
30
|
## How to Submit
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ ralph-lisa submit-lisa --file .dual-agent/submit.md
|
|
|
38
38
|
|
|
39
39
|
Inline mode (`ralph-lisa submit-lisa "[TAG] ..."`) is deprecated — it breaks on special characters. Use `--file` or `--stdin` instead.
|
|
40
40
|
|
|
41
|
-
This automatically passes the turn to Ralph. Then
|
|
41
|
+
This automatically passes the turn to Ralph. Then wait — do not take further action until it is your turn again.
|
|
42
42
|
|
|
43
43
|
## Tags You Can Use
|
|
44
44
|
|
|
@@ -59,7 +59,7 @@ This automatically passes the turn to Ralph. Then you MUST STOP.
|
|
|
59
59
|
3. Review following the behavior spec below
|
|
60
60
|
4. Write review to .dual-agent/submit.md
|
|
61
61
|
5. ralph-lisa submit-lisa --file .dual-agent/submit.md
|
|
62
|
-
6.
|
|
62
|
+
6. Wait for Ralph's response
|
|
63
63
|
7. ralph-lisa whose-turn → Check again
|
|
64
64
|
8. Repeat
|
|
65
65
|
```
|
|
@@ -101,20 +101,21 @@ This is your PRIMARY responsibility — catching direction drift early saves mor
|
|
|
101
101
|
| Cite `file:line` | Every `[PASS]` or `[NEEDS_WORK]` must reference at least one specific `file:line` location to support your conclusion. |
|
|
102
102
|
| View full file context | When reviewing changes, read the full file (not just the diff snippet) to understand surrounding context. |
|
|
103
103
|
| Check research | If the task involves reference implementations, protocols, or external APIs, verify that `[RESEARCH]` was submitted before `[CODE]`. |
|
|
104
|
+
| Verify test execution | For `[CODE]`/`[FIX]`, verify Test Results contain actual command, exit code, and pass/fail count — OR an explicit `Skipped:` with valid justification (e.g., config-only, no testable logic). If results look suspicious (missing numbers, generic text), return `[NEEDS_WORK]`. |
|
|
105
|
+
| Re-run tests | For `[CODE]`/`[FIX]` with executed tests, run the test command yourself to verify results. For skipped tests, verify the justification is valid. Report your findings in the review. |
|
|
106
|
+
| Verify test plan alignment | For `[CODE]`/`[FIX]`, verify Test Results match the test plan from the `[PLAN]` phase. If tests differ from the plan without explanation, return `[NEEDS_WORK]`. |
|
|
104
107
|
|
|
105
108
|
### SHOULD (professional standard)
|
|
106
109
|
|
|
107
110
|
| Recommendation | Details |
|
|
108
111
|
|----------------|---------|
|
|
109
112
|
| Check test quality | Examine test files for coverage, assertion strength, and edge case handling. |
|
|
110
|
-
| Verify test results | Confirm that Ralph's reported test results are plausible given the changes. |
|
|
111
113
|
| Look for regressions | Consider whether changes could break existing functionality. |
|
|
112
114
|
|
|
113
115
|
### YOUR JUDGMENT (not prescribed)
|
|
114
116
|
|
|
115
117
|
| Area | Details |
|
|
116
118
|
|------|---------|
|
|
117
|
-
| Run tests yourself | You may choose to run tests independently. This is your professional call. |
|
|
118
119
|
| Write verification tests | When static analysis is insufficient, write ad-hoc tests in `.dual-agent/tests/` and reference the output in your review. These are auto-cleaned on [CONSENSUS]. |
|
|
119
120
|
| Review depth | Decide what to focus on based on risk and complexity. |
|
|
120
121
|
| Accept or reject | Your verdict is your own professional judgment. |
|
|
@@ -125,7 +126,8 @@ This is your PRIMARY responsibility — catching direction drift early saves mor
|
|
|
125
126
|
- [ ] Logic correct
|
|
126
127
|
- [ ] Edge cases handled
|
|
127
128
|
- [ ] Tests adequate
|
|
128
|
-
- [ ] **Test Results
|
|
129
|
+
- [ ] **Test Results verified** — `[CODE]`/`[FIX]` must have actual command + exit code + pass count, or explicit `Skipped:` with valid justification
|
|
130
|
+
- [ ] **Tests re-run** — You ran the test command yourself and confirmed results match (or verified skip justification)
|
|
129
131
|
- [ ] **Research adequate** (if task involves reference implementations/protocols/external APIs, check that [RESEARCH] was submitted)
|
|
130
132
|
- [ ] **Research verified** — [RESEARCH] submissions must include at least one `Verified:` or `Evidence:` marker. Reject unverified claims.
|
|
131
133
|
- [ ] **Factual claims verified** — For claims that a feature is "missing" or "not implemented", require `file:line` evidence or explicit acknowledgment that source code was not accessible
|
|
@@ -150,10 +152,16 @@ Lisa: [NEEDS_WORK] ...
|
|
|
150
152
|
Ralph: [FIX] Agree, because... / [CHALLENGE] Disagree, because...
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
## Long-Running Tasks
|
|
156
|
+
|
|
157
|
+
For time-consuming operations (large-scale code review, batch test re-runs, deep research verification), consider using subagents or background tasks to work in parallel. Summarize subagent results before submitting your review.
|
|
158
|
+
|
|
159
|
+
This avoids blocking the main collaboration loop while waiting for slow operations to complete.
|
|
160
|
+
|
|
153
161
|
## Handling Disagreement
|
|
154
162
|
|
|
155
163
|
If Ralph uses [CHALLENGE]:
|
|
156
164
|
1. Consider his argument carefully
|
|
157
165
|
2. If convinced → Change your verdict
|
|
158
166
|
3. If not → Explain your reasoning with [CHALLENGE] or [DISCUSS]
|
|
159
|
-
4. After 5 rounds →
|
|
167
|
+
4. After 5 rounds → Deadlock auto-detected, watcher pauses for user intervention
|
package/templates/roles/ralph.md
CHANGED
|
@@ -16,16 +16,16 @@ Then based on result:
|
|
|
16
16
|
```bash
|
|
17
17
|
ralph-lisa read review.md
|
|
18
18
|
```
|
|
19
|
-
- `lisa` → Say "Waiting for Lisa" and
|
|
19
|
+
- `lisa` → Say "Waiting for Lisa's feedback" and wait — do not take further action until your turn
|
|
20
20
|
|
|
21
21
|
**Do NOT wait for user to tell you to check. Check automatically.**
|
|
22
22
|
|
|
23
23
|
## CRITICAL: Turn-Based Rules
|
|
24
24
|
|
|
25
|
-
- Output `ralph` → You can work
|
|
26
|
-
- Output `lisa` →
|
|
25
|
+
- Output `ralph` → You can work. If it's your turn but you cannot complete work (missing input, environment error, etc.), tell the user the specific reason and wait — do not retry repeatedly.
|
|
26
|
+
- Output `lisa` → Tell user it's not your turn. You may use subagents for preparatory work, but do not submit until it is your turn.
|
|
27
27
|
|
|
28
|
-
**NEVER skip this check.
|
|
28
|
+
**NEVER skip this check. When it's not your turn, do not submit work. You may use subagents for preparatory tasks (research, environment checks). If triggered by the user but it's not your turn, suggest checking watcher status: `cat .dual-agent/.watcher_heartbeat` and `ralph-lisa status`.**
|
|
29
29
|
|
|
30
30
|
## How to Submit
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ ralph-lisa submit-ralph --file .dual-agent/submit.md
|
|
|
38
38
|
|
|
39
39
|
Inline mode (`ralph-lisa submit-ralph "[TAG] ..."`) is deprecated — it breaks on special characters. Use `--file` or `--stdin` instead.
|
|
40
40
|
|
|
41
|
-
This automatically passes the turn to Lisa. Then
|
|
41
|
+
This automatically passes the turn to Lisa. Then wait — do not take further action until it is your turn again.
|
|
42
42
|
|
|
43
43
|
## Tags You Can Use
|
|
44
44
|
|
|
@@ -74,10 +74,15 @@ This is required when the task involves reference implementations, protocols, or
|
|
|
74
74
|
|
|
75
75
|
**[CODE] or [FIX] submissions must include:**
|
|
76
76
|
|
|
77
|
-
### Test Results
|
|
78
|
-
- Test command:
|
|
79
|
-
-
|
|
80
|
-
-
|
|
77
|
+
### Test Results (must be from actual execution, not fabricated)
|
|
78
|
+
- Test command: the exact command you ran (e.g., `pytest -x`, `npm test`)
|
|
79
|
+
- Exit code: 0 (all passed) or non-zero (failures)
|
|
80
|
+
- Result: X/Y passed (concrete numbers)
|
|
81
|
+
- Failed output: if any failures, include last 30 lines of error output
|
|
82
|
+
- If skipping tests, must explain why — Lisa will judge whether the reason is valid
|
|
83
|
+
- Tests must follow the test plan established in the `[PLAN]` phase
|
|
84
|
+
- Test Results must reference the planned test command
|
|
85
|
+
- If the test plan changed, explain why in the submission
|
|
81
86
|
|
|
82
87
|
## Round 1: Mandatory [PLAN]
|
|
83
88
|
|
|
@@ -86,6 +91,13 @@ your understanding of the task before you start coding. Include:
|
|
|
86
91
|
- Your understanding of the task goal
|
|
87
92
|
- Proposed approach
|
|
88
93
|
- Expected deliverables
|
|
94
|
+
- **Test plan** (mandatory):
|
|
95
|
+
- Test command (e.g., `pytest -x`, `npm test`, `go test ./...`, `flutter test`)
|
|
96
|
+
- Expected test coverage scope
|
|
97
|
+
- If no test framework exists, explain verification approach
|
|
98
|
+
- **Quality gate commands** (recommended): Identify lint/format/type-check commands for the project
|
|
99
|
+
- Examples: `npm run lint`, `ruff check .`, `go vet ./...`
|
|
100
|
+
- These can be configured via `RL_RALPH_GATE` + `RL_GATE_COMMANDS` for auto mode
|
|
89
101
|
|
|
90
102
|
## Workflow
|
|
91
103
|
|
|
@@ -96,7 +108,7 @@ your understanding of the task before you start coding. Include:
|
|
|
96
108
|
→ Submit [RESEARCH] first, wait for Lisa's review
|
|
97
109
|
4. Write content to .dual-agent/submit.md
|
|
98
110
|
5. ralph-lisa submit-ralph --file .dual-agent/submit.md
|
|
99
|
-
6.
|
|
111
|
+
6. Wait for Lisa's response
|
|
100
112
|
7. ralph-lisa whose-turn → Check again
|
|
101
113
|
8. (If ralph) Read Lisa's feedback: ralph-lisa read review.md
|
|
102
114
|
9. Respond or proceed based on feedback
|
|
@@ -121,13 +133,17 @@ After context compaction, run `ralph-lisa recap` to recover current state:
|
|
|
121
133
|
|
|
122
134
|
## Handling Lisa's Feedback
|
|
123
135
|
|
|
124
|
-
- `[PASS]` →
|
|
136
|
+
- `[PASS]` → First check PASS quality:
|
|
137
|
+
- Does Lisa's PASS include substantive review content (specific file checks, test verification, technical analysis)?
|
|
138
|
+
- If it's a rubber-stamp PASS (no specific reasons, no code references, no test verification), submit `[CHALLENGE]` requesting substantive review — **at most once**
|
|
139
|
+
- If Lisa resubmits PASS after your challenge (even if still thin), accept and submit `[CONSENSUS]` to avoid infinite loop
|
|
140
|
+
- If it's a substantive PASS and you agree, submit `[CONSENSUS]`
|
|
125
141
|
- `[NEEDS_WORK]` → You MUST explain your reasoning:
|
|
126
142
|
- If you agree: explain WHY Lisa is right, then submit [FIX]
|
|
127
143
|
- If you disagree: use [CHALLENGE] to provide counter-argument
|
|
128
144
|
- **Never submit a bare [FIX] without explanation. No silent acceptance.**
|
|
129
145
|
- **You CANNOT submit [CODE]/[RESEARCH]/[PLAN] after NEEDS_WORK** — the CLI will reject it. Address the feedback first, or run `ralph-lisa scope-update` if the task scope changed.
|
|
130
|
-
- After
|
|
146
|
+
- After 8 consecutive NEEDS_WORK rounds → DEADLOCK auto-detected, watcher pauses for user intervention
|
|
131
147
|
|
|
132
148
|
## Submission Test Requirements
|
|
133
149
|
|
|
@@ -144,6 +160,12 @@ After context compaction, run `ralph-lisa recap` to recover current state:
|
|
|
144
160
|
- "New tests: 0" requires justification (valid: pure UI layout, config-only change)
|
|
145
161
|
- Invalid excuse: "requires E2E" for pure functions, data shape validation, or mock-able IPC
|
|
146
162
|
|
|
163
|
+
## Long-Running Tasks
|
|
164
|
+
|
|
165
|
+
For time-consuming operations (large-scale code search, batch test runs, CI waits, complex refactoring), consider using subagents or background tasks to work in parallel. Summarize subagent results before submitting.
|
|
166
|
+
|
|
167
|
+
This avoids blocking the main collaboration loop while waiting for slow operations to complete.
|
|
168
|
+
|
|
147
169
|
## Your Responsibilities
|
|
148
170
|
|
|
149
171
|
1. Planning and coding
|
package/templates/skill.json
CHANGED
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"rules": {
|
|
23
23
|
"consensus": "Both parties must agree before proceeding",
|
|
24
24
|
"verdict": "PASS/NEEDS_WORK is advisory, not a command",
|
|
25
|
-
"deadlock": "After
|
|
25
|
+
"deadlock": "After 8 consecutive NEEDS_WORK rounds, watcher pauses for user intervention"
|
|
26
26
|
}
|
|
27
27
|
}
|