fix(lib): handle agent_not_ready startup state and reject claude fullscreen upsell modal
- Accept agent_not_ready from herdr agent start to allow startup dialog handling without premature rollback, while preserving fail-closed behavior on dead process timeouts. - Match Claude fullscreen renderer upsell modal via 'Yes, try it' and dismiss with Escape to avoid dropping permission flags or deadlocking on idle /tui tips. - Add behavioral test suite in test_b19_headless_reconcile_fixes.py and cross-agent review reports.
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
# 교차 코드 리뷰 리포트 — Job c666854d (rev.2)
|
||||
|
||||
- **대상**: `.agents/skills/lib.sh`, `tests/test_b19_headless_reconcile_fixes.py`, `FIX.md`
|
||||
- **리뷰어**: claude (`planner-reviewer-claude-01`)
|
||||
- **선행 리뷰**: Job b9e42784 — `[VERDICT: NOT PASS]` (D-1 ~ D-5)
|
||||
- **관점**: 린트 / 동작성 / 유실
|
||||
- **결론**: 선행 리뷰의 지적 5건이 **모두 정확히 해소**되었고, 이번에는 **실제 Claude Code 클라이언트로 종단 검증**까지 마쳤다.
|
||||
잔여 지적은 전부 경미(Low)하며 병합을 막지 않는다.
|
||||
|
||||
작업 트리 diff는 브리프에 첨부된 diff와 **완전히 일치**한다 (`lib.sh` 16줄, 테스트 106줄, `FIX.md` 신규).
|
||||
|
||||
---
|
||||
|
||||
## 0. 검증 방법
|
||||
|
||||
이번 리뷰의 핵심은 **실물 검증**이다. 선행 리뷰에서는 합성 화면으로만 확인했으나, 이번에는
|
||||
`CLAUDE_CODE_FORCE_FULLSCREEN_UPSELL=1`로 **실제 업셀 모달을 강제 재현**하여 확인했다.
|
||||
|
||||
| # | 방법 |
|
||||
|---|---|
|
||||
| V-1 | 실제 herdr 0.8.2 출력값으로 `agent start` 분류 로직 재현 |
|
||||
| V-2 | herdr 워크스페이스에 **Claude Code v2.1.247 실기동** → 모달 강제 표시 → Escape 전송 → 상태 측정 |
|
||||
| V-3 | `lib.sh`를 그대로 source 하여 실제 함수 실행 (HEAD 대비 비교) |
|
||||
| V-4 | **거부되었던 rev.1 구현을 격리 worktree에 복원**하고 신규 테스트를 돌려 회귀 검출력 확인 |
|
||||
| V-5 | pytest 전체 |
|
||||
|
||||
프로브 워크스페이스(`w1H`/`w1J`/`w1K`/`w1M`/`w1N`)와 임시 worktree는 **전부 정리 완료**.
|
||||
현재 남은 워크스페이스는 실사용 `w1E` 하나뿐이며, `git worktree list`도 1개(본체)로 복귀했다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 선행 지적 해소 확인
|
||||
|
||||
### D-1 (🔴 → ✅) — 죽은 프로세스가 성공으로 승격되던 문제
|
||||
|
||||
성공 정규식이 `agent_started|agent_not_ready`로 축소되었다.
|
||||
선행 리뷰에서 **실제 herdr 프로브로 채집한 출력값**을 그대로 넣어 분류를 재현한 결과:
|
||||
|
||||
| herdr 실제 출력 | 분류 결과 |
|
||||
|---|---|
|
||||
| `{"error":{"code":"timeout","message":"timed out waiting for agent startup"}}` (← `/bin/false`, **죽은 프로세스**) | `retry → exit 1` ✅ |
|
||||
| `{"error":{"code":"agent_pane_busy", …}}` | `retry → exit 1` ✅ |
|
||||
| `{"error":{"code":"agent_not_ready", …}}` (프로세스 생존, 다이얼로그 차단) | `success=1` ✅ |
|
||||
| `agent_started` | `success=1` ✅ |
|
||||
|
||||
Fail-Closed 복원 확인. 치명 오류 우선 분류 순서도 유지되었다.
|
||||
|
||||
또한 이번 실기동에서 herdr가 **정확히 그 상태를 반환하는 것을 실물로 확인**했다:
|
||||
|
||||
```
|
||||
{"error":{"code":"agent_not_ready","message":"agent probe-fs5 is blocked during startup and is not ready for prompts"}}
|
||||
```
|
||||
|
||||
→ `agent_not_ready`를 롤백 사유로 보지 않는 처리가 **가정이 아니라 실측으로** 정당화되었다.
|
||||
|
||||
부수 확인: 워크스페이스 생성 직후 즉시 `agent start` 하면 `agent_pane_busy`가 실제로 발생한다(2회 재현).
|
||||
`lib.sh`의 3회 백오프(0.5/1/2초) 재시도가 이 창구를 정확히 덮으므로 **재시도 루프는 유지되어야 한다**.
|
||||
|
||||
### D-2 / D-2c (🔴 → ✅) — idle 팁을 차단형 다이얼로그로 오인하던 문제
|
||||
|
||||
광의 토큰 `fullscreen renderer|Try the new fullscreen`이 제거되고 모달 고유 문자열만 사용한다.
|
||||
`lib.sh`를 실제 source 하여 **정상 기동(팁만 표시, TUI 준비 완료)** 화면을 넣은 결과:
|
||||
|
||||
| 팁만 있는 정상 화면 | HEAD | rev.1 (거부됨) | **rev.2 (현재)** |
|
||||
|---|---|---|---|
|
||||
| `_pane_dialog_open` | false | 🔴 TRUE | ✅ **false** |
|
||||
| `handle_startup_dialogs` 전송 키 | 0 | 🔴 Enter 20회 | ✅ **0회** |
|
||||
| `wait_for_tui_ready` | rc=0 | 🔴 rc=1 (+Enter 30회) | ✅ **rc=0** |
|
||||
|
||||
정상 경로가 HEAD와 **완전히 동일**하게 복귀했다. 데드락 해소 확인.
|
||||
|
||||
### D-3 (🟠 → ✅) — Enter가 업셀을 "수락"하던 문제
|
||||
|
||||
**실제 모달을 강제 재현해 캡처했다.** 모달 하단 안내가 결정적이다:
|
||||
|
||||
```
|
||||
Try the new fullscreen renderer?
|
||||
|
||||
· Flicker-free output
|
||||
· Mouse support — click to move your cursor or expand results
|
||||
· Selected text auto-copies to your clipboard
|
||||
|
||||
❯ 1. Yes, try it
|
||||
2. Not now
|
||||
|
||||
Enter to confirm · Esc to cancel
|
||||
```
|
||||
|
||||
- `Enter to confirm` → 기본 선택지 `Yes, try it` 수락. 선행 리뷰의 D-3 지적이 **모달 자체 문구로 확증**되었다.
|
||||
- `Esc to cancel` → 수정이 택한 Escape가 **모달이 스스로 안내하는 취소 키**다.
|
||||
|
||||
Escape 전송 후 실측:
|
||||
|
||||
| 항목 | 결과 |
|
||||
|---|---|
|
||||
| 모달 제거 | ✅ 사라짐 (`Yes, try it` 0건) |
|
||||
| 잔여 다이얼로그 토큰 | ✅ 0건 → `send_keys_safe` 차단 해제 |
|
||||
| ready 토큰 가시성 | ✅ 2건 (`Claude Code v2.1.247`, `Sonnet 5 with high effort`) → `wait_for_tui_ready` 통과 |
|
||||
| **`⏵⏵ bypass permissions on` 유지** | ✅ **세션 재시작 없음 — `--dangerously-skip-permissions` 보존** |
|
||||
|
||||
마지막 항목이 중요하다. 우려했던 "수락 시 permission flag 없이 재시작" 경로를 **Escape가 회피함을 실물로 확인**했다.
|
||||
|
||||
### D-4 (🟠 → ✅) — 테스트가 소스 문자열만 확인하던 문제
|
||||
|
||||
신규 4건 중 3건이 `_pane_capture`를 stub 하고 **함수를 실제 실행**하는 행위 테스트로 바뀌었다.
|
||||
(`_pane_tail` → `_pane_capture` → `_sks_herdr` 체인이므로 `_pane_capture` stub은 올바른 주입 지점이다.)
|
||||
|
||||
회귀 검출력을 직접 측정했다. **거부되었던 rev.1 구현을 격리 worktree에 복원**하고 신규 테스트를 실행:
|
||||
|
||||
```
|
||||
FAILED test_agent_start_success_tokens_exclude_startup_timeout
|
||||
FAILED test_fullscreen_tip_is_not_a_blocking_dialog
|
||||
FAILED test_fullscreen_modal_is_rejected_not_accepted
|
||||
FAILED test_wait_for_tui_ready_succeeds_on_fullscreen_tip
|
||||
4 failed
|
||||
```
|
||||
|
||||
→ **4건 전부 rev.1에서 실패하고 rev.2에서 통과**한다. 실질적 회귀 방지력이 확인되었다.
|
||||
`test_wait_for_tui_ready_succeeds_on_fullscreen_tip` 실패 로그에는 rev.1의 `Enter` 30회 주입과
|
||||
`⚠️ TUI readiness check timed out`이 그대로 찍혔다 — 정확히 선행 리뷰가 지적한 증상이다.
|
||||
|
||||
### D-5 (🔵 → 대부분 해소)
|
||||
|
||||
`FIX.md`가 재작성되어 순서 변경·타임아웃 토큰 배제 근거·팁/모달 구분이 모두 기술되었고, 말미 개행도 정상이다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 테스트 / 린트 결과
|
||||
|
||||
- `tests/` 전체 **397 passed** (8분 30초). HEAD 393 + 신규 4건과 정확히 일치.
|
||||
- 변경 파일 단독 **10 passed** (신규 4건 포함).
|
||||
- `bash -n .agents/skills/lib.sh` **통과**. `shellcheck`는 이 환경에 미설치라 미실행.
|
||||
|
||||
---
|
||||
|
||||
## 3. 잔여 지적 (전부 Low — 병합 차단 아님)
|
||||
|
||||
### N-1 `_MAM_DIALOG_TOKENS`의 `|Yes, try it`은 **불필요하며** 오탐 면적만 넓힌다
|
||||
|
||||
실제 모달 문구에는 `Esc to cancel`이 포함되어 있고, 이 토큰은 **HEAD의 기존 토큰 목록에 이미 존재**한다.
|
||||
HEAD 토큰만으로 실제 모달이 매칭되는 것을 확인했다 → **탐지 목적으로는 추가가 중복**이다.
|
||||
(선행 리뷰의 합성 픽스처에는 이 하단 안내줄이 없어 드러나지 않았던 부분이다.)
|
||||
|
||||
반면 `Yes, try it`은 평문 대화에 등장할 수 있는 자연어다. 실제로 아래 한 줄이 `_pane_dialog_open`을 참으로 만든다:
|
||||
|
||||
```
|
||||
⏺ Sure — if the build fails again, Yes, try it with the --clean flag.
|
||||
```
|
||||
|
||||
→ `send_keys_safe`가 30초 대기 후 `rc=2`로 실패한다. 확률은 낮고, HEAD에도 `Allow this` / `No, exit` 같은
|
||||
평문형 토큰 선례가 있어 **새로운 부류의 위험은 아니다.** 다만 이 건은 얻는 것이 없으므로 제거를 권한다.
|
||||
|
||||
- **권고**: `_MAM_DIALOG_TOKENS`에서 `|Yes, try it` 제거. `handle_startup_dialogs`의 분기는 그대로 둔다
|
||||
(모달 탐지는 기존 `Esc to cancel`이 이미 담당). 더 좁히려면 팁에 없는 물음표형
|
||||
`Try the new fullscreen renderer\?`를 앵커로 쓰는 편이 가장 정확하다.
|
||||
|
||||
### N-2 테스트의 `_init_herdr_isolation` stub이 **동작하지 않는다**
|
||||
|
||||
`_run_lib_helpers`는 `source` **뒤에** `_init_herdr_isolation() { :; }`을 정의하지만,
|
||||
`lib.sh:1881`에서 이미 source 시점에 실호출된다. 따라서 stub은 사실상 죽은 코드이고,
|
||||
매 테스트가 `$WORKSPACE_ROOT/.mam/shim/herdr`를 실제로 기록한다(실행 중 mtime 갱신 확인).
|
||||
`.mam/`은 gitignore 대상이라 git 오염은 없고 멱등이라 실피해도 없으나, **의도와 실제가 어긋나 있다.**
|
||||
|
||||
- **권고**: 아래 N-3의 미사용 파라미터를 활용해 `WORKSPACE_ROOT`를 임시 디렉터리로 넘긴다.
|
||||
|
||||
### N-3 `_run_lib_helpers(env_extra=...)`가 **어떤 호출부에서도 사용되지 않는다** (미사용 파라미터)
|
||||
|
||||
N-2의 해법 통로이므로 제거보다 활용을 권한다.
|
||||
|
||||
### N-4 테스트가 `/tmp/mam-fs-*-keys.$$`를 하드코딩한다
|
||||
|
||||
스크립트 말미의 `rm -f`는 `set -euo pipefail` 하에서 앞 단계가 실패하면 실행되지 않아 잔여 파일이 남을 수 있다
|
||||
(이번 실행에서는 잔여물 없음). pytest `tmp_path` 사용을 권한다.
|
||||
|
||||
### N-5 `FIX.md`가 여전히 **untracked**다
|
||||
|
||||
변경 근거 문서로 참조되고 있으므로, 병합 전 커밋하거나 의도적으로 제외한다면 그 판단을 남겨야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. git discard 여부
|
||||
|
||||
**discard 하지 말 것.** 두 문제 모두 실재함이 이번에 실물로 확정되었다 —
|
||||
herdr가 기동 중 차단 상태에서 `agent_not_ready`를 반환하는 것, 그리고
|
||||
Claude Code v2.1.247이 `Try the new fullscreen renderer?` 모달로 기동을 막는 것 모두 직접 재현했다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 요약
|
||||
|
||||
| 선행 지적 | 상태 | 근거 |
|
||||
|---|---|---|
|
||||
| D-1 죽은 프로세스 → 성공 승격 | ✅ 해소 | 실채집 herdr 출력 4종 분류 재현 |
|
||||
| D-2 팁을 다이얼로그로 오인 | ✅ 해소 | 실함수 실행, HEAD와 동일 동작 복귀 |
|
||||
| D-2c `wait_for_tui_ready` 데드락 | ✅ 해소 | rc=1 → **rc=0** |
|
||||
| D-3 Enter가 업셀 수락 | ✅ 해소 | 실모달 `Enter to confirm · Esc to cancel`, Escape 후 permission flag 보존 확인 |
|
||||
| D-4 회귀 방지력 없음 | ✅ 해소 | rev.1 복원 시 신규 4건 전부 실패 |
|
||||
| D-5 문서/린트 | ✅ 대부분 해소 | FIX.md 재작성 |
|
||||
|
||||
| 신규 지적 | 심각도 | 요지 |
|
||||
|---|---|---|
|
||||
| N-1 | 🔵 Low | `Yes, try it` 토큰 추가는 중복이며 평문 오탐 면적만 넓힘 |
|
||||
| N-2 | 🔵 Low | `_init_herdr_isolation` stub 무효 → 테스트가 `.mam/shim` 실제 기록 |
|
||||
| N-3 | 🔵 Low | `env_extra` 미사용 파라미터 |
|
||||
| N-4 | 🔵 Low | `/tmp` 하드코딩, 실패 시 잔여 가능 |
|
||||
| N-5 | 🔵 Low | `FIX.md` untracked |
|
||||
|
||||
핵심 결함은 모두 해소되었고 잔여는 전부 위생 수준이므로 병합 가능하다고 판단한다.
|
||||
설계 재작업 사유가 없어 PLANNER 에스컬레이션은 두지 않는다.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Cross-Code Review Report — Job 825cb977
|
||||
|
||||
Reviewer: `reviewer-cline-01` (Cline)
|
||||
Changeset: `.agents/skills/lib.sh` (+12/−4), `tests/test_b19_headless_reconcile_fixes.py` (+106), `FIX.md` (new, 22 lines).
|
||||
Scope: lint, behavior, and loss/orphan cross-review of the two lib.sh fixes + accumulated diff.
|
||||
|
||||
## 1. Are the targeted problems real? (discard gate)
|
||||
|
||||
The brief's stated "goal" text describes the *original* FIX.md intent (allow `timed out waiting for agent startup` + add generic fullscreen tokens). The **actual diff** does the corrected opposite on point 1 and a safer variant on point 2. Both addressed problems are real — this is **not** a discard candidate.
|
||||
|
||||
- **Fix 1 — agent-start detection.** Real problem: the prior code treated only `agent_started` as success, so herdr's documented `agent_not_ready` ("process up, blocked on a dialog") status caused rollback of a legitimately-starting agent that merely needed dialog handling. The fix promotes `agent_not_ready` to success (→ `wait_for_tui_ready`) and classifies fatal CLI errors first. It also **excludes** `timed out waiting for agent startup` from success — correct, because that string is ambiguous (herdr returns it for a dead `/bin/false` too), so promoting it would misclassify a dead process and waste the 30s readiness window.
|
||||
- **Fix 2 — fullscreen renderer upsell modal.** Real problem: Claude's fullscreen upsell modal (`Yes, try it`) is a blocking dialog. The fix adds the modal-unique token `Yes, try it` to `_MAM_DIALOG_TOKENS` and dismisses with **Escape** (reject). This is the safe choice: Enter would accept `Yes, try it` and restart the session without `--dangerously-skip-permissions` (permission-flag drop). The idle `/tui fullscreen` *tip* (`Try the new fullscreen renderer … · /tui fullscreen`, with a `❯` prompt) is intentionally **not** matched — it is non-blocking, and a generic `fullscreen renderer` token would false-match that ready idle screen and deadlock `wait_for_tui_ready`.
|
||||
|
||||
## 2. Lint
|
||||
|
||||
- `bash -n .agents/skills/lib.sh` → OK.
|
||||
- `.venv/bin/python -m py_compile tests/test_b19_headless_reconcile_fixes.py` → OK.
|
||||
- Shell quoting/regex consistent with surrounding code: fatal-error `grep -qiE` (case-insensitive ERE) first; success `grep -qE "agent_started|agent_not_ready"` (literal alternation, no unescaped metachars); new `Yes, try it` token is a literal with no ERE specials — safe inside `grep -Eq`/`grep -q`.
|
||||
- No shellcheck-style issues introduced (no unquoted expansions, no word-splitting hazards in the added lines).
|
||||
|
||||
## 3. Behavior
|
||||
|
||||
- **Fix 1 (lib.sh L546-556):** fatal errors (`^usage:`/`^error:`/etc.) break with `success=0` → downstream `if [ "$success" -ne 1 ]` (L562) → `exit 1` (fail-fast). `agent_started|agent_not_ready` → `success=1; break` → proceeds to `wait_for_tui_ready`. Timeout-only output → no match → retries (3 backoffs ≈3.5s) → `exit 1` (fast dead-process failure instead of a 30s wait). The `success` init/check chain is intact.
|
||||
- **Fix 2 (lib.sh L62, L1859-1862):** `Yes, try it` added to `_MAM_DIALOG_TOKENS` (so `_pane_dialog_open` detects the modal — also correctly gates `send_keys_safe` against prompting under a modal) and to `handle_startup_dialogs` (sends Escape). The branch is placed **before** `Yes, proceed` and the readiness-token branch — correct ordering (modal must be dismissed before ready detection). After Escape the loop re-captures and returns 0 once the banner appears; bounded by `timeout` (default 20s). The idle tip contains no `Yes, try it` → `_pane_dialog_open` returns false → `wait_for_tui_ready` detects the banner (no deadlock).
|
||||
- **Tests:** the 4 new tests are genuine **behavior tests** (stub `_pane_capture`/`_sks_herdr`/`sleep`, source the real `lib.sh`, exercise real `_pane_dialog_open`/`handle_startup_dialogs`/`wait_for_tui_ready`). `_LIB_SH` uses `Path(__file__).resolve()` (CWD-independent). One source-string guard (`test_agent_start_success_tokens_exclude_startup_timeout`) asserts token membership + error-before-success ordering.
|
||||
## 4. Loss / Orphan analysis
|
||||
|
||||
- **lib.sh:** the removed standalone `if grep -q "agent_started"; then success=1; break; fi` is fully superseded by the combined `agent_started|agent_not_ready` check — no orphaned variable or branch. `success=0` init and the downstream `success`-ne-1 guard remain consistent. The new `Yes, try it`→Escape branch is self-contained; no existing branch was orphaned.
|
||||
- **Tests:** `from pathlib import Path` is used by `_LIB_SH`; both `_FULLSCREEN_TIP`/`_FULLSCREEN_MODAL` fixtures are used; `_run_lib_helpers` is used by 3 behavior tests. No unused imports or dead helpers introduced.
|
||||
- **No lost functionality:** `agent_not_ready` is a *superset-preserving* addition (still proceeds to `wait_for_tui_ready`); the timeout exclusion is an intentional, justified narrowing (ambiguous token), not a loss of needed behavior. `FIX.md` is an accurate working note (untracked, expected to ship with the fix).
|
||||
|
||||
## 5. Test results
|
||||
|
||||
- Cited 4 suites (`test_b19_headless_reconcile_fixes.py`, `test_herdr_shim_contract.py`, `test_a4_adapter_contract.py`, `test_b8_send_keys_verification.py`) → **29 passed**.
|
||||
- Broader sweep `pytest tests/ -q`: ~378 tests passed with **0 failures** (full unit + component + tier1/2 + tier3 integration all green). The final tier4 e2e segment spawns real tmux/herdr subprocesses and hung at ~97% — environmental, unrelated to this surgical changeset (terminated to free resources). Zero failure lines in the output.
|
||||
- Regression-guard effectiveness (mutation-tested in the prior adjudication pass on this same diff, re-confirmed here by inspection): Escape→Enter on the modal makes `test_fullscreen_modal_is_rejected_not_accepted` FAIL; re-adding `fullscreen renderer` to `_MAM_DIALOG_TOKENS` makes `test_fullscreen_tip_is_not_a_blocking_dialog` FAIL. Guards are non-vacuous.
|
||||
|
||||
## 6. Edge cases examined
|
||||
|
||||
- E-1: Branch order in `handle_startup_dialogs` — `Yes, try it` precedes `Yes, proceed` and the readiness branch. The two dialogs are distinct (no token overlap); order is safe and correct (dismiss modal before ready).
|
||||
- E-2: Other consumers of `_MAM_DIALOG_TOKENS` — `send_keys_safe` gating via `_pane_dialog_open` also treats the modal as a dialog (blocks prompting under a modal). Consistent and desirable.
|
||||
- E-3: `Yes, try it` false-positive risk — specific affirmative phrase unique to the upsell modal; the tip fixture (contains `Try the new fullscreen renderer` but not `Yes, try it`) returns `DIALOG_CLOSED`. Low risk; acceptable.
|
||||
- E-4: Fatal-error regex `^error:` (case-insensitive) ordered first — if herdr ever emitted both an error line and a status, fatal wins (fail-safe). herdr success outputs are status lines, not `error:`. No conflict.
|
||||
- E-5: `agent_not_ready`→success then `wait_for_tui_ready` — if the process is up but never shows a banner (unhandled dialog), the readiness loop is bounded (30s) → abort. No zombie.
|
||||
- E-6: Escape on the modal re-captures next iteration; if the banner appears → return 0; if the modal re-appeared (unlikely) it would Escape again, bounded by the 20s `timeout`. Safe.
|
||||
|
||||
## 7. Verdict
|
||||
|
||||
Both targeted problems are real and correctly fixed. The changeset is surgical, lint-clean, behavior-tested with non-vacuous guards, and introduces no orphans or lost functionality. No design-level rework is required.
|
||||
|
||||
[VERDICT: PASS]
|
||||
+12
-4
@@ -59,7 +59,7 @@ for dir in /home/linuxbrew/.linuxbrew/bin /home/linuxbrew/.linuxbrew/sbin "$HOME
|
||||
done
|
||||
|
||||
# Central TUI dialog and readiness validation tokens (OP-6)
|
||||
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel|Resuming the full session|Resume from summary'
|
||||
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel|Resuming the full session|Resume from summary|Yes, try it'
|
||||
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku'
|
||||
|
||||
# Workspace-relative defaults with environment overrides (Phase Z)
|
||||
@@ -543,11 +543,15 @@ except Exception:
|
||||
else
|
||||
res=$(eval "_real_herdr agent start \"$agent_name\" --kind \"$kind\" --pane \"$target_pane\"" 2>&1 || true)
|
||||
fi
|
||||
if echo "$res" | grep -q "agent_started"; then
|
||||
success=1
|
||||
# Fatal CLI errors abort immediately. agent_not_ready is herdr's
|
||||
# documented "process is up, blocked on a dialog" status — continue
|
||||
# to wait_for_tui_ready. Do NOT treat "timed out waiting for agent
|
||||
# startup" as success: herdr returns that for a dead process too.
|
||||
if echo "$res" | grep -qiE "^usage:|unknown option|unknown flag|missing required|invalid_agent_name|^error:"; then
|
||||
break
|
||||
fi
|
||||
if echo "$res" | grep -qiE "^usage:|unknown option|unknown flag|missing required|invalid_agent_name|^error:"; then
|
||||
if echo "$res" | grep -qE "agent_started|agent_not_ready"; then
|
||||
success=1
|
||||
break
|
||||
fi
|
||||
if [ "$i" -lt 2 ]; then
|
||||
@@ -1852,6 +1856,10 @@ handle_startup_dialogs() {
|
||||
pane=$(_pane_tail "$sess" 20)
|
||||
if printf '%s\n' "$pane" | grep -Eq 'Do you trust the files|Yes, I trust this folder|Quick safety check'; then
|
||||
_sks_herdr send-keys -t "$sess" Enter
|
||||
elif printf '%s\n' "$pane" | grep -q 'Yes, try it'; then
|
||||
# Fullscreen-renderer upsell modal (not the idle /tui tip). Enter would
|
||||
# accept and restart the session without permission flags — reject.
|
||||
_sks_herdr send-keys -t "$sess" Escape
|
||||
elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then
|
||||
_sks_herdr send-keys -t "$sess" Down
|
||||
sleep 0.3
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
### 수정 내역 (lib.sh)
|
||||
|
||||
1. 에이전트 시작 판별 조건 완화 (new-session / `agent start`)
|
||||
|
||||
- 치명적 CLI 오류(`^usage:` / `^error:` 등)를 **성공 분류보다 먼저** 판별한다.
|
||||
- `agent_started`뿐 아니라 herdr 문서의 `agent_not_ready`(프로세스는 살아 있고 다이얼로그에 막힘)도
|
||||
롤백하지 않고 `wait_for_tui_ready`로 넘긴다.
|
||||
- `timed out waiting for agent startup`은 **성공으로 승격하지 않는다.** herdr는 `/bin/false`처럼
|
||||
이미 죽은 프로세스에도 같은 메시지를 반환하므로, 이 토큰은 실패와 지연을 구분하지 못한다.
|
||||
|
||||
2. Claude 풀스크린 렌더러 업셀 모달
|
||||
|
||||
- 차단형 모달 고유 시그니처는 `Yes, try it`이다. `_MAM_DIALOG_TOKENS`과
|
||||
`handle_startup_dialogs`는 이 문자열만 쓴다.
|
||||
- 거절은 **Escape**다. Enter는 기본 선택지 `Yes, try it`을 수락하고 세션을 permission flag 없이
|
||||
재시작한다.
|
||||
- idle 팁 `Try the new fullscreen renderer — … · /tui fullscreen`은 입력을 막지 않으므로
|
||||
다이얼로그로 취급하지 않는다. 광의 토큰 `fullscreen renderer|Try the new fullscreen`는
|
||||
팁과 모달을 동시에 매칭해 `wait_for_tui_ready`를 데드락 시킨다.
|
||||
|
||||
3. 검증: `tests/test_b19_headless_reconcile_fixes.py`에서 `_pane_capture`를 stub 한 행위 테스트
|
||||
(모달 vs 팁).
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from lib_py.layout import compute_2xk_layout
|
||||
@@ -101,6 +102,111 @@ def test_bug4_send_keys_safe_gating_order():
|
||||
assert dialog_idx < prompt_idx, "_pane_dialog_open must execute before agent prompt fast-path"
|
||||
|
||||
|
||||
_LIB_SH = str(Path(__file__).resolve().parent.parent / ".agents" / "skills" / "lib.sh")
|
||||
|
||||
_FULLSCREEN_TIP = """Claude Code
|
||||
Try the new fullscreen renderer — flicker-free output, mouse support, auto-copy on select · /tui fullscreen
|
||||
❯
|
||||
"""
|
||||
|
||||
_FULLSCREEN_MODAL = """Try the new fullscreen renderer?
|
||||
Flicker-free output
|
||||
Selected text auto-copies to your clipboard
|
||||
Yes, try it
|
||||
"""
|
||||
|
||||
|
||||
def _run_lib_helpers(script_body, env_extra=None):
|
||||
env = dict(os.environ)
|
||||
if env_extra:
|
||||
env.update(env_extra)
|
||||
wrapper = f"""
|
||||
set -euo pipefail
|
||||
source "{_LIB_SH}"
|
||||
_init_herdr_isolation() {{ :; }}
|
||||
{script_body}
|
||||
"""
|
||||
return subprocess.run(["bash", "-c", wrapper], capture_output=True, text=True, env=env)
|
||||
|
||||
|
||||
def test_agent_start_success_tokens_exclude_startup_timeout():
|
||||
"""D-1: dead-process timeout must not be promoted to success; agent_not_ready may."""
|
||||
content = open(_LIB_SH, encoding="utf-8").read()
|
||||
success_line = next(l for l in content.splitlines()
|
||||
if 'grep -qE "agent_started' in l)
|
||||
assert "agent_not_ready" in success_line
|
||||
assert "timed out waiting for agent startup" not in success_line
|
||||
err_line = next(i for i, l in enumerate(content.splitlines())
|
||||
if 'grep -qiE "^usage:' in l)
|
||||
ok_line = next(i for i, l in enumerate(content.splitlines())
|
||||
if 'grep -qE "agent_started' in l)
|
||||
assert err_line < ok_line
|
||||
|
||||
|
||||
def test_fullscreen_tip_is_not_a_blocking_dialog():
|
||||
"""D-2: idle /tui fullscreen tip must not trip _pane_dialog_open or send keys."""
|
||||
script = f"""
|
||||
_pane_capture() {{ printf '%s' '{_FULLSCREEN_TIP}'; }}
|
||||
KEYS=/tmp/mam-fs-tip-keys.$$
|
||||
: > "$KEYS"
|
||||
_sks_herdr() {{
|
||||
if [ "${{1:-}}" = "send-keys" ]; then echo "$*" >> "$KEYS"; fi
|
||||
return 0
|
||||
}}
|
||||
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
|
||||
handle_startup_dialogs dummy 2
|
||||
echo "KEYS_CONTENT=$(tr '\\n' '|' < "$KEYS")"
|
||||
rm -f "$KEYS"
|
||||
"""
|
||||
res = _run_lib_helpers(script)
|
||||
assert res.returncode == 0, res.stderr + res.stdout
|
||||
assert "DIALOG_CLOSED" in res.stdout
|
||||
assert "Escape" not in res.stdout
|
||||
assert "Enter" not in res.stdout.split("KEYS_CONTENT=")[-1]
|
||||
|
||||
|
||||
def test_fullscreen_modal_is_rejected_not_accepted():
|
||||
"""D-3: Yes, try it modal is a dialog; dismiss with Escape, never Enter."""
|
||||
script = f"""
|
||||
_pane_capture() {{ printf '%s' '{_FULLSCREEN_MODAL}'; }}
|
||||
KEYS=/tmp/mam-fs-modal-keys.$$
|
||||
: > "$KEYS"
|
||||
_sks_herdr() {{
|
||||
if [ "${{1:-}}" = "send-keys" ]; then echo "$*" >> "$KEYS"; fi
|
||||
return 0
|
||||
}}
|
||||
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
|
||||
handle_startup_dialogs dummy 1
|
||||
echo "KEYS_CONTENT=$(tr '\\n' '|' < "$KEYS")"
|
||||
rm -f "$KEYS"
|
||||
"""
|
||||
res = _run_lib_helpers(script)
|
||||
assert res.returncode == 0, res.stderr + res.stdout
|
||||
assert "DIALOG_OPEN" in res.stdout
|
||||
keys = res.stdout.split("KEYS_CONTENT=")[-1]
|
||||
assert "Escape" in keys
|
||||
assert "Enter" not in keys
|
||||
|
||||
|
||||
def test_wait_for_tui_ready_succeeds_on_fullscreen_tip():
|
||||
"""D-2c: tip + ready banner must not deadlock wait_for_tui_ready."""
|
||||
script = f"""
|
||||
_pane_capture() {{ printf '%s' '{_FULLSCREEN_TIP}'; }}
|
||||
_sks_herdr() {{
|
||||
if [ "${{1:-}}" = "capture-pane" ]; then printf '%s' '{_FULLSCREEN_TIP}'; return 0; fi
|
||||
if [ "${{1:-}}" = "send-keys" ]; then echo "KEY:$*" >&2; return 0; fi
|
||||
return 0
|
||||
}}
|
||||
sleep() {{ :; }}
|
||||
export MAM_READY_TOKENS='Claude Code|Welcome'
|
||||
wait_for_tui_ready dummy-sess claude
|
||||
"""
|
||||
res = _run_lib_helpers(script)
|
||||
assert res.returncode == 0, res.stderr + res.stdout
|
||||
assert "TUI detected ready" in res.stdout
|
||||
assert "KEY:" not in res.stderr
|
||||
|
||||
|
||||
def test_bug4_no_duplicate_input_on_rpc_success(tmp_path):
|
||||
"""Verify Bug 4: when herdr agent prompt succeeds, send_keys_safe returns 0 without calling paste-buffer."""
|
||||
test_script = f"""#!/usr/bin/env bash
|
||||
|
||||
Reference in New Issue
Block a user