@ai-dev-methodologies/rlp-desk 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ # rlp-desk 0.11.1 — Tmux session/pane lifecycle resilience (ralplan v3)
2
+
3
+ > v3: Codex Critic ITERATE 흡수 (7 patches): 단일 5s 권위 timeout, mid-iter pane death 감지, SESSION_NAME `$$` + rand 충돌 회피, destroy-unattached 한계 명시, shasum 대체 체인, mkdir atomic lock, self-V mechanical fixture (grep-only 금지).
4
+ > v2: Architect 1차 ITERATE 흡수 — ground-truth 검증으로 bug premise 수정. 실제 session-config.json pane 필드는 정상 기록, 진짜 결함은 tmux session 자체 사라짐.
5
+
6
+ ## Context
7
+
8
+ 소비자 handoff `coordination/handoffs/2026-04-26-rlp-desk-tmux-pane-disappearance-bug.md` (P0) + ground-truth 검증.
9
+
10
+ ### 보고된 증상 vs 실제 ground truth
11
+
12
+ | 항목 | 보고 | 실제 (검증 후) |
13
+ |---|---|---|
14
+ | session-config.json pane 필드 | 4 fields = null | leader=%1007, worker=%1016, verifier=%1017 (정상) |
15
+ | tmux pane lifecycle | %1014/%1015 사라짐 | session `ai-blog-system-624` 자체가 사라짐 → 모든 pane 함께 사망 |
16
+ | process state | 살아있음 | runner pid 83304 살아있음 (tmux session 만 dropped) |
17
+
18
+ **진짜 문제**: tmux session 의 lifetime 이 wrapper terminal / claude-code session 의 lifetime 과 묶여 있어서, 외부 close 시 session 이 사망 → 모든 pane id 가 stale 됨.
19
+
20
+ ### 검증 evidence (현재 시각)
21
+
22
+ ```
23
+ $ tmux ls | grep ai-blog
24
+ ai-blog-system-625 (ai-blog-system-624 부재)
25
+
26
+ $ cat .../blog-v31-flywheel-telemetry/runtime/session-config.json | jq .panes
27
+ { "leader": "%1007", "worker": "%1016", "verifier": "%1017" }
28
+ ```
29
+
30
+ → pane id 는 작성 시점엔 valid 였으나 session 이 사라져 pane 도 dead.
31
+
32
+ ## 근본 원인 (revised)
33
+
34
+ **session lifecycle ↔ wrapper lifecycle decoupling 부족**:
35
+
36
+ 1. **H1 (확인)** — runner 가 `tmux new-session -d -s "$SESSION_NAME"` 으로 detached session 생성. 그러나 wrapper 가 nohup 으로 spawn 시 wrapper 자신의 terminal close 가 자식 tmux client 도 함께 끊고, attached client 가 0 이 되면 일부 환경에서 session GC 됨 (특히 tmux server 재시작 / 사용자 manual kill).
37
+ 2. **H2 (확인)** — wrapper duplicate spawn race (96581 + 83265) 로 두 wrapper 가 동일 desk 의 다른 mission 진입. 한 쪽 cleanup 이 다른 쪽 session 영향.
38
+ 3. **H3 (가능성 낮음, 폐기)** — pane id 캡처 시점 race. 실제 file 검증 결과 pane id 는 valid → 캡처 자체는 성공.
39
+
40
+ → H1 + H2 가 주범. 보고된 H3 (캡처 race) 는 실제 ground truth 와 모순되어 폐기.
41
+
42
+ ## RALPLAN-DR
43
+
44
+ **Principles**:
45
+ 1. **Fail loud, not silent** — session/pane 사망 시 명시 alert (next iter 진입 직전 detect)
46
+ 2. **Defense-in-depth** — H1 + H2 동시 차단 (단일 fix 부족)
47
+ 3. **Backward-compat** — 기존 single-mission 인터랙티브 운영 그대로
48
+ 4. **Self-verification mechanical** — 변경 코드 직접 invoke + grep anti-tautology
49
+
50
+ **Decision Drivers**:
51
+ 1. session 이 외부 영향으로 사라져도 wrapper / 사용자 가 즉시 인지
52
+ 2. duplicate wrapper spawn 시 second-mover 가 명시 reject
53
+ 3. 작성된 session-config 가 "live" 와 "stale" 구분 가능
54
+
55
+ **Viable Options**:
56
+
57
+ - **A (채택)** — 3-pronged + Architect ITERATE 흡수:
58
+ - **R12 — Pane lifecycle monitor** — 3 검증 시점: (a) `create_session()` 직후, (b) main loop 매 iter 진입 직전, (c) 매 worker/verifier `send-keys` 직후 wait-loop 진입 직전. 각 pane `#{pane_dead}` + session `has-session` 확인. dead 발견 시 즉시 BLOCKED with `reason_category=infra_failure` + recoverable=true + suggested_action=restart. **단일 권위 timeout: 5s 총 — 1초 간격 5회 polling 후 fail (Critic 불일치 해소)**.
59
+ - **R13 — Detached session protection** — RLP_BACKGROUND=1 이면 `tmux set -t "$SESSION_NAME" destroy-unattached off` 적용해 attached client 0 일 때도 session 유지. `tmux new-session` exit code 명시 검증, fail 시 dedicated 새 이름 (`${SESSION_NAME}-bg-$(date +%s)`) 으로 retry 1회. **NEW-3: SESSION_NAME 이미 SLUG 포함하므로 중복 suffix 안 함**.
60
+ - **R14 — Project-scoped runner lockfile** — `RUNNER_LOCKFILE_PATH="$DESK/logs/.rlp-desk-runner-$(echo "$ROOT" | shasum | cut -c1-8).lock"`. 동일 project root 에서 duplicate runner spawn 차단, 다른 project 의 동시 runner 는 허용. stale pid (`kill -0` fail) 시 갱신 + log 안내.
61
+ - B — R14 only (race 차단으로 충분) — H1 (session GC) 잔존 → 폐기.
62
+ - C — skip background mode — wrapper API breaking → 폐기.
63
+
64
+ **Pre-implementation gate (NEW-4)**: 본 plan 채택 전, 위 ground-truth 검증 (실제 session-config.json 파일 + `tmux ls` 출력) 완료. 실제 결함 = session 사망 + lockfile 부재 두 축으로 확정.
65
+
66
+ ## 해결 계획
67
+
68
+ ### Fix R12: Pane lifecycle monitor + bounded retry
69
+
70
+ **대상**: `src/scripts/lib_ralph_desk.zsh` 신규 helper + `src/scripts/run_ralph_desk.zsh` main loop 진입점
71
+
72
+ **변경**:
73
+ 1. `lib_ralph_desk.zsh` 신규:
74
+ ```zsh
75
+ _verify_pane_alive() {
76
+ local pane_id="$1"
77
+ [[ -z "$pane_id" ]] && return 1
78
+ local dead
79
+ dead=$(tmux display-message -p -t "$pane_id" '#{pane_dead}' 2>/dev/null)
80
+ [[ "$dead" == "0" ]]
81
+ }
82
+ _verify_session_alive() {
83
+ local session="$1"
84
+ [[ -z "$session" ]] && return 1
85
+ tmux has-session -t "$session" 2>/dev/null
86
+ }
87
+ ```
88
+ 2. `run_ralph_desk.zsh` 3 검증 시점에 helper 호출:
89
+ ```zsh
90
+ _r12_check_lifecycle() {
91
+ local site="$1" # "create" | "iter_start" | "post_send"
92
+ local _attempts=0
93
+ while ! _verify_session_alive "$SESSION_NAME" || \
94
+ ! _verify_pane_alive "$LEADER_PANE" || \
95
+ ! _verify_pane_alive "$WORKER_PANE" || \
96
+ ! _verify_pane_alive "$VERIFIER_PANE"; do
97
+ (( _attempts++ ))
98
+ if (( _attempts >= 5 )); then
99
+ log_error "[r12:$site] tmux session/pane dead after 5×1s polling (5s total budget). session=$SESSION_NAME panes leader=$LEADER_PANE worker=$WORKER_PANE verifier=$VERIFIER_PANE"
100
+ tmux list-panes -a -F '#{session_name}:#{pane_id} dead=#{pane_dead}' 2>&1 | head -20 >> "$DEBUG_LOG"
101
+ write_blocked_sentinel "tmux session/pane dead during $site" "${CURRENT_US:-ALL}" "infra_failure"
102
+ exit 1
103
+ fi
104
+ sleep 1
105
+ done
106
+ }
107
+ ```
108
+ 호출: `create_session` 끝, main loop 진입 직전, 모든 `paste_to_pane`/`send-keys` 직후 wait-loop 시작 전.
109
+ 3. **단일 권위 timeout: 5s 총** (5회 × 1s polling), 다른 모든 "3 retries"/"4s" 표현 제거.
110
+
111
+ **검증 (us024)**:
112
+ - AC1: `_verify_pane_alive` + `_verify_session_alive` helper 정의
113
+ - AC2: create_session + main loop iter 진입 + post-send-keys 3 시점에서 caller 가 helper 호출
114
+ - AC3: behavioural — 죽은 pane id fixture → exit 1 with `infra_failure` sentinel
115
+ - AC4 (Critic): mid-iter pane kill fixture — worker pane 을 send-keys 직후 외부에서 kill → 다음 wait-loop 진입 시 R12 가 5s 안에 BLOCKED with `reason_category=infra_failure`
116
+
117
+ ### Fix R13: Detached session protection + new-session exit-code verify
118
+
119
+ **대상**: `src/scripts/run_ralph_desk.zsh:744` `create_session()`
120
+
121
+ **변경**:
122
+ 1. `tmux new-session -d -s "$SESSION_NAME"` 실행 후 즉시 `$?` 검증:
123
+ ```zsh
124
+ if ! tmux new-session -d -s "$SESSION_NAME" -x 200 -y 50 -c "$ROOT" 2>/dev/null; then
125
+ if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
126
+ if [[ "${RLP_BACKGROUND:-0}" == "1" ]]; then
127
+ # daemon mode: 충돌 회피 (Critic NEW-3: epoch + pid + rand 4-digit 까지 강화)
128
+ SESSION_NAME="${SESSION_NAME}-bg-$(date +%s)-$$"
129
+ while tmux has-session -t "$SESSION_NAME" 2>/dev/null; do
130
+ SESSION_NAME="${SESSION_NAME}-$(awk 'BEGIN{srand();print int(1000+rand()*9000)}')"
131
+ done
132
+ tmux new-session -d -s "$SESSION_NAME" -x 200 -y 50 -c "$ROOT" || die "tmux new-session retry failed: $SESSION_NAME"
133
+ fi
134
+ else
135
+ die "tmux new-session failed and session does not exist: $SESSION_NAME"
136
+ fi
137
+ fi
138
+ ```
139
+ 2. RLP_BACKGROUND=1 이면 새/재생성된 session 마다 즉시 `tmux set-option -t "$SESSION_NAME" destroy-unattached off` 호출 — attached client 0 일 때도 session 유지.
140
+ **한계 명시 (Critic R13)**: 이 옵션은 best-effort. **수동 `tmux kill-session` 또는 tmux server 재시작에는 보호 안 됨**. 둘 중 하나가 발생하면 session 은 사라지며, R12 (lifecycle monitor) 가 다음 검증 시점에서 BLOCKED 처리한다.
141
+
142
+ **검증 (us025)**:
143
+ - AC1: `tmux new-session` 실패 시 dedicated 이름으로 retry 1회 (RLP_BACKGROUND only)
144
+ - AC2: RLP_BACKGROUND=1 시 `destroy-unattached off` 호출 grep
145
+ - AC3: SESSION_NAME 변경 시 session-config 의 session_name 가 최종 이름 반영
146
+
147
+ ### Fix R14: Project-scoped runner lockfile
148
+
149
+ **대상**: `src/scripts/run_ralph_desk.zsh:231` 부근 (LOCKFILE_PATH 정의)
150
+
151
+ **변경**:
152
+ 1. 신규 변수 — shasum 대체 체인 (Critic R14 portability):
153
+ ```zsh
154
+ ROOT_HASH=$(printf '%s' "$ROOT" | { shasum 2>/dev/null || sha1sum 2>/dev/null || cksum; } | awk '{print substr($1,1,8)}')
155
+ RUNNER_LOCKFILE_PATH="$DESK/logs/.rlp-desk-runner-$ROOT_HASH.lock"
156
+ RUNNER_LOCKDIR="${RUNNER_LOCKFILE_PATH}.d"
157
+ ```
158
+ 2. 기존 `LOCKFILE_PATH` (per-SLUG) 그대로 유지 — concurrent same-slug 차단
159
+ 3. **mkdir atomic lock 패턴 (Critic R14 race fix)** — check-then-write race 차단:
160
+ ```zsh
161
+ if ! mkdir "$RUNNER_LOCKDIR" 2>/dev/null; then
162
+ existing=$(jq -r '.pid' "$RUNNER_LOCKFILE_PATH" 2>/dev/null || echo 0)
163
+ existing_slug=$(jq -r '.slug // "unknown"' "$RUNNER_LOCKFILE_PATH" 2>/dev/null || echo unknown)
164
+ if [[ "$existing" -gt 0 ]] && kill -0 "$existing" 2>/dev/null; then
165
+ log_error "duplicate rlp-desk runner detected on this project root. existing pid=$existing slug=$existing_slug, this attempt slug=$SLUG. exiting."
166
+ echo " Recover with: rm -rf '$RUNNER_LOCKDIR' '$RUNNER_LOCKFILE_PATH' (after confirming pid $existing is not active)" >&2
167
+ exit 1
168
+ fi
169
+ # stale: 다른 wrapper 가 이미 stale 청소 중일 수 있음 — atomic mkdir 재시도
170
+ rm -rf "$RUNNER_LOCKDIR"
171
+ mkdir "$RUNNER_LOCKDIR" 2>/dev/null || {
172
+ log_error "failed to acquire runner lock after stale cleanup; another wrapper raced ahead. exit 1"
173
+ exit 1
174
+ }
175
+ log " stale runner lockfile cleaned (pid $existing dead) — acquired"
176
+ fi
177
+ printf '{"pid":%s,"slug":"%s","root":"%s","started_at":"%s"}\n' \
178
+ "$$" "$SLUG" "$ROOT" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$RUNNER_LOCKFILE_PATH"
179
+ ```
180
+ 4. cleanup trap 에서 own_slug 확인 후 `RUNNER_LOCKDIR` + `RUNNER_LOCKFILE_PATH` 둘 다 rm:
181
+ ```zsh
182
+ if [[ -f "$RUNNER_LOCKFILE_PATH" ]]; then
183
+ own_slug=$(jq -r '.slug' "$RUNNER_LOCKFILE_PATH" 2>/dev/null)
184
+ [[ "$own_slug" == "$SLUG" ]] && rm -rf "$RUNNER_LOCKDIR" "$RUNNER_LOCKFILE_PATH"
185
+ fi
186
+ ```
187
+
188
+ **검증 (us026)**:
189
+ - AC1: `RUNNER_LOCKFILE_PATH` 변수 정의 + project root hash
190
+ - AC2: 동일 root 에서 alive duplicate runner → exit 1 + 명시 메시지
191
+ - AC3: stale pid 시 lockfile 갱신 (no exit)
192
+ - AC4: 다른 root (다른 hash) 의 동시 runner 는 허용 (multi-project parallelism preserved)
193
+ - AC5: cleanup trap 이 own_slug 일치 시만 삭제
194
+
195
+ ### Self-verification scenario (mechanical, real fixture)
196
+
197
+ `tests/test_self_verification_0_11_1.sh` — **grep-only 금지 (Critic Self-V)**. 각 함수가:
198
+ 1. 임시 desk fixture (mktemp dir + plans/PRD + memos/)
199
+ 2. 실제 helper 직접 invoke (zsh -c source) 또는 mini runner 진입
200
+ 3. 구체 process exit code + 생성된 파일 / log line 검증
201
+ 4. anti-tautology 보조 grep — primary 가 아닌 secondary
202
+
203
+ ```bash
204
+ test_r12_pane_dead_blocks() {
205
+ # 1) 가짜 dead pane id 로 _verify_pane_alive 호출
206
+ # 2) tmux new-session 으로 alive session 만든 후 일부러 kill
207
+ # 3) helper 가 false 반환하는지 + 호출자가 exit 1 + sentinel 작성하는지 확인
208
+ zsh -c "source $LIB; _verify_pane_alive '%99999'" && fail "expected dead detection"
209
+ # ... real fixture run + assert sentinel.md exists with reason_category=infra_failure
210
+ }
211
+ test_r13_session_disambiguation() {
212
+ # 1) tmux new-session -d -s "test-session-fixture" alive
213
+ # 2) RLP_BACKGROUND=1 + SESSION_NAME="test-session-fixture" 으로 create_session-like 진입
214
+ # 3) 실제 새로 생긴 session 이름이 ${name}-bg-... 인지 + alive 인지 확인
215
+ }
216
+ test_r14_lockfile_duplicate_reject() {
217
+ # 1) RUNNER_LOCKDIR mkdir
218
+ # 2) ${LOCK}/pid file 에 alive pid 작성 (sleep & 으로 백그라운드)
219
+ # 3) 두 번째 mkdir 시도 → exit 1 + stderr 에 "duplicate" 출력 검증
220
+ }
221
+ test_r14_lockfile_other_root_allowed() {
222
+ # 1) ROOT=/tmp/r1 인 lockfile 존재
223
+ # 2) ROOT=/tmp/r2 의 hash 가 다름 → 두 번째 mkdir 성공
224
+ }
225
+ ```
226
+ 각 함수 종료 시 (a) exit code 검증, (b) 생성된 sentinel/log 파일 존재 확인, (c) 패치된 함수가 호출되었음을 grep 으로 secondary 증명.
227
+
228
+ ## 변경 대상 파일
229
+
230
+ ```
231
+ src/scripts/run_ralph_desk.zsh # R12 caller, R13 create_session 가드, R14 lockfile
232
+ src/scripts/lib_ralph_desk.zsh # R12 _verify_pane_alive, _verify_session_alive
233
+ src/governance.md # §7e (lane 옆) 신규 §7h "Tmux session lifecycle"
234
+ tests/test_us024_pane_lifecycle.sh
235
+ tests/test_us025_session_disambiguation.sh
236
+ tests/test_us026_runner_lockfile.sh
237
+ tests/test_self_verification_0_11_1.sh
238
+ ```
239
+
240
+ ## 검증
241
+
242
+ 1. **LOW** — `zsh -n`, `node --check` (~10s)
243
+ 2. **MEDIUM** — us024–026 신규 (~30s)
244
+ 3. **CRITICAL** — us017–023 + us012–016 + us001/us007 무손실 (~3min)
245
+ 4. **자가검증 매핑** — 4 함수 mechanical anti-tautology
246
+
247
+ ## ADR
248
+
249
+ - **Decision**: R12 (pane/session monitor + bounded retry) + R13 (detached session protection + new-session verify) + R14 (project-root-hashed lockfile). Bug report 의 null-field 주장은 ground-truth 와 모순되어 폐기, 진짜 결함 (session lifecycle GC + duplicate wrapper) 에 집중.
250
+ - **Drivers**: visual feedback 회복, duplicate wrapper 안전, multi-project 병렬 보존.
251
+ - **Alternatives considered**: R14 only (H1 잔존), skip background (API breaking), `--isolated-session` flag (over-engineering).
252
+ - **Consequences**:
253
+ - 기존 single-mission 인터랙티브 영향 없음 (R13 dedicated 이름 retry 는 RLP_BACKGROUND only)
254
+ - duplicate wrapper 시 second-mover 명시 차단, 사용자 명령으로 lockfile 복구 가능
255
+ - 매 검증 시점 최대 5s 추가 (단일 권위 budget: 5×1s polling). 최선 케이스는 0s (첫 시도 alive).
256
+ - 다른 project 동시 runner 는 hash 분리로 그대로 동작
257
+ - **Follow-ups**:
258
+ - tmux pane lifecycle dashboard
259
+ - mission-level pane 격리 옵션 (`--isolated-session`)
260
+ - bug-report contract: 다음번부터 consumer 가 evidence 파일 (실제 session-config.json + tmux ls 출력) 첨부
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-dev-methodologies/rlp-desk",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Fresh-context iterative loops for Claude Code — autonomous task completion with independent verification",
5
5
  "scripts": {
6
6
  "postinstall": "node scripts/postinstall.js",
package/src/governance.md CHANGED
@@ -774,6 +774,19 @@ The base signal vocabulary (`continue | verify | blocked`) is binary at the iter
774
774
 
775
775
  The downgrade is intentionally recoverable: the malformed signal is a worker-side prompt regression, not an environment failure, and the operator can fix it in-place.
776
776
 
777
+ ## 7h. Tmux Session Lifecycle Resilience (US-024/025/026 R12+R13+R14 P0)
778
+
779
+ Multi-mission queue/daemon (`RLP_BACKGROUND=1`) workflows can lose their tmux session between missions — terminal close, manual `tmux kill-session`, or tmux server restart all drop the session and every pane in it. Three independent guards now compose:
780
+
781
+ ### R12 — Pane lifecycle monitor (5s authoritative budget)
782
+ `_verify_pane_alive` and `_verify_session_alive` (lib_ralph_desk.zsh) check `#{pane_dead}` and `tmux has-session`. The runner invokes `_r12_check_lifecycle` at three sites: (1) immediately after `create_session()`, (2) at the top of every iteration, (3) right after worker dispatch and before the wait-loop. The check polls 5 attempts with 1-second sleep (5-second hard budget). On expiry it writes a BLOCKED sentinel with `reason_category=infra_failure`, `recoverable=true`, `suggested_action=restart` and exits 1 — never an infinite loop.
783
+
784
+ ### R13 — Detached session protection (RLP_BACKGROUND only)
785
+ When `tmux new-session -d` collides with an existing session and `RLP_BACKGROUND=1`, the runner appends `-bg-<epoch>-<pid>` to `SESSION_NAME` and runs a `tmux has-session` loop with random 4-digit suffixes until the name is unique. The new session also sets `destroy-unattached off` so the session survives every attached client disconnecting. **Limits**: this option is best-effort; it does NOT survive a manual `tmux kill-session` or a tmux server restart. R12 will detect those events at the next checkpoint.
786
+
787
+ ### R14 — Project-scoped runner lockfile (mkdir atomic)
788
+ `RUNNER_LOCKFILE_PATH` keys on `ROOT_HASH` (`shasum || sha1sum || cksum` of the repo root), so two different projects can run runners in parallel while the same project root is single-runner. `RUNNER_LOCKDIR` (`${RUNNER_LOCKFILE_PATH}.d`) is acquired by `mkdir` for true filesystem-level atomicity — no check-then-write race. Stale pids (no longer responding to `kill -0`) are reaped automatically; live duplicates exit 1 with a recovery hint.
789
+
777
790
  ## 8. Circuit Breaker
778
791
 
779
792
  | Condition | Verdict |