@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.
|
|
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.
|
|
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();
|