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 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 Escape** — After 5 rounds: `[OVERRIDE]` or `[HANDOFF]` to human
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)");
@@ -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
- if (newCount >= 5) {
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 "[TAG] summary
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 v4 - round-based change detection + persistent state
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 log_file="\$1"
2021
+ local pane="\$1"
1918
2022
  local stable_seconds="\${2:-5}"
1919
2023
 
1920
- if [[ ! -f "\$log_file" ]]; then
1921
- return 0
1922
- fi
1923
-
1924
- local mtime_epoch now_epoch elapsed
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 (( elapsed >= stable_seconds )); then
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 "\$log_file" 5; then
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
- sleep 3
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
- # 6. Post-send verification: wait up to 20s for agent to start responding
2041
- # Record size AFTER send+retry completes (not before), so we only measure
2042
- # the agent's actual response, not the injected text appearing in the pane.
2043
- local post_send_baseline=0
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
- post_send_baseline=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ')
2157
+ log_size_before=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ')
2046
2158
  fi
2047
- local verify_wait=0
2048
- while (( verify_wait < 20 )); do
2049
- sleep 4
2050
- verify_wait=\$((verify_wait + 4))
2051
- if [[ -f "\$log_file" ]]; then
2052
- local cur_size
2053
- cur_size=\$(wc -c < "\$log_file" 2>/dev/null | tr -d ' ')
2054
- if (( cur_size > post_send_baseline + 100 )); then
2055
- echo "[Watcher] OK: \$agent_name responded (output grew +\$((cur_size - post_send_baseline)) bytes)"
2056
- return 0
2057
- fi
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
- done
2181
+ fi
2060
2182
 
2061
- echo "[Watcher] WARN: \$agent_name did not produce output after send — may not have received message"
2062
- return 1
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 check_consensus_reached; then
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
- # Check for context limit in pane output (unrecoverable notify user immediately)
2392
- local pane_tail
2393
- pane_tail=\$(tmux capture-pane -t "\${SESSION}:\${target_pane}" -p 2>/dev/null | tail -10)
2394
- if echo "\$pane_tail" | grep -qiE "context limit|conversation too long|token limit|context window"; then
2395
- if (( REMINDER_LEVEL < 3 )); then
2396
- echo "[Watcher] CONTEXT LIMIT detected for \$target_name. Manual intervention required."
2397
- echo "[Watcher] Restart the agent session to continue."
2398
- REMINDER_LEVEL=3
2399
- fi
2400
-
2401
- # Time-based escalation: each level checked independently by elapsed time.
2402
- # If L1/L2 delivery fails, time still advances, so L3 is always reachable.
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
- # Level 3: notify user after 10 minutes always reachable regardless of L1/L2 success
2405
- elif (( elapsed >= 600 && REMINDER_LEVEL < 3 )); then
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
- # Level 2: slash command after 5 minutes, with prompt guard
2410
- elif (( elapsed >= 300 && REMINDER_LEVEL < 2 )); then
2411
- if ! check_for_interactive_prompt "\$target_pane"; then
2412
- echo "[Watcher] Escalation L2: Sending /check-turn to \$target_name (no response for \${elapsed}s)"
2413
- if send_go_to_pane "\$target_pane" "\$target_name" "\$target_log" "/check-turn"; then
2414
- REMINDER_LEVEL=2
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
- # Level 1: REMINDER after 2 minutes
2421
- elif (( elapsed >= 120 && REMINDER_LEVEL < 1 )); then
2422
- echo "[Watcher] Escalation L1: Sending REMINDER to \$target_name (no response for \${elapsed}s)"
2423
- if send_go_to_pane "\$target_pane" "\$target_name" "\$target_log" "REMINDER: It is your turn. Please check turn and continue working."; then
2424
- REMINDER_LEVEL=1
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 v4... (Ctrl+C to stop)"
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.11",
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`: STOP immediately and wait for Lisa's response
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.**
@@ -36,4 +36,4 @@ Detailed content here...
36
36
 
37
37
  ## After Submission
38
38
 
39
- The turn automatically passes to Lisa. You must STOP and wait.
39
+ The turn automatically passes to Lisa. Wait for her feedback — do not take further action until it is your turn again.
@@ -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`: STOP immediately and wait for Ralph's submission
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 before working.**
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`.**
@@ -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 STOP
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` → STOP immediately, tell user "Waiting for 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. NEVER work when it's not your turn.**
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 you MUST STOP.
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. STOP and wait for Ralph
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 included in submission** (required for [CODE]/[FIX])
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 → Accept OVERRIDE or propose HANDOFF
167
+ 4. After 5 rounds → Deadlock auto-detected, watcher pauses for user intervention
@@ -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 STOP
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` → STOP immediately, tell user "Waiting for 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. NEVER work when it's not your turn.**
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 you MUST STOP.
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: `npm test` / `pytest` / ...
79
- - Result: Passed / Failed (reason)
80
- - If skipping tests, must explain why
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. STOP and wait for Lisa
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]` → Submit [CONSENSUS] to close. Lisa's [PASS] already approves — no need to wait for her [CONSENSUS] back (single-round consensus).
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 3 consecutive NEEDS_WORK rounds → DEADLOCK auto-detected, watcher pauses for user intervention
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
@@ -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 5 rounds, use OVERRIDE or HANDOFF"
25
+ "deadlock": "After 8 consecutive NEEDS_WORK rounds, watcher pauses for user intervention"
26
26
  }
27
27
  }