자주 묻는 문제와 문제 해결¶
문제가 생겼을 때 사람은 어느 모듈이 망가졌는지 모른다 —— 자기가 무엇을 봤는지만 안다. 그래서 이 페이지는 당신이 관찰한 현상으로 묶었지, 서브시스템으로 묶지 않았다.
각 항목의 구조는 같다: 증상(실제로 본 것) → 원인 → 어떻게 할 것인가.
이 중 다섯 항목은 알려진 결함이며, 설계 의도가 아니다. 해당 항목은 버그임을 직접 밝히고 issue 링크와 우회 방법을 준다 —— 의도된 동작인 척하지 않는다.
설치가 안 됨 / 실행이 안 됨¶
설치 전체 절차는 install.md를 보라. 이 절은 "설치는 됐는데 명령이 실행되지 않는" 경우만 모았다.
Python 버전이 3.10 미만¶
증상: 설치 도중 문법 오류가 튀어나오거나, pip이 조건을 만족하는 버전을 찾을 수 없다고 바로 말한다.
원인: flower는 Python ≥ 3.10을 요구한다. 유일한 런타임 의존은 claude-agent-sdk이고, 네이티브 바이너리는 그 wheel 안에 들어 있다 —— 그래서 설치 실패는 대개 인터프리터 버전 문제이지 네트워크 문제가 아니다.
어떻게 할 것인가: 먼저 어느 인터프리터에 설치할지 확인한다.
3.10 미만이면 다른 것으로 바꿔서 설치한다. 시스템에 딸려 온 python3은 터미널에서 python이 가리키는 그것이 아닌 경우가 많다. 설치 전에 버전을 한 번 맞춰 보는 편이 설치 후에 원인을 캐는 것보다 싸다 (install.md 참고).
설치는 됐는데 flower: command not found¶
증상:
원인: 패키지는 설치됐지만, 생성된 실행 스크립트가 있는 디렉터리가 PATH에 없다. 이건 "설치 안 됨"과는 다른 얘기다 —— python3 -c "import flower"가 오류를 내지 않으면 패키지는 멀쩡하다.
어떻게 할 것인가: flower 스크립트의 shebang은 절대 경로다. 그러니 이미 PATH에 있는 디렉터리로 심볼릭 링크를 하나 걸어 주면 충분하고, 아무것도 source 할 필요가 없다.
macOS: install.sh가 안내한 대로 PATH를 추가했는데도 command not found¶
알려진 문제(issue #16)
이 안내는 하필 그것이 필요한 바로 그 기계에서 무효가 된다.
증상: macOS에서 install.sh를 돌린 뒤, 마지막에 나온 안내대로 ~/.local/bin을 PATH에 추가하고 터미널을 새로 열었는데도 flower는 여전히 command not found다.
원인: pip 폴백 경로로 들어간 경우, macOS의 pip은 실행 스크립트를 ~/Library/Python/3.X/bin에 설치한다. 그런데 install.sh가 추가하라고 안내하는 건 ~/.local/bin이다. 두 디렉터리가 맞지 않으니 안내대로 해도 소용없다.
install.sh가 설치 방식을 고르는 순서, 그리고 그 안내문의 원문
우선순위는 두 단계가 아니라 네 단계다(install.sh:35-56):
1. 有 uv → uv tool install --force
2. 否则有 pipx → pipx install --force
3. 否则 → curl astral.sh/uv/install.sh 自举 uv,成功则用 uv 装
4. 自举也失败 → "$PY" -m pip install --user --upgrade ← 出问题的是这一条
마지막 PATH 안내문의 원문(install.sh:62-68, command -v flower가 실패했을 때만 출력된다):
BINDIR은 $HOME/.local/bin으로 하드코딩돼 있다(install.sh:63). 1번과 3번 경로에서는 이게 맞다 —— uv가 거기에 설치하니까. 오직 4번 pip 폴백만 macOS에서 어긋난다. 그래서 이 함정은 앞의 세 경로가 모두 실패한 기계에서만 나타난다.
어떻게 할 것인가: 디렉터리를 추측하지 말고 인터프리터에게 물어라.
출력된 그 디렉터리를 PATH에 넣거나, 거기서 ~/.local/bin으로 심볼릭 링크를 건다:
ln -sf "$(python3 -c "import sysconfig; print(sysconfig.get_path('scripts', scheme='posix_user'))")/flower" ~/.local/bin/flower
uv / pipx / pip으로 설치한 게 서로 다른 flower다¶
증상: flower는 실행되는데 소스를 고쳐도 반영되지 않는다. 또는 업그레이드했는데 여전히 옛 버전이다. 또는 같은 기계의 두 터미널에서 동작이 다르다.
원인: 세 가지 설치 방식은 패키지와 실행 스크립트를 서로 다른 곳에 놓고, PATH에서 먼저 걸리는 것이 실행된다.
세 가지 설치 방식이 각각 어디에 떨어지는가
| 설치 방식 | 실행 스크립트 | 언제 쓰나 |
|---|---|---|
python3 -m venv .venv + pip install -e . | .venv/bin/flower | 소스를 고칠 때. 고치면 바로 반영된다 |
uv tool install / pipx install | ~/.local/bin/flower | 고치지 않고 쓰기만 하며, 격리된 환경이 필요할 때 |
pip install --user | Linux ~/.local/bin, macOS ~/Library/Python/3.X/bin | 폴백. 디렉터리는 앞 항목 참고 |
어떻게 할 것인가: 지금 실행되는 게 어느 것인지 먼저 확인하고, 그다음에 어느 쪽을 고칠지 정한다.
소스를 고치려면 venv + -e .를 쓰고, uv / pipx로 설치한 것과 공존시키지 마라 —— 공존 상태에서의 원인 추적 비용은 한 번 다시 설치하는 것보다 훨씬 비싸다 (install.md 참고).
자격 증명과 게이트웨이¶
缺少凭证:需要 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN¶
증상:
원인: flower는 setting_sources=[]로 호스트 설정을 차단하므로, 자격 증명은 직접 가져와야 한다. 전체 탐색 순서는 config.md를 보라.
어떻게 할 것인가: 저장소 루트의 .env에 쓰거나, 프로세스 환경에 넣는다.
.env는 이미 gitignore 돼 있다. 컨테이너에서의 방법은 deploy.md를 보라.
"flower는 ~/.claude/settings.json을 읽지 않는다"고 말한다 —— 이 문장은 틀렸다¶
증상: 자격 증명이 제대로 설정되지 않았을 때 env.py:192가 이렇게 출력한다:
원인: 이 문장은 코드와 맞지 않는다. env.py:56-75는 ~/.claude/settings.json을 실제로 읽고, 그중 자격 증명 필드만 가져와 마지막 폴백으로 쓴다 —— install.sh가 홍보하는 것도 바로 이 동작이다. 이 문장은 폴백까지 비어 있을 때만 출력되므로 무언가를 실패시키지는 않는다. 하지만 사람에게 "flower는 내 Claude Code의 토큰을 못 쓴다"는 결론을 내리게 만들고, 그건 틀렸다. issue #13에 기록해 뒀다.
어떻게 할 것인가: 이 기계에 Claude Code를 설치한 적이 있다면 자격 증명을 새로 발급받을 필요가 없다. 폴백이 알아서 주워 온다 (install.md 참고). 정말 이 문장을 봤다면 그 파일에도 쓸 만한 자격 증명 필드가 없다는 뜻이다 —— 앞 항목대로 .env를 써라.
실제로 적용된 자격 증명과 엔드포인트가 무엇인지 확신이 안 선다¶
증상: .env를 분명히 고쳤는데 요청은 여전히 옛 게이트웨이로 간다. 또는 지금 어떤 모델을 쓰는지 설명하지 못한다.
원인: 자격 증명과 엔드포인트에는 여러 출처(프로세스 환경, .env, 폴백)가 있고, 누가 이겼는지는 설정 파일이 아니라 런타임을 봐야 안다.
어떻게 할 것인가: -v를 붙여 한 번 띄운다. 시작할 때 describe()가 출력된다: 적용된 BASE_URL과 모델 매핑, 토큰은 마스킹된다.
스위치 전체 표는 cli.md, 변수 전체 표는 config.md를 보라.
.env에 KEY=를 남겨 두면 하위 출처는 영영 채우지 못한다¶
증상: 프로세스 환경에 토큰을 export 했고 .env에도 ANTHROPIC_AUTH_TOKEN= 줄이 있는데, 여전히 "자격 증명 없음"이라고 나온다.
원인: 빈 값도 한 번의 할당이다. 우선순위가 높은 출처의 KEY=가 그 키를 선점하고, 우선순위가 낮은 출처는 더 이상 채워 넣지 않는다. 그런데 check_credentials()(env.py:184에 정의)가 판정하는 건 "값이 비어 있지 않음"이므로 결국 없음으로 보고한다. "선점"과 "없음"은 다른 두 가지인데 현상은 완전히 똑같다 —— 이 부류의 문제에서 스스로 알아채기 가장 어려운 지점이다.
어떻게 할 것인가: 줄 전체를 지워라. 빈 값을 남기지 마라.
지운 뒤 -v로 적용값을 다시 확인한다. 파싱 규칙은 config.md를 보라.
서드파티 게이트웨이: 연결은 됐는데 첫 라운드부터 실패한다¶
증상: 401 / 403. 또는 모델 이름이 존재하지 않는다는 오류. 또는 시작하자마자 handoff가 일어나고 "시작 바닥값" 오류가 난다.
원인: 세 가지 설정 오류가 있고, 현상이 서로 다르다.
세 가지 게이트웨이 설정 오류, 각각 어떻게 알아보나
| 현상 | 대개 | 어디를 손대나 |
|---|---|---|
| 401 / 403 | 자격 증명은 맞지만 이 게이트웨이가 발급한 게 아니다. 또는 BASE_URL에 경로가 빠졌거나 끝 슬래시가 남았다 | config.md |
| 모델 이름이 존재하지 않는다 | 게이트웨이가 자기만의 모델 이름만 인식하는데 매핑을 안 했다 | config.md |
| 시작하자마자 handoff, "시작 바닥값" 오류 | window를 너무 작게 잡았다: 임계값이 역할의 시작 바닥값보다 낮다(coordinator는 실측 약 34k) | --window, handoff.md 참고 |
어떻게 할 것인가: 설정을 손대기 전에 -v로 적용값을 한 번 출력해 보라. 특히 window는 맞춰 볼 값어치가 있다 —— 개발용 이 기계의 게이트웨이는 claude-opus-5[1m]로 설정돼 있는데, 예전처럼 20만으로 계산하면 15만마다 한 세대씩 넘어간다. 실제로는 95만까지 갈 수 있으니 5배 차이이고, long-horizon 작업이 잘게 토막 난다.
실행은 되는데 동작이 잘못됐다¶
이 묶음의 증상은 오류가 아니라 명령이 끝나고 종료 코드가 0인데, 한 일이 잘못된 경우다. 앞의 네 항목은 확인된 코드 결함이고 issue도 올려 뒀다. 여기 적힌 건 우회 방법이지 수정 방법이 아니다. 마지막 항목은 설계 의도다.
flower setup이 띄우는 건 agent다¶
증상: flower setup을 실행하면 엔드포인트와 토큰을 물어볼 줄 알았는데, "무엇을 하고 싶은가"를 묻기 시작하고 이어서 go 전체 흐름을 다 돌면서 setup이라는 단어를 작업 설명으로 취급한다. 다 돌고 나면 자격 증명은 한 글자도 쓰이지 않았다.
원인: cli.py:937의 _CMDS에 "go", "run", "once"만 있고 "setup"이 빠졌다. 기본 서브커맨드를 채우는 단계에서 argv ["setup"]이 ["go", "setup"]으로 다시 쓰이고 —— setup은 서브커맨드에서 go의 첫 위치 인자, 즉 요구 사항 자체로 강등된다. 어떤 argv로도 설정 마법사에 도달할 수 없다. #11에 보고했다.
어떻게 할 것인가: Ctrl-C로 끊고 설정 파일을 직접 쓴다. setup이 원래 하던 일도 이 파일에 글자를 쓰는 것뿐이다:
전체 변수 이름과 자격 증명 탐색 순서는 config.md를 보라. 다 쓴 뒤 아무 디렉터리에서나 flower -v를 한 번 돌리면, 시작할 때 출력되는 적용 엔드포인트가 곧 확인 결과다.
전각 ?는 오라클 질의를 발동시키지 못한다¶
증상: 오라클 질의 방식대로 입력 프롬프트에 ?这个目录能删吗라고 쳤는데, oracle이 뜨지 않고 그 문장이 현재 질문에 대한 답변으로 취급되거나 그대로 수신함에 들어간다.
원인: cli.py:907이 startswith("?") 판정을 연달아 두 번 하는데 두 번 모두 같은 ASCII 문자를 쓴다. 코드 의도대로라면 두 번째는 전각 ?를 판정해야 한다. 중국어 입력기가 기본으로 내놓는 건 전각이다 —— 이 기능의 주 사용자가 하필 아무도 쓸 수 없다. #12에 보고했다.
이 항목은 요구 사항을 오염시킨다
빗나간 ?는 오류를 내지도, 버려지지도 않는다. 평범한 입력으로 처리된다: 확인 단계에서는 현재 질문에 대한 답변으로, 그 외에는 수신함으로 들어간다. 몰래 한마디 물어보려던 문장이 brief에 기록된다. 잘못 친 걸 발견했다면 그 자리에서 .flower/notes/需求.md를 고쳐라. 그 파일이 하위 단계의 기준이다.
어떻게 할 것인가: 반각으로 바꿔 ?를 치거나, 반각 ?를 먼저 치고 나서 중국어로 전환해 본문을 쓴다.
once의 누적 비용은 영원히 $0.00, 소요 시간은 영원히 0:00¶
증상: flower once를 처음부터 끝까지 돌려도 하단 상태 줄의 누적 비용이 계속 累计 $0.00으로 나오고 타이머는 계속 0:00인데, 같은 모델 같은 작업을 go로 돌리면 숫자가 나온다.
원인: once의 render()는 이벤트를 하나 받을 때마다 Render를 새로 만든다. 누산기도 함께 다시 만들어지니 매번 0에서 시작한다. 누적값은 반복해서 0으로 초기화되는 것이지 집계가 없는 게 아니다. #14에 보고했다.
어떻게 할 것인가: 숫자가 정확해야 하면 go로 가라. 그 경로는 영향을 받지 않는다. once의 단일 라운드 형태를 유지하면서 비용도 보고 싶다면, 다 돌린 뒤 runs/manifest.json을 봐라 —— 각 step의 비용이 거기 기록돼 있고, 그 기록은 맞다. 자세한 건 config.md를 보라.
plugin/에 넣은 skill이 계속 로드되지 않는다¶
증상: deploy.md대로 skill을 작성했고 디렉터리 구조도 맞는데, agent는 그 존재를 아예 모르는 것처럼 행동한다 —— 오류도 없고 로그 한 줄도 없다.
원인: plugin/이 wheel에 포함되지 않았다. 설치된 패키지에서 PLUGIN_DIR은 <site-packages>/plugin을 가리키는데 그 디렉터리는 없고, 로드 전 존재 여부 판정에서 그대로 조용히 건너뛴다. install.sh의 세 경로 모두 이 문제에 걸린다. 소스 체크아웃한 저장소만 로드할 수 있다. #15에 보고했다.
어떻게 할 것인가: 먼저 경로가 어디로 해석됐는지 자가 점검한다.
False가 나오면 이 항목이다. skill을 쓰고 싶다면 지금으로선 방법이 하나뿐이다: 소스 체크아웃에서 실행하기.
git clone https://github.com/ChenyuHeee/flower
cd flower
python3 -m venv .venv && .venv/bin/pip install -e .
ln -sf "$PWD/.venv/bin/flower" ~/.local/bin/flower
-e로 설치한 패키지는 체크아웃 디렉터리를 가리키므로 PLUGIN_DIR이 실제 plugin/에 떨어지고, 자가 점검을 다시 돌리면 True가 나온다.
-T가 go에서는 효과가 안 보인다¶
증상: flower go에 -T를 붙였는데, 붙인 것과 안 붙인 것을 비교해 보면 동작이 완전히 같아서 스위치가 고장 난 것처럼 보인다.
원인: 이 항목은 설계 의도이고 결함이 아니다. go 경로는 원래 trim이 기본으로 켜져 있어서 -T가 표현하는 의도가 이미 충족돼 있고, 그래서 한 번 더 줘도 달라질 게 없다. 이 경로에서 진짜 스위치는 반대 방향인 --no-trim이다 —— trim을 끄고 싶을 때만 명시적으로 준다. -T는 run과 once에서만 의미 있는 스위치다.
어떻게 할 것인가: go에서 trim이 실제로 켜져 있는지 확인하고 싶으면 -v로 시작 출력을 봐라. -T의 유무로 판단하지 마라. 끄고 싶으면 --no-trim을 준다. 스위치의 전체 의미는 cli.md를 보라.
다 했다고 하는데 안 됐다¶
goal guard가 존재하는 이유가 바로 이 부류를 막기 위해서다 —— worker에게는 체계적인 낙관 편향이 있고, 자기가 무엇을 했는지는 알지만 무엇을 빠뜨렸는지는 모른다. 그런데 guard 자신도 오판하고, 오판의 방향에는 규칙성이 있다. 아래 다섯 항목은 "통과시키면 안 되는 걸 통과시킨 경우"와 "계속 통과시키지 않는 경우"로 나눴다.
"여기서는 검증할 수 없다"고 말하고는 통과시킨다¶
증상: verdict 결과에 "현재 환경에서는 이 항목을 검증할 수 없으므로 달성으로 본다"라고 적혀 있고, workflow가 그대로 다음으로 넘어간다.
원인: judge가 「달성 불가」와 「미달성」을 하나의 결론으로 뭉갰다. 이 둘은 서로 다른 결론이고, 결론은 둘이 아니라 셋이다가 다루는 것이 바로 이 얘기다: 「미달성」은 되돌려 보내서 계속 시키는 것이고, 「달성 불가」는 멈추고 사람에게 묻는 것이다. 받아들일지, 목표를 바꿀지, 아니면 판정이 틀렸다고 할지를 고르게 된다. "달성/미달성" 두 결론만 있으면, 사실 불가능한 목표 하나가 coordinator를 라운드마다 헛돌게 해서 예산이 바닥날 때까지 태운다.
어떻게 할 것인가: "여기서는 검증할 수 없다"를 절대 달성으로 취급해서는 안 된다. judge에게 주는 instructions에 당신의 시나리오에서 무엇이 불가능인지 명시해서, 「달성 불가」를 줘야 할 때 「달성 불가」를 주게 하라. 멈춰서 묻는 게 정말 싫다면 --timeout 0을 줘라: 달성 불가일 때 바로 멈추고 사유는 디스크에 남는다. 얼버무리고 넘어가지 않는다.
소스만 읽고 다 됐다고 한다¶
증상: verdict 사유에 "코드에 X가 이미 구현돼 있다", "함수 시그니처가 요구를 충족한다"라고 적혀 있는데, 빌드 산출물이나 명령 출력, 띄운 서비스는 하나도 건드려지지 않았다.
원인: judge가 소스 쪽으로 유도됐다. 검증 대상은 산출물이지 소스가 아니다 —— 소스가 맞아 보이는지와 내놓은 것이 쓸 수 있는지는 다른 두 가지다. 전자는 worker가 이미 확신하고 있는 것이라, 한 번 더 확신해 봐야 새로운 정보가 생기지 않는다.
어떻게 할 것인가: 판정 항목은 산출물에 대한 단언으로 써야 한다. "내보내기 기능을 구현했다"는 안 되고, "./app export out.csv를 돌리면 out.csv에 헤더 3열이 있다"라야 된다. 목표를 쓰는 단계에서 이렇게 써야지, 안 그러면 judge가 모호한 항목을 보고 알아서 메울 수밖에 없다.
judge가 명령을 실행할 수 없어서 Makefile을 읽고 통과시킨다¶
증상: 목표가 "Linux에서 실행 가능한 바이너리를 빌드한다"였고 verdict는 통과였다. 직접 file을 돌려 보면 산출물은 Mach-O이고, ELF가 전혀 아니다.
원인: judge는 기본이 judge(can_run=False)라, 손에 Read / Glob / Grep밖에 없다. 이 세 도구로는 파일은 읽을 수 있지만 file도, ./app --version도 실행할 수 없다. 그래서 차선책으로 Makefile을 읽고, Darwin 분기에 크로스 컴파일이 적혀 있는 걸 보고 조건이 충족됐다고 판단했다. 거짓말을 한 게 아니라, 자기 능력 범위 안에서 가장 증거처럼 보이는 것을 찾았을 뿐이다.
언제 반드시 can_run을 켜야 하나
판단 기준은 간단하다: 목표에 "빌드해 낸 것" 같은 말이 나오면 켜라.
- 산출물류: 바이너리, 이미지, 패키징 파일, 생성된 데이터 —— 켠다
- 동작류: 서비스가 뜨는가, 명령이 0을 반환하는가, 출력이 어떤 패턴에 맞는가 —— 켠다
- 순수 텍스트류: 문서를 썼는가, 어떤 필드를 schema에 추가했는가 —— 안 켜도 된다
명령줄에서는 --judge-can-run이다. 직접 배선할 때는 진입점마다 쓰는 법이 다르지만, 결국 모두 judge()의 그 인자로 떨어진다:
| 진입점 | 어떻게 넘기나 | 출처 |
|---|---|---|
judge() | can_run=이 정식 인자다 | roles.py:361 |
with_goal() | can_run=이 인자이고, judge()로 전달된다 | goal.py:155 → :170 |
goal_step() | can_run 인자가 없지만 **spec_kw로 들어가고, 그 줄이 바로 judge(..., **spec_kw)다 —— 도달한다 | goal.py:97 → :105 |
starter_flow() | judge_can_run=이 with_goal(can_run=…)으로 바뀐다. --judge-can-run이 타는 게 바로 이 경로다 | starter.py:105 → :196 |
대가는 judge가 실제로 명령을 실행하게 되어 한 라운드의 판정이 더 느리고 더 비싸진다는 것이다. 대신 얻는 건 현장을 검증한다는 점이다. 현장의 설명서가 아니라. judge가 명령을 실행할 수 있는가를 보라.
어떻게 할 것인가: 목표가 산출물에 관한 것이면 --judge-can-run을 켜라. 켜지 않을 때는 "읽을 수만 있으면 판정 가능"을 판정 항목 작성의 강제 제약으로 삼아라 —— 그렇게 쓸 수 없는 항목은 애초에 명령 실행이 필요한 항목이라는 뜻이다.
판정 목록이 십몇 개, 계속 통과를 못 한다¶
증상: 라운드마다 되돌려지고, 무엇이 모자란지 길게 나열되며, 고칠수록 늘어나서 일이 안 끝난다.
원인: 목록을 "내가 얼마나 엄밀하고 싶은가" 기준으로 썼지, "이 일에 실패 방식이 몇 가지인가" 기준으로 쓰지 않았다. 목록의 길이는 실패 방식이 몇 가지인지가 정한다: git clone && make && ./app 같은 작업이면 세 개에서 다섯 개면 충분하다 —— 빌드 성공, 실행됨, 쓸 수 있음. HT002의 실제 사고에서는 "저장소를 설치해서 돌린다"는 작업이 15개로 쓰였다: 그중 5개만 쓸 수 있는지를 검증했고, 6개는 과정에서 규칙을 지켰는지를 검증했으며, 4개는 원리상 검증 자체가 불가능했다.
어떻게 할 것인가: .flower/notes/目标.md를 고쳐라. 그 파일이 판정의 근거다. 항목마다 "이건 어떤 실패에 대응하는가"를 물어보고, 답이 안 나오면 지워라. 목표를 세우는 단계에서 검증 불가능한 항목은 원래 경고를 내니, 경고가 난 항목들을 억지로 남겨 두지 마라.
경계가 판정 항목으로 쓰였다¶
증상: 목록에 "brew install을 실행한 적 없음", "프로젝트 디렉터리 밖의 파일을 수정한 적 없음" 같은 항목이 나오고, judge가 결백을 입증하려고 ~/.zshrc의 mtime을 조회하고 .flower/ 디렉터리가 건드려졌는지 뒤진다.
원인: 경계와 판정 항목은 서로 다른 것을 제약한다. 섞으면 HT002 때의 주된 원인이 된다 (근본 원인 1).
| 무엇을 제약하나 | 어떻게 지키나 | |
|---|---|---|
| 경계 | 일하는 방식("프로젝트 디렉터리 안에서만 설치", "업무 코드는 건드리지 말 것") | 넘지 않음으로 지킨다. 사후 자기 입증이 아니다 |
| 판정 항목 | 내놓은 것("실행이 되는가", "결과가 맞는가") | 그 자리에서 검증해 지킨다 |
경계는 clarify 단계에서 잔뜩 쓰라고 권장되는 바로 그 부분이다. 그것들을 하나씩 목록으로 옮기면 경계를 하나 추가할 때마다 검사가 하나 늘어나는 셈인데, 이런 검사는 대부분 검증할 수 없다 —— 검증할 수 없는 항목은 판정 한 라운드 전체를 함께 실패시킨다.
어떻게 할 것인가: 경계는 brief의 「경계」 절에 남겨 두고 넘지 않음으로 지켜라. 판정 목록에 넣지 마라. 정말 설명이 필요하면 한 문장으로 끝내고, 여섯 항목으로 쪼개지 마라.
컨텍스트와 비용¶
long-horizon 실행에서 컨텍스트와 돈은 같은 문제다: 컨텍스트가 천장에 닿으면 handoff를 하거나 그 step이 터지고, 라운드마다 반복해서 하는 말은 뒤의 모든 라운드에서 다시 값을 치러야 한다.
도중에 스스로 새 session을 열고 "handoff"라고 한다¶
증상 이벤트 스트림에 handoff가 나타나고, payload["phase"]가 먼저 near, 그다음 done이 되며, 중간에 handoff document를 쓰는 한 라운드가 더 든다. 그 뒤로는 하던 일을 그대로 계속한다.
원인 컨텍스트가 임계값에 근접했다. flower는 compact를 하지 않는다 —— 현재 session의 상태를 다섯 절짜리 handoff document로 쓰고, 새 session을 띄워 그것을 읽고 이어서 하게 한다. compact는 "막힌 길" 같은 가장 비싼 정보까지 함께 지워 버리지만, handoff document는 명시적이고 디스크에 떨어져 있으며 언제든 고칠 수 있다: 인계받는 session이 읽는 것이 바로 그 파일이다.
어떻게 할 것인가 이건 정상 경로이니 신경 쓰지 않아도 된다. handoff는 재시도로 세지 않는다 —— attempts는 올라가지 않고(그건 실패를 세는 값이다), 소모된 session_id는 StepResult.retired에 기록되며, 밖으로 노출되는 session_id는 언제나 살아 있는 후임 쪽이다 (../guide/handoff.md#换代不算重试账怎么记 참고). 정말 SDK의 auto-compact로 되돌리고 싶으면 --no-handoff를 써라.
임계값은 어떻게 나오는가, 그리고 왜 기본값을 이렇게 공격적으로 잡았는가
at = window - headroom. window는 기본 100만이고 모델 이름으로 판단한다: 이름에 haiku가 있으면 20만, 나머지는 100만. headroom은 기본 50k다 —— auto-compact가 −33k에서 발동하니 handoff는 그보다 앞서야 하고, "handoff를 쓰는" 것 자체가 한 라운드를 더 쓰므로 50k가 이 두 가지를 동시에 만족시킨다.
크게 잡는 건 치명적 오류가 아니다: 실제 window가 더 작으면 임계값에 영영 닿지 못하고, 요청이 API에서 「prompt가 너무 길다」로 반려된다. flower는 이 신호를 알아보고(handoff.is_overflow()) 그 자리에서 기계적으로 조립한 축소판으로 handoff하며, 이 step은 실패하지 않는다 (../guide/handoff.md#is_overflow把硬错变成当场换代 참고).
실측으로 언급할 만한 것: 개발용 이 기계의 게이트웨이는 claude-opus-5[1m]로 설정돼 있다. 예전처럼 20만으로 계산하면 15만마다 한 세대씩 넘어가는데, 실제로는 95만까지 갈 수 있다 —— 5배 차이이고, long-horizon 작업이 잘게 토막 난다.
시작하자마자 handoff, 그리고 멈추지 않는다¶
증상 오류에 "시작 바닥값"이 언급되거나, 같은 step에서 반복해서 handoff가 일어나 max_generations=8에 부딪힌다.
원인 window를 작게 잡아서 임계값이 그 역할의 시작 바닥값보다 낮다 —— coordinator는 실측 약 34k인데, 시스템 프롬프트에 workbench 인덱스만 더해도 그만큼 잡아먹는다. 새 session이 입을 떼자마자 선을 넘고, 그래서 handoff를 쓰고, 세대를 넘기고, 다시 선을 넘고, 영원히 멈추지 않는다 (handoff는 재시도 예산을 먹지 않고, 그건 의도적이다).
어떻게 할 것인가 --window를 모델의 실제 window로 맞춰라. -v가 적용된 엔드포인트와 모델 매핑을 출력한다. 정상적인 장시간 실행에서는 8세대까지 갈 일이 없고, 정말 부딪혔다면 거의 확실히 이 원인이다. 오류 메시지도 바로 그렇게 말한다 (../guide/handoff.md#一道防跑飞的闸 참고). 관련된 또 다른 증상은 "handoff가 항상 축소판으로 나온다"이다. 원인은 runs/manifest.json의 errors에 적힌다.
도중에 멈추고, 예산이 다 찼다고 한다¶
증상 step이 안 끝났는데 멈추고, 사유는 비용 초과다.
원인 AgentSpec(max_budget_usd=...)는 하드 상한이지 부드러운 알림이 아니다. Runtime.total_cost()는 이번 run의 합계다.
어떻게 할 것인가 상한을 올리기 전에 헛돌고 있는 건 아닌지 먼저 확인하라. 라운드마다 되돌려지면서 매 라운드 진전이 없다면, 대개 judge가 「달성 불가」를 줘야 할 자리에 「미달성」을 준 것이다 —— 사실 불가능한 목표는 예산이 바닥날 때까지 계속 태운다 (../guide/goal.md#三个结论不是两个 참고). 정상적으로 일하고 있음을 확인한 뒤에 상한을 올려라.
이번 run은 왜 이렇게 비싼가¶
증상 비용이 예상을 크게 넘는데, 출력을 봐도 돈이 어디에 쓰였는지 알 수 없다.
원인 장부는 모델의 컨텍스트 안에 없다. 각 step의 session_id, 비용, 재시도 횟수, 실패 사유는 runs/manifest.json에만 기록되고 프로세스를 넘어 append된다. 재시도 이력과 오류 원문도 여기에만 있다 —— 모델은 볼 수 없고, 그건 의도적이다: 거부된 호출이 컨텍스트에 쌓이면 coordinator는 "Bash는 어차피 막힌다"를 학습해서 git status조차 시도하지 않게 된다 (Runtime(keep_denials=1)이 기본으로 이미 비운다. 키우지 마라).
어떻게 할 것인가 runs/manifest.json을 열어 step별로 비용을 대조하라(디스크 배치는 config.md#磁盘布局 참고). 실측 참고값 몇 가지:
| 비용 | |
|---|---|
| subagent 하나의 시작 바닥값(분산 불가) | ~4.3k tokens |
| coordinator의 시작 바닥값 | ~34k tokens |
tests/smoke.py 단일 agent 전체 체인 | ~$0.21 |
tests/flow_demo.py workflow 세 가지 연결 방식 | ~$0.39 |
tests/delegation.py 분업 + 컨텍스트 분포 측정 | ~$0.71 |
tests/isolation.py 세 개의 issue, 세 개의 worktree | ~$0.9 |
컨텍스트가 일보다 빨리 자란다¶
증상 라운드마다 task brief에 같은 규율("먼저 파일을 읽고 고쳐라", "업무 코드는 건드리지 마라", "고친 뒤 테스트를 돌려라")을 반복하고 있는데, worker는 원래부터 그렇게 하고 있다.
원인 coordinator가 하는 모든 말은 자기 transcript에 들어가고, transcript는 늘기만 하고 줄지 않는다. 규율을 한 번 되풀이하면 이번 라운드에 값을 치러야 하고, 뒤의 모든 라운드에서 그 대목을 다시 치러야 한다. 상대가 이미 아는 것을 되풀이하면 이득은 0, 비용은 영구적이다.
어떻게 할 것인가 규율은 메커니즘에 넣고 매 라운드의 말에 넣지 마라: allowed_tools, brief의 「경계」 절, workbench 인덱스로 표현할 수 있는 것은 task brief에 쓰지 마라. task brief에는 이번 라운드에 무엇이 달라졌는지만 써라. 분업 자체가 가장 많이 절약되는 층이다 (../guide/context.md#第一层分工省得最多 참고). resume 할 때는 오래된 큰 도구 결과를 -T로 파일 포인터로 바꿀 수 있다.
중단과 continuity¶
프로세스가 kill 됐고, 기계가 재부팅됐다¶
증상 도중에 사라졌고, 터미널을 다시 열었을 때 어떻게 주워 담아야 할지 모르겠다.
원인 주워 담을 게 없다. lineage(runs/lineage.json)는 step 이름 → session_id를 기록하고, 각 step이 끝날 때마다 디스크에 떨어진다. 쓸 때는 먼저 .tmp에 쓰고 원자적으로 교체한다 —— 도중에 죽어도 반쪽짜리 파일이 남지 않는다.
어떻게 할 것인가 같은 디렉터리로 돌아가 flower를 다시 실행하면 각 step이 지난번 session을 이어 간다: 요구 사항을 다시 캐묻지 않고, 목표를 다시 세우지 않으며, coordinator가 어떤 막다른 길을 시도해 봤는지까지 기억한다. 아무 말도 하고 싶지 않으면 그냥 엔터를 쳐라 (../guide/continuity.md#进程被杀和机器重启 참고). judge는 예외다 —— 그건 Step이 아니라 gate에서 바로 파견되는 것이라 lineage 경로를 애초에 타지 않는다. 그래서 매 라운드 완전히 새로운 한 쌍의 눈이다.
매번 처음부터 시작하고, 전혀 이어지지 않는다¶
증상 같은 디렉터리에서 다시 돌렸는데 요구 사항을 또 캐묻는다.
원인 세 가지 "불일치"가 있고, flower는 일괄적으로 조용히 처음부터 시작으로 되돌아가며 오류를 내지 않는다 —— continuity는 있으면 좋은 것이지, 그것이 실패했다고 사람이 일하는 걸 막아서는 안 된다:
runs/lineage.json이 없거나, 그 안의workspace가 지금 경로와 일치하지 않는다(디렉터리를 복사해 옮겼으면 이렇게 된다)- session이 이미
runs/sessions.db에 없다(DB를 지운 적 있음) - lineage 파일이 깨졌다
어떻게 할 것인가 먼저 runs/lineage.json이 있는지, workspace가 맞는지 확인하라 (세 파일의 역할은 ../guide/continuity.md#落在磁盘上的三个文件 참고). 디렉터리를 옮긴 뒤 continuity가 안 되는 건 의도적이다: project_key는 워크스페이스 경로에서 유도되므로 옛 session은 새 위치에서 조회되지 않는다.
새로 시작하고 싶지만 이력은 잃고 싶지 않다¶
증상 요구 사항이 이미 다른 방향으로 바뀌었는데, 지난번 판을 이어서 말하게 하고 싶지 않다.
원인 기본 동작이 이어서 말하는 것이다. 이미 쓴 적 있는 디렉터리에서 flower "顺便支持代码块高亮"는 새 작업이 아니라 한마디 더 한 것이다.
어떻게 할 것인가 --new. 이건 삭제가 아니라 아카이브이며, 옛것은 notes/archive/에 남는다. wake 시점에 현재 컨텍스트 규모를 한 줄 알려 주는데, 크다 싶으면 이 경로로 가면 된다.
네트워크가 끊겼는데 오류도 안 내고 움직이지도 않는다¶
증상 화면에 새 이벤트가 없고 프로세스는 살아 있어서 멈춘 것처럼 보인다.
원인 네트워크 단절은 실패가 아니라 "좀 기다리자"로 취급된다. flower는 걸어 놓고 기다린다: 먼저 DNS를 탐색하고, 다음에 TCP를 탐색하며, 통하면 계속한다 (../guide/continuity.md#韧性断网时挂着等而且错误不进接续后的上下文 참고). HT001에서 이 항목이 실제 장애로 검증됐다 (../cases/ht001.md#六断网续跑第一次被真实故障验证 참고).
어떻게 할 것인가 기다려라. -v로 탐색이 도는 걸 볼 수 있다. 대기 중에 쌓인 그 오류들은 continuity 이후의 컨텍스트에 들어가지 않는다 —— runs/manifest.json에만 떨어지고, 인계받는 session이 보는 것은 깨끗한 현장이라 줄줄이 이어진 타임아웃에 휘둘리지 않는다.
Ctrl-C를 연속 두 번 눌렀더니 마무리가 다 안 됐다¶
증상 kill(SIGTERM)로 끄는 것과 Ctrl-C를 연속 두 번 누르는 것이 남기는 현장이 다르다.
원인 알려진 구멍이다. Ctrl-C 두 번은 KeyboardInterrupt를 던진다: cli.py의 _drive에 있는 finally가 rt.close()는 호출하지만 rt.rescue()는 호출하지 않는다 —— rescue()는 SIGHUP/SIGTERM 핸들러에서만 호출된다.
어떻게 할 것인가 lineage는 두 경로 모두에서 지켜진다(각 step이 끝날 때마다 원자적으로 디스크에 떨어진다). 그래서 다시 돌려도 그대로 이어지고, 이 구멍 때문에 진행 상황을 잃지는 않는다. 완전한 마무리를 거치려면 Ctrl-C를 마구 누르지 말고 kill <pid>를 써라.
병렬과 isolation¶
not in a git repository¶
증상 isolation을 켰더니 아예 시작되지 않고 not in a git repository가 난다.
원인 worker(..., isolate=True)는 git worktree로 agent마다 사본 하나씩을 주는데, workspace가 git 저장소가 아니면 만들 수 없다.
어떻게 할 것인가 이 항목은 조용히 성능 저하 모드로 넘어가지 않는다 —— 진짜 저장소 안에서 돌리든지, isolate를 끄든지 둘 중 하나다. isolation이 보장하는 건 "여러 agent가 동시에 고치면서 서로의 작업 트리를 보지 못한다"는 것이지, 병합에 충돌이 없다는 것을 대신 보장해 주지는 않는다.
격리된 agent가 workbench에 쓰지 못한다¶
증상 subagent가 "쓰기 권한 거부"를 보고하고 스크립트와 산출물이 디스크에 떨어지지 않는다. 또는 산출물이 어느 한 worktree 안으로 들어가서 다른 agent가 보지 못한다.
원인 worktree는 agent마다의 사본이고, workbench는 agent를 가로지르는 공유 층이다. 공유되어야 할 것을 사적인 울타리 안에 넣었으니 남이 못 가져가는 게 당연하다.
어떻게 할 것인가 isolation을 켤 때는 workbench를 저장소 밖으로 지정하라:
워크스페이스 밖에 두면 Runtime이 자동으로 add_dirs로 권한을 준다. Runtime(workbench=True)는 이걸 이미 처리하고 있고, 직접 만든 Workbench의 권한은 직접 줘야 한다.
workbench에는 기본 위치가 둘이고, 서로 다르다
Workbench(workspace) —— CLI와 starter_flow()가 타는 경로다 —— 는 workbench를 <workspace>/.flower에 둔다. 반면 Runtime(workbench=True)(즉 -W)가 두는 곳은 <run_dir>/workbench, 곧 runs/workbench다. 그래서 flower를 한 번 돌리면 .flower/를 얻지만, Python에서 Runtime(workbench=True)를 호출하면 얻지 못한다.
flower를 돌리면 .flower/가 생기는데, 직접 스크립트를 쓰면 안 생긴다¶
증상 brief를 분명히 .flower/notes/需求.md에 썼는데 coordinator는 읽은 적 없는 것처럼 군다. 오류는 없다.
원인 손에 있는 게 workbench 객체 두 개다. brief는 A 디렉터리에 쓰이는데, system prompt에 주입되는 인덱스는 B 디렉터리를 스캔한다. "시작하자마자 요구 사항 파일이 어디 있는지 안다"는 약속이 조용히 무효가 된다. 두 가지 잘못된 방식 모두 오류를 내지 않는다: 직접 brief_path를 프로세스 cwd 기준으로 조립하면 -W가 만든 <run_dir>/workbench와는 다른 두 디렉터리가 된다. Runtime에서 거꾸로 가져오는 것도 안 된다 —— cli.py가 먼저 main()을 호출해 Workflow를 만들고 나서야 Runtime을 만드는데, 그때 brief_path는 이미 고정돼 있다.
어떻게 할 것인가 Workbench를 직접 하나 만들고, 같은 객체를 Workflow와 Runtime에 함께 걸면 위치가 못 박힌다:
wb = Workbench(Path.cwd()).ensure()
wf = Workflow(channel=ch, workbench=wb, steps=[...])
rt = Runtime(workspace=".", workbench=wb)
tests/trial_offline.py가 이 항목을 못 박아 뒀다: 다섯 번째 단언이 brief가 prompt_block() 안에 나타나는지를 본다. 또한 인덱스는 coordinator의 system prompt에만 주입되고 subagent는 상속받지 못한다 (실측 $0.2461, tests/prelude_live.py) —— 경로는 coordinator가 아래로 전달해 줘야 하고, subagent가 각자 자동으로 아는 게 아니다 (../guide/workflow.md#工作台要挂在-workflow-上 참고).