@ai-dev-methodologies/rlp-desk 0.14.0 → 0.14.2

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.
@@ -1,5 +1,227 @@
1
- # Plan — v0.14.0 Recovery: 원래 의도대로 동작 회복 (zsh primary for tmux)
1
+ # Plan — v0.14.1: Codex verifier idle을 frozen으로 오인하는 버그 수정
2
2
 
3
+ > **상위 우선순위 plan. 아래 v0.14.0 / v0.13.0 plan은 history reference로 보존.**
4
+ > **Trigger**: BOS Bug Report #3 (`/Users/kyjin/dev/doul/bos/docs/exec-plans/active/2026-05-04-rlp-desk-bug-report-3-verifier-noprogress.md`). codex verifier가 verdict 작성 + idle UI ("Worked for 5m 36s ──") 표시 시 main polling loop의 byte-stasis 감지가 BLOCKED 판정 → 8 iter (148분) 손실.
5
+ > **Target version**: 0.14.1
6
+ > **Severity**: HIGH (Bug Report 분류).
7
+ > **승인된 strategy**: 작성 시점 미정 — 본 plan에 단일 추천안 명시 후 ExitPlanMode로 승인 요청.
8
+
9
+ ---
10
+
11
+ ## A. Context (v0.14.1)
12
+
13
+ ### 문제 진단
14
+
15
+ v0.14.0 출시 후 production path는 `--mode tmux` → `run_ralph_desk.zsh`. 신규 버그는 zsh runner의 main polling loop에서 발생. codex (gpt-5.5:high) verifier가:
16
+ 1. verdict file (`<slug>-verify-verdict.json`) 작성 완료
17
+ 2. 이후 codex CLI는 다음 입력 대기 idle UI 노출 (예: `─ Worked for 5m 36s ──`, `› Summarize recent commits`, `gpt-5.5 high · Context 66% left`)
18
+ 3. pane content 변화 0 → main loop의 `check_no_progress()` (`PROGRESS_NO_CHANGE_TIMEOUT`=600s) 가 frozen 판정
19
+ 4. BLOCKED `verifier_dead` (sentinel category=infra_failure) 작성 → 캠페인 종료
20
+ 5. **그 시점에 verdict 파일은 이미 디스크에 있었다** → 무효화
21
+
22
+ Node leader (`--mode agent`, alpha)에도 동일 결함 존재: `signal-poller.mjs` + `prompt-dismisser.mjs` 에 codex idle 패턴 부재. agent mode에서 동일 시나리오로 회귀할 위험.
23
+
24
+ ### Explore 결과 (file:line 인용)
25
+
26
+ **zsh runner**:
27
+ - `src/scripts/run_ralph_desk.zsh:1437-1469` — `check_no_progress()`. `PROGRESS_NO_CHANGE_TIMEOUT` (기본 600s) 이상 pane bytes 동일 시 BLOCKED. **verdict 파일 검사 없음**.
28
+ - `src/scripts/run_ralph_desk.zsh:1407-1427` — `check_prompt_stall()`. `_PROMPT_RE`/`_AFFORDANCE_RE` 만 검사. **codex idle UI 패턴 없음**.
29
+ - `src/scripts/run_ralph_desk.zsh:2185, 2194` — main polling loop가 `check_prompt_stall` + `check_no_progress` 호출.
30
+ - `src/scripts/run_ralph_desk.zsh:2269-2306` — codex verifier 전용 폴링: verdict file 존재 + jq valid JSON + 30s grace + `pane_current_command` shell-back 조기 종료. **그러나 위 main loop의 check_no_progress 가 동시에 작동 → 동일 600s에 BLOCKED 발화**.
31
+ - `src/scripts/run_ralph_desk.zsh:272` — `VERDICT_FILE="${MEMOS_DIR}/${SLUG}-verify-verdict.json"`.
32
+ - `src/scripts/run_ralph_desk.zsh:382, 407, 536, 560` — codex working/thinking/Exploring/Running/reading/searching/editing/writing 키워드만 인식. idle UI 미인식.
33
+
34
+ **Node leader**:
35
+ - `src/node/runner/prompt-dismisser.mjs:18-26` — `PROMPT_RE` + `AFFORDANCE_RE` 모두 claude 패턴. codex 특화 0개.
36
+ - `src/node/runner/prompt-detector.mjs:6-11` — claude permission 시그니처 only.
37
+ - `src/node/polling/signal-poller.mjs:118-242` — `pollForSignal(signalFile, { timeoutMs })`. signal file이 없고 pane이 shell로 돌아오면 `WorkerExitedError`. timeout 시 `TimeoutError`. **verdict 파일 자체를 polling 함** — verdict 작성 후 codex idle 시점이라도 file 존재하면 즉시 반환. 즉 Bug #3는 Node 측에서는 발생 빈도가 낮으나, 여전히 timeout 600s 안에 read 못하면 (예: codex가 verdict를 매우 늦게 atomic-write) 회귀 가능.
38
+ - `src/node/runner/campaign-main-loop.mjs:1449-1468` — verifier 폴링 호출.
39
+ - `src/node/runner/campaign-main-loop.mjs:471-501` — `BLOCK_TAGS`: `VERIFIER_TIMEOUT`, `VERIFIER_EXITED`, `PROMPT_BLOCKED`, `PERMISSION_PROMPT`.
40
+
41
+ ### 핵심 결정 (추천안)
42
+
43
+ **zsh runner와 Node leader 양쪽에 "verdict file 우선 검사 + codex idle UI 인식" 이중 방어 추가**. Bug Report Fix-A + Fix-B를 두 경로에 적용. Fix-C(`--verifier-noprogress-timeout` 옵션 분리)는 보류 — Fix-A 가 효력을 보이면 불필요.
44
+
45
+ **근거**:
46
+ - v0.14.0 production path(zsh)에서 즉시 효과. BOS 캠페인 즉시 회복.
47
+ - agent mode(Node, alpha)에도 같은 회귀 위험이 있으므로 동시 적용으로 일관된 contract.
48
+ - 1줄 추가(verdict file 존재 검사)는 risk가 매우 낮고, codex idle 패턴 추가는 기존 패턴 정규식에 alternation 추가로 끝.
49
+ - workaround W1(`--verifier-model sonnet`) 은 BOS 권고이지만 codex consensus 가치를 깎으므로 fix 가 우선.
50
+
51
+ ---
52
+
53
+ ## B. Approach (5 Phases)
54
+
55
+ ### Phase 1 — zsh: verdict-aware no-progress + codex idle 인식 (Day 1, 2시간)
56
+
57
+ **파일**: `src/scripts/run_ralph_desk.zsh`
58
+
59
+ 1. **`check_no_progress()` (L1437-1469)** 진입부에 verdict-aware short-circuit 추가:
60
+ ```zsh
61
+ check_no_progress() {
62
+ # v0.14.1 Fix-A: codex verifier가 verdict 작성 후 idle UI 노출 시 byte-stasis가
63
+ # frozen으로 오인됨. main verdict 파일이 이미 valid JSON이면 verifier는 done이며
64
+ # main loop 다음 phase(verdict 수확)가 처리해야 한다 — frozen으로 분류 X.
65
+ if [[ "${PHASE:-}" == "verifier" || "${PHASE:-}" == "final_verifier" ]] \
66
+ && [[ -f "$VERDICT_FILE" ]] \
67
+ && jq -e . "$VERDICT_FILE" >/dev/null 2>&1; then
68
+ return 0 # verdict already written; let polling loop harvest
69
+ fi
70
+ # ... 기존 byte-stasis 로직 유지 ...
71
+ }
72
+ ```
73
+ - `PHASE` 변수 노출이 미흡하면, current pane id 가 `$VERIFIER_PANE` 인지로 분기.
74
+ - consensus 모드에서는 `${SLUG}-verify-verdict-claude.json` / `${SLUG}-verify-verdict-codex.json` 도 OR 검사.
75
+
76
+ 2. **`check_prompt_stall()` (L1407-1427)** 의 `_PROMPT_RE` / `_AFFORDANCE_RE` 에 codex idle 패턴 추가하지 **않는다** — 이건 prompt가 아니라 idle 상태. 대신 신규 helper `is_codex_idle_ui()` 추가:
77
+ ```zsh
78
+ is_codex_idle_ui() {
79
+ local pane_text="$1"
80
+ # codex post-work idle: "─ Worked for Xm Ys ──", "› " prefix, "Context X% left"
81
+ echo "$pane_text" | grep -qE '─ Worked for [0-9]+m [0-9]+s ─' \
82
+ || echo "$pane_text" | grep -qE 'Context [0-9]+% left'
83
+ }
84
+ ```
85
+ `check_no_progress()` 의 byte-stasis 단계에서 verdict file이 부재해도 codex idle UI 가 감지되면 추가 grace 기간(예: +120s) 부여. timeout 사용자에게 명확하도록 stderr 노티스 1회.
86
+
87
+ 3. **codex verifier 폴링 (L2269-2306)** 의 grace period(현재 30s) 는 그대로 유지.
88
+
89
+ ### Phase 2 — Node leader: signal-poller + prompt-dismisser 보강 (Day 1, 3시간)
90
+
91
+ **파일**: `src/node/runner/prompt-dismisser.mjs`, `src/node/polling/signal-poller.mjs`, `src/node/runner/prompt-detector.mjs`
92
+
93
+ 1. **`prompt-dismisser.mjs`** 에 codex idle 인식용 정규식 분리 (기존 PROMPT_RE 와는 다른 카테고리):
94
+ ```js
95
+ // v0.14.1: codex post-work idle UI markers. Not a permission prompt — work
96
+ // is done; the CLI is just waiting for next user input. Emitting these as
97
+ // "prompt blocked" would be wrong — the right response is to let the
98
+ // verifier-side polling harvest the already-written verdict file.
99
+ export const CODEX_IDLE_RE = /─\s*Worked for \d+m \d+s\s*─|Context \d+% left/;
100
+ export function isCodexIdleUi(paneText) { return CODEX_IDLE_RE.test(paneText); }
101
+ ```
102
+
103
+ 2. **`signal-poller.mjs`** (L210-226 의 pane-shell-back 분기 직전):
104
+ ```js
105
+ // v0.14.1 Fix-A: re-read signal file once more before declaring exit.
106
+ // Codex may write verdict + return to idle UI almost simultaneously; if
107
+ // the verdict landed on disk after our last readFile, we must not
108
+ // misclassify the idle UI as WorkerExited.
109
+ try {
110
+ const raw = await readFile(signalFile, 'utf8');
111
+ const parsed = JSON.parse(raw);
112
+ return parsed;
113
+ } catch { /* still missing — fall through to existing exit logic */ }
114
+ ```
115
+ 현재 코드 패스가 이미 readFile loop을 하므로 차이가 작을 수 있음 — 정확한 위치는 구현 시점에 결정.
116
+
117
+ 3. **timeout 직전(L deadline 비교)** 에 verdict file 마지막 점검 추가 (Bug Report Fix-A 의 Node 버전):
118
+ ```js
119
+ if (Date.now() >= deadline) {
120
+ try {
121
+ const last = await readFile(signalFile, 'utf8');
122
+ return JSON.parse(last);
123
+ } catch {}
124
+ throw new TimeoutError(`Timed out waiting for valid JSON signal at ${signalFile}`);
125
+ }
126
+ ```
127
+
128
+ 4. **`prompt-detector.mjs`** 는 권한 프롬프트 전용이므로 codex idle 추가하지 않음. 대신 `prompt-dismisser` 의 `isCodexIdleUi()` 를 signal-poller가 임포트해서, codex mode 일 때 idle 감지 시 deadline 연장 (예: 마지막 byte-change 가 600s 전이라도 idle UI 보이면 추가 120s grace).
129
+
130
+ ### Phase 3 — 테스트 (Day 1, 3시간)
131
+
132
+ **신규 / 갱신**:
133
+ - `tests/node/test-prompt-dismisser.mjs` (신규 또는 기존 보강): `CODEX_IDLE_RE` / `isCodexIdleUi()` 단위 테스트. 실제 BOS 캡처 텍스트 fixture로 verify (≥3 케이스).
134
+ - `tests/node/test-signal-poller.mjs` (us003 보강): "verdict written between polls + codex idle UI" 시나리오 — `readFile` mock이 첫 호출 ENOENT, 두 번째 호출 valid JSON 반환. timeout 직전 last-read 가 verdict 회수하는지 검증.
135
+ - `tests/node/us003-signal-poller.test.mjs` 의 기존 flake 테스트는 손대지 않음(타이밍 flake — 별도 이슈).
136
+ - zsh 측: `tests/test_us0XX_codex_idle_no_progress.sh` 신규. mock pane 텍스트 + verdict file 시뮬레이션으로 `check_no_progress()` 가 `PHASE=verifier && verdict exists` 일 때 BLOCKED 발화 안 함을 zsh 단위 테스트로 검증.
137
+
138
+ ### Phase 4 — SV gate 갱신 (Day 1, 1시간)
139
+
140
+ **파일**: `tests/sv-self-verify-0.14.sh` 보강 또는 신규 `tests/sv-self-verify-0.14.1.sh`.
141
+ 신규 시나리오:
142
+ - L6.1 (CRITICAL) verdict-aware no-progress: `PHASE=verifier` + verdict 파일 존재 시 `check_no_progress` 가 BLOCKED 안 함.
143
+ - L6.2 (CRITICAL) Node `signal-poller` last-chance verdict read: deadline 직전 verdict 작성된 경우 timeout 대신 verdict 반환.
144
+ - L6.3 (MEDIUM) `isCodexIdleUi()` 단위 테스트 PASS.
145
+ - L6.4 (MEDIUM) BOS-shape 캡처 텍스트("Worked for 5m 36s ──", "Context 66% left") 가 idle 로 인식.
146
+ - L6.5 (LOW) v0.14.0 contract 회귀 가드: `--mode tmux` 가 여전히 zsh subprocess로 위임 (us008 happy 재실행).
147
+
148
+ ### Phase 5 — Ship (Day 2, CLAUDE.md mandate)
149
+
150
+ 1. self-verification gate 100% PASS.
151
+ 2. ralplan + codex review (governance.md 변경 없으면 ralplan 생략 가능, 단 governance docs에 codex idle 인식 정책 1단락 추가 시 mandatory).
152
+ 3. version bump 0.14.1.
153
+ 4. CHANGELOG: "Fix codex verifier idle UI being mis-classified as no-progress; verdict-aware short-circuit in zsh runner; symmetric guard in Node signal-poller (agent mode)."
154
+ 5. commit + push + gh release + npm publish (각 단계 사용자 승인 필수, CLAUDE.md `Commit & Publish Gate`).
155
+ 6. local sync banner-aware verify.
156
+
157
+ ---
158
+
159
+ ## C. v0.14.0 / v0.13.x 에서 보존되는 것
160
+
161
+ - v0.14.0 routing contract: `--mode tmux` → zsh subprocess. 변경 없음.
162
+ - v0.13.0 path migration (`.rlp-desk/`).
163
+ - v0.13.0 prompt-detector + signal-poller permission_prompt 감지.
164
+ - v0.13.1 detached vs attached tmux 분기.
165
+ - agent-mode alpha 라벨링.
166
+
167
+ ---
168
+
169
+ ## D. Critical Files
170
+
171
+ ```
172
+ src/scripts/run_ralph_desk.zsh # Phase 1 — verdict-aware check_no_progress + is_codex_idle_ui()
173
+ src/node/runner/prompt-dismisser.mjs # Phase 2 — CODEX_IDLE_RE + isCodexIdleUi()
174
+ src/node/polling/signal-poller.mjs # Phase 2 — last-chance verdict read on deadline + idle-UI grace
175
+ src/node/runner/prompt-detector.mjs # Phase 2 — 변경 없음 (확인용)
176
+ src/node/runner/campaign-main-loop.mjs # 변경 거의 없음 — pollForSignal 호출은 그대로
177
+ tests/node/test-prompt-dismisser.mjs # 신규/보강
178
+ tests/node/test-signal-poller.mjs # 보강
179
+ tests/test_us0XX_codex_idle_no_progress.sh # 신규 (zsh 단위)
180
+ tests/sv-self-verify-0.14.1.sh 또는 0.14.sh 보강 # Phase 4
181
+ package.json # Phase 5 — 0.14.1
182
+ docs/plans/spicy-booping-galaxy.md # 본 파일 (최종 plan 기록)
183
+ ```
184
+
185
+ Verdict file paths (참조 only, 변경 없음):
186
+ - zsh: `${MEMOS_DIR}/${SLUG}-verify-verdict.json` (run_ralph_desk.zsh:272)
187
+ - Node: `paths.verdictFile = <deskRoot>/memos/<slug>-verify-verdict.json` (campaign-main-loop.mjs:78)
188
+ - Consensus: `<slug>-verify-verdict-claude.json`, `<slug>-verify-verdict-codex.json`
189
+
190
+ ---
191
+
192
+ ## E. Verification (E2E)
193
+
194
+ 1. **BOS 회귀 (CRITICAL)**: BOS Phase 1 캠페인을 `--worker-model sonnet --verifier-model gpt-5.5:high --consensus final-only` 로 재실행. US-003 verifier idle UI 시점에 BLOCKED 발화 안 함, verdict 정상 회수 + 다음 iteration 진입 확인.
195
+ 2. **Local sync**: `node scripts/postinstall.js` 후 banner-aware diff 0 mismatches.
196
+ 3. **Backward compat**:
197
+ - claude-only verifier (`--verifier-model opus`): 회귀 없음 — verdict-aware short-circuit 도 trip 안 함 (claude는 idle 시점 자체가 다름).
198
+ - `--mode agent`: 동일 fix 적용으로 회귀 없음.
199
+ - 진짜 frozen worker (verdict 부재 + pane 600s 변화 없음): 기존대로 BLOCKED 정상 발화.
200
+ 4. **Inspection 세션 정리**: 사용자 측 `tmux session doul-bos-583` (worker pane `%1681` node 잔존) 은 이번 fix 이후 재실행 결정 시점에 사용자가 직접 정리 (`/rlp-desk clean <slug> --kill-session`).
201
+
202
+ ---
203
+
204
+ ## F. 기각된 대안
205
+
206
+ - **Fix-C (`--verifier-noprogress-timeout` 옵션 분리)**: 사용자 노출 surface 증가. Fix-A 가 효력을 보이면 불필요. v0.15.0+ 에 별도 백로그.
207
+ - **Workaround W1 영구화 (claude 만 verifier 허용)**: codex consensus 가치 손실. 부정.
208
+ - **agent mode 만 fix, tmux 보류**: production path 가 zsh이므로 BOS 회복 안 됨. 부정.
209
+ - **prompt-detector에 codex idle 추가**: detector 는 permission prompt 전용 — 의미적으로 어긋남. dismisser쪽 분리 함수가 정확.
210
+
211
+ ---
212
+
213
+ ## G. Risk Notes
214
+
215
+ - `PHASE` 변수가 zsh runner 전반에 노출되어 있는지 확인 필요(미노출 시 verifier pane id로 분기 변경).
216
+ - consensus 모드 시 두 verdict 파일 OR 검사로 단일 verifier 작성된 시점에 short-circuit 안 됨 — 이 부분은 Phase 1 구현 시 명시 검증.
217
+ - `jq -e .` 검사가 codex가 partial-write 중인 verdict (파일 생성 후 atomic mv 전)에 false-positive 안 발생하는지 확인 — `atomic_write` 가 이미 mv 사용이라 안전.
218
+ - Node 측 last-chance read 가 race condition (deadline 전 다른 thread가 file 작성) 에서도 동작하는지 확인 — fs.readFile 은 atomic 이므로 OK.
219
+
220
+ ---
221
+
222
+ # v0.14.0 (HISTORY) — Restore zsh as primary tmux runner
223
+
224
+ > **Status**: SHIPPED 2026-05-03. v0.14.0 npm + GitHub release 게시 완료.
3
225
  > **상위 우선순위 plan. 아래 v0.13.0 plan은 history reference로 보존.**
4
226
  > **Trigger**: 사용자 평가 — "rlp-desk가 못 쓸 폐급, 통제 불가능 수준". v0.13.0/v0.13.1 fix는 빙산 일각.
5
227
  > **Target version**: 0.14.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-dev-methodologies/rlp-desk",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
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",
@@ -127,6 +127,11 @@ export async function pollForSignal(
127
127
  capturePane = defaultCapturePane,
128
128
  sendKeys = defaultSendKeys,
129
129
  log = () => {},
130
+ // v0.14.2 Bug Report #4 Fix-D: optional legacy fallback path checked
131
+ // only after the canonical signalFile last-chance read fails. Caller
132
+ // (campaign-main-loop) supplies the pre-v0.13.0 .claude/ralph-desk
133
+ // memos path; signal-poller stays read-only and never migrates.
134
+ legacySignalFile = null,
130
135
  } = {},
131
136
  ) {
132
137
  const deadline = Date.now() + timeoutMs;
@@ -238,5 +243,35 @@ export async function pollForSignal(
238
243
  await delay(pollIntervalMs);
239
244
  }
240
245
 
246
+ // v0.14.1: last-chance verdict read before declaring timeout. Codex CLI
247
+ // can finish work + atomic-mv the verdict + return to its idle UI all
248
+ // within a single poll interval; if our previous readFile happened to
249
+ // race with the rename, we would have seen ENOENT/SyntaxError. Try once
250
+ // more synchronously before throwing — the file is now either present
251
+ // and parseable (success) or genuinely missing (real timeout).
252
+ // Bug Report #3 (BOS 2026-05-04).
253
+ try {
254
+ const rawContent = await readFile(signalFile);
255
+ return JSON.parse(rawContent);
256
+ } catch {
257
+ // fall through
258
+ }
259
+
260
+ // v0.14.2 Bug Report #4 Fix-D: codex sometimes lands the verdict at the
261
+ // pre-v0.13.0 legacy path (.claude/ralph-desk/memos/...) instead of the
262
+ // canonical .rlp-desk/memos/. If the caller passed `legacySignalFile`,
263
+ // try that path before declaring timeout — same semantics as the
264
+ // canonical last-chance read. campaign-main-loop is responsible for
265
+ // migrating the file into the canonical location after observing it;
266
+ // signal-poller stays read-only.
267
+ if (legacySignalFile) {
268
+ try {
269
+ const rawContent = await readFile(legacySignalFile);
270
+ return JSON.parse(rawContent);
271
+ } catch {
272
+ // fall through to TimeoutError
273
+ }
274
+ }
275
+
241
276
  throw new TimeoutError(`Timed out waiting for valid JSON signal at ${signalFile}`);
242
277
  }
@@ -180,6 +180,11 @@ export async function assembleVerifierPrompt({
180
180
  verifiedUs = [],
181
181
  autonomousMode = false,
182
182
  conflictLogPath = '',
183
+ // v0.14.2 Bug Report #4 Fix-E: when supplied, the assembled prompt
184
+ // ends with a strong "MUST write verdict to <absolute_path>" rule so
185
+ // codex (which sometimes infers the legacy .claude/ralph-desk path
186
+ // from CWD) lands the verdict where the leader is polling.
187
+ verdictWritePath = '',
183
188
  } = {}) {
184
189
  const basePrompt = await readRequiredFile(promptBase, 'Verifier prompt base file');
185
190
  const promptLines = [
@@ -209,5 +214,19 @@ export async function assembleVerifierPrompt({
209
214
  appendAutonomousModeSection(promptLines, { conflictLogPath, verifier: true });
210
215
  }
211
216
 
217
+ if (verdictWritePath) {
218
+ promptLines.push('');
219
+ promptLines.push('---');
220
+ promptLines.push('## CRITICAL: Verdict file write path (v0.14.2)');
221
+ promptLines.push('');
222
+ promptLines.push('Write `verify-verdict.json` ONLY to this absolute path:');
223
+ promptLines.push('');
224
+ promptLines.push(` ${verdictWritePath}`);
225
+ promptLines.push('');
226
+ promptLines.push('DO NOT write to `.claude/ralph-desk/memos/` — that path is deprecated since');
227
+ promptLines.push('v0.13.0. The leader polls only the absolute path above; writing elsewhere');
228
+ promptLines.push('triggers BLOCKED `verifier_dead` even though your verdict is correct.');
229
+ }
230
+
212
231
  return `${promptLines.join('\n')}\n`;
213
232
  }
@@ -43,6 +43,31 @@ const MODEL_UPGRADES = {
43
43
  'gpt-5.3-codex-spark:xhigh': 'BLOCKED',
44
44
  };
45
45
 
46
+ // v0.14.2 Bug Report #4 Fix-D: codex occasionally lands the verdict at the
47
+ // pre-v0.13.0 `.claude/ralph-desk/memos/` path despite prompt instructions.
48
+ // signal-poller's `legacySignalFile` last-chance branch returns the parsed
49
+ // verdict in memory; these two helpers move the file into the canonical
50
+ // .rlp-desk/memos/ location AFTER the polling loop succeeds, so analytics
51
+ // archival + sentinel hygiene remain consistent.
52
+ export async function _verdictMigrationNeeded(paths) {
53
+ if (!paths?.legacyVerdictFile || !paths?.verdictFile) return false;
54
+ // Migration is only meaningful when the legacy file exists AND the
55
+ // canonical file does not. If both exist, canonical wins (already
56
+ // observed) and we leave legacy alone.
57
+ let legacyExists = false;
58
+ let canonicalExists = false;
59
+ try { legacyExists = await exists(paths.legacyVerdictFile); } catch {}
60
+ try { canonicalExists = await exists(paths.verdictFile); } catch {}
61
+ return legacyExists && !canonicalExists;
62
+ }
63
+
64
+ export async function _migrateLegacyVerdict(paths) {
65
+ if (!paths?.legacyVerdictFile || !paths?.verdictFile) return false;
66
+ await fs.mkdir(path.dirname(paths.verdictFile), { recursive: true });
67
+ await fs.rename(paths.legacyVerdictFile, paths.verdictFile);
68
+ return true;
69
+ }
70
+
46
71
  // v0.13.0: legacy .claude/ralph-desk/ guidance for run mode (no auto-mv).
47
72
  export function detectLegacyDeskInRunMode(rootDir, env = process.env) {
48
73
  const legacyPath = path.join(rootDir, LEGACY_DESK_REL);
@@ -76,6 +101,12 @@ function buildPaths(rootDir, slug, env = process.env) {
76
101
  doneClaimFile: path.join(deskRoot, 'memos', `${slug}-done-claim.json`),
77
102
  signalFile: path.join(deskRoot, 'memos', `${slug}-iter-signal.json`),
78
103
  verdictFile: path.join(deskRoot, 'memos', `${slug}-verify-verdict.json`),
104
+ // v0.14.2 Bug Report #4 Fix-D: codex sometimes lands the verdict at the
105
+ // pre-v0.13.0 legacy path. We track the absolute legacy location so the
106
+ // signal-poller last-chance read can fall back to it before declaring
107
+ // timeout. Always rooted at <project>/.claude/ralph-desk/memos/, even
108
+ // when RLP_DESK_RUNTIME_DIR overrides the canonical deskRoot.
109
+ legacyVerdictFile: path.join(rootDir, '.claude', 'ralph-desk', 'memos', `${slug}-verify-verdict.json`),
79
110
  blockedSentinel: path.join(deskRoot, 'memos', `${slug}-blocked.md`),
80
111
  completeSentinel: path.join(deskRoot, 'memos', `${slug}-complete.md`),
81
112
  contextFile: path.join(deskRoot, 'context', `${slug}-latest.md`),
@@ -376,6 +407,11 @@ async function dispatchVerifier({
376
407
  verifyMode: 'per-us',
377
408
  usId,
378
409
  verifiedUs: state.verified_us,
410
+ // v0.14.2 Fix-E: hand the absolute canonical verdict path to the
411
+ // verifier prompt. assembleVerifierPrompt appends a "CRITICAL: write
412
+ // verdict to <path>" footer so codex does not infer the legacy
413
+ // .claude/ralph-desk/memos/ location from CWD.
414
+ verdictWritePath: paths.verdictFile,
379
415
  });
380
416
  const fileName = suffix
381
417
  ? `${suffix}.verifier-prompt.md`
@@ -1452,7 +1488,21 @@ async function _runCampaignBody(slug, options, paths, rootDir) {
1452
1488
  mode: parseModelFlag(verifierModel, 'verifier').engine,
1453
1489
  paneId: state.verifier_pane_id,
1454
1490
  timeoutMs: iterTimeoutMs,
1491
+ // v0.14.2 Fix-D: codex sometimes writes the verdict at the legacy
1492
+ // .claude/ralph-desk/memos/ path. signal-poller's last-chance read
1493
+ // tries this fallback before timing out.
1494
+ legacySignalFile: paths.legacyVerdictFile,
1455
1495
  });
1496
+ // v0.14.2 Fix-D continued: if the verdict came from the legacy path,
1497
+ // migrate it into the canonical location so the rest of the pipeline
1498
+ // (analytics archival, sentinels, status) sees a single canonical
1499
+ // file. Best-effort — any rename failure is logged but does not
1500
+ // re-throw because we already have the parsed verdict in memory.
1501
+ if (await _verdictMigrationNeeded(paths)) {
1502
+ await _migrateLegacyVerdict(paths).catch((migrateErr) => {
1503
+ console.error('[v0.14.2] legacy verdict migration failed:', migrateErr?.message ?? migrateErr);
1504
+ });
1505
+ }
1456
1506
  validateArtifact(verdict, {
1457
1507
  expectedSlug: slug,
1458
1508
  iterationFloor: state.iteration,
@@ -35,6 +35,27 @@ const DEFAULT_NO_RE = /\[y\/N\]|\(yes\/no,\s*default\s+no\)|[Dd]efault[: ]+[Nn]o
35
35
  // output that may legitimately contain "(y/n)"-shaped substrings.
36
36
  const ACTIVE_TASK_RE =
37
37
  /esc to interrupt|background terminal running|^\s*[·✻]\s+[A-Za-z]+(\.{3}|…)/m;
38
+
39
+ // v0.14.1 / v0.14.2: codex post-work idle UI markers. NOT a permission prompt
40
+ // — the codex CLI has finished its task and is waiting for the next user
41
+ // input. Sources: BOS Bug Report #3 (2026-05-04) + #4 (2026-05-05).
42
+ // Treat this as "task done, idle awaiting input"; callers should harvest
43
+ // the verdict file rather than escalate as `prompt_blocked`.
44
+ //
45
+ // v0.14.2 relaxation (Bug #4): the v0.14.1 strict "─ Worked for Xm Ys ─"
46
+ // regex required the surrounding horizontal rule to match. tmux capture
47
+ // truncation occasionally dropped those rules so the pattern missed in
48
+ // production. Match on multiple independent markers; ANY one is enough.
49
+ // 1. "Worked for Xm Ys" — duration line, codex-only
50
+ // 2. "Context X%left" (no space) — status bar; tolerate wrap removal
51
+ // 3. "gpt-X.Y reasoning · branch" — codex idle status line
52
+ // 4. codex default suggestions — only printed at the idle prompt
53
+ export const CODEX_IDLE_RE =
54
+ /Worked for \d+m \d+s|Context \d+%\s*left|gpt-\d+(\.\d+)? (low|medium|high|xhigh) ·|Improve documentation in @|Summarize recent commits|Explain (this )?code/;
55
+ export function isCodexIdleUi(paneText) {
56
+ if (typeof paneText !== 'string' || paneText.length === 0) return false;
57
+ return CODEX_IDLE_RE.test(paneText);
58
+ }
38
59
  const DEBOUNCE_MS = 3000;
39
60
 
40
61
  const lastApprovalAt = new Map();