사전 확인¶
손대기 전에 요구사항부터 분명히 한다. 확인자는 질문만 하고 작업은 하지 않는 역할로, 분명해질 때까지 묻고, 마지막에 정확히 네 단락짜리 브리프를 출력해 디스크에 얼린다. 이후 모든 스텝은 이 문서를 읽고 시작하며, 요구사항을 다시 추측하지 않는다 —— 그리고 그 질의응답은 하위 컨텍스트에 한 번도 들어간 적이 없다.
무슨 문제를 해결하나¶
flower가 컨텍스트를 정리할 때 걷어내는 것은 전부 현장이다: 유효기간이 지난 것, 거부된 호출의 제거, 오류 메시지의 제거, 큰 결과의 스필. 현장은 버려도 괜찮다. 다시 돌리면 또 생긴다.
그렇지 않은 오류가 한 부류 있다: 목표를 잘못 이해한 것. 이것은 컨텍스트를 정리하면 오히려 악화되는 유일한 종류의 오류다. 현장이 버려지고 나면 남는 것이 하필 그 잘못된 전제 위에 세워진 결정이고, 그것은 올바른 결정과 완전히 똑같아 보인다 —— 전제가 의심스럽다는 흔적이 어디에도 남지 않는다.
long-horizon은 이것을 최악까지 증폭시킨다. 잘못된 전제로 먼저 몇 시간을 돌고, subagent를 열몇 개 파견하고, 디스크에 산출물을 잔뜩 남긴 다음에야 드러난다. 그때 비싼 것은 토큰이 아니라 모든 산출물이 잘못된 요구사항 위에 지어졌다는 사실이다. HT001의 계산서에서 그 비율을 잴 수 있다. 요구사항을 확인한 스텝은 $0.3704 / 5 라운드 / 0.06h, 그 뒤 실제로 일한 스텝은 $171.2476 / 31 라운드 / 10.44h.
그래서 "멈춰 서서 물을 수 있는" 통로가 필요하고, 그것은 반드시 작업 이전에 있어야 한다.
어떻게 쓰나(최소 코드)¶
코드 없이: 커맨드라인¶
프로젝트 디렉터리로 들어가서 바로 실행한다:
터미널에는 이런 질문이 보인다:
- 번호를 입력해 선택지를 고르거나, 그냥 타이핑해서 답한다
- 엔터 = 이 질문을 건너뛰기. 스스로 판단하게 하고 그 가정을 「未知与假设」에 기록시킨다
- 네 단락이 다 갖춰져야 통과하며, 브리프는
.flower/notes/需求.md에 얼려진다 - 다시 돌려도 또 캐묻지 않는다 —— 다시 확인받고 싶으면 그 파일을 지우거나
--new를 붙인다
무엇을 묻는지만 보고 그 아래로는 진행하지 않으려면: flower --clarify-only. 질문에 하드 쿼터를 주려면: --asks 12 (지정했을 때만 선택지 아래에 (还能问 N 次) 한 줄이 더 나온다. 기본은 횟수 무제한이라 그 줄이 나오지 않는다). 지켜보는 사람이 없으면: --timeout 0. 이 경로의 구현은 flower/workflow/starter.py에 있다.
답변은 표준입력으로 들어온다. 진짜 터미널에서 실행할 것
파이프, nohup, CI에서는 아무도 답할 수 없다. stdin이 EOF에 닿는 순간 그때 걸려 있던 질문은 "입력이 닫혔다"로 처리되어 건너뛰고, 이후의 모든 질문은 --timeout을 꽉 채워 기다리게 된다. 이런 상황에서는 그냥 --timeout 0을 준다 —— 모든 질문이 즉시 무응답 처리되고, 스스로 판단해 가정을 「未知与假设」에 적는다.
직접 배선하기¶
from pathlib import Path
from flower import HumanChannel, Step, Workbench, Workflow, clarify_step
wb = Workbench(Path.cwd()).ensure()
ch = HumanChannel(log_path=wb.notes / "问答记录.md") # 기본값은 질문 횟수 무제한, 사람을 30분 기다림
wf = Workflow(channel=ch, workbench=wb, steps=[
clarify_step(ch, brief_path=wb.notes / "需求.md", prompt="帮我做一个 X"),
Step("干活", spec=协调者, prompt=lambda ctx: f"照这份需求做:\n\n{ctx['确认需求']}"),
])
prompt에는 당신의 원래 요구만 쓴다. 한 문장이면 충분하다. 무엇을 물을지는 확인자가 스스로 정한다 —— 당신의 도메인에서 어떤 것을 물어야 하는지는 프레임워크가 알지 못하고, 알아서도 안 된다. 위의 协调者는 당신이 coordinator()로 직접 만든 AgentSpec이다. 워크플로 설계를 보라.
clarify_step()의 파라미터:
| 파라미터 | 기본값 | 설명 |
|---|---|---|
channel | — | HumanChannel. 같은 인스턴스를 Workflow(channel=...)에도 걸어야 한다 |
brief_path | — | 브리프를 어디에 떨굴지. 걸어둔 그 워크벤치 안이어야 한다. 아래 참고 |
prompt | — | 당신의 원래 요구. str 또는 Callable[[Ctx], str] |
name | "确认需求" | 스텝 이름이자 ctx의 키 이름 |
spec | None | 직접 만든 AgentSpec. 주면 clarify()로 만들지 않는다 |
instructions | "" | CLARIFIER_RULES 뒤에 덧붙일 도메인 지시 |
always_ask | False | True = 매번 다시 확인(요구사항을 바꿀 때 사용) |
on_fail | "stop" | 네 단락이 안 갖춰졌을 때의 행선지. Step.on_fail과 동일 |
retries | 0 | 네 단락이 안 갖춰졌을 때 몇 번 재시도할지 |
**spec_kw | — | clarify()로 그대로 전달: can_read / model / effort / max_turns / max_budget_usd |
돌고 나면 ctx에 세 가지가 들어 있다:
ctx["确认需求"] # str, 네 단락의 압축판(prompt_block). 하위 prompt에 바로 삽입한다. 키 이름 = 스텝 이름
ctx[BRIEF_KEY] # "_brief" —— Brief 객체. 단락별로 꺼내 쓰려면 이것
ctx[MISSING_KEY] # "_brief_missing" —— 네 단락이 모자랄 때만 존재: 어느 단락이 빠졌는지, UI 표시용
잘 안 맞을 때 먼저 돌려볼 손잡이들:
| 증상 | 어디를 돌리나 |
|---|---|
| 너무 많이, 너무 잘게 묻는다 | max_asks에 하드 쿼터를 준다. instructions에 당신 도메인에서 자명한 것이 무엇인지 적는다 |
| 너무 적게 묻고 바로 시작한다 | instructions에 반드시 확실히 해야 할 항목을 지목한다(횟수는 기본이 이미 무제한이라 쿼터를 만져도 소용없다) |
| 네 단락을 성의 없이 채운다 | instructions에 당신 도메인의 예시를 하나 준다 |
| 지켜보는 사람이 없는데 멈춰 있다 | timeout_s=0 |
| 매번 다시 확인받고 싶다 | always_ask=True, 또는 브리프 파일을 지운다 |
실제로 하는 일¶
발동 시점: 세 군데 배선, 새 필드는 하나도 없다¶
clarify_step()이 만들어내는 것은 평범한 Step이고, 콜백 세 개를 채워둔 것뿐이다:
| 어디에 걸리나 | 언제 도나 | 무엇을 하나 |
|---|---|---|
Step.when | 이 스텝에 들어가기 전 | 브리프가 이미 있고 네 단락이 다 갖춰져 있으면 건너뛰고, 그것을 ctx에 부어 넣는다 |
Step.gate | 이 스텝이 끝나고 결과를 하위로 넘기기 전 | 네 단락이 다 안 적혔으면 아래로 못 가게 막는다. 다 갖춰졌으면 write()로 얼린다 |
Step.reduce | 통과한 뒤 | 하위로 파싱된 네 단락을 넘긴다. 모델 원문이 아니다 |
건너뛸 때도 ctx를 채운다. 이건 놓치기 쉽다. when이 False를 반환하면 Workflow는 이 스텝을 실행하지 않고, 당연히 ctx[step.name]도 쓰지 않는다 —— 그래서 clarify_step은 when 안에서 기존 브리프를 부어 넣는다. 그러지 않으면 재실행 시 하위가 KeyError를 받는다.
reduce가 넘기는 것은 모델 원문이 아니라 Brief.prompt_block()이다. 원문에는 모델이 더 써놓은 것이 섞여 있을 수 있기 때문이다 (실측에서 코드 전체를 답변에 붙여 넣었다).
연속성 상황에서는 이 스텝의 첫 마디가 바뀐다 —— CLARIFY_RESUME: "아까 다 못 끝낸 요구사항 확인을 이어서 진행한다 —— 처음부터 다시 시작하는 것이 아니다……". 이 문장을 주지 않으면, 연속 실행이 원래 요구를 새 태스크로 다시 보내고, 확인자는 이미 물었던 것을 또 물을 수 있다.
경계: 질의응답은 하위 컨텍스트로 들어가지 않는다¶
clarify_step의 resume_from은 기본값 None을 유지하므로, 다음 스텝은 새 세션이고 브리프만 받는다. 그 질의응답은 코디네이터의 컨텍스트에 한 번도 들어간 적이 없다 —— "들어갔다가 잘려 나간" 것이 아니다. 이 차이는 실질적이다. 잘려 나간 것은 sessions.db에 여전히 남아 있고 resume으로 되돌아올 수 있다. 애초에 들어간 적이 없는 것에는 그런 문제가 없다.
질의응답 자체는 log_path에 append된다. 이 기록은 컨텍스트를 차지하지 않고, compact의 영향을 받지 않으며, 다른 기계로 옮겨도 남아 있다 —— 워크벤치와 같은 발상이다.
네 단락이 각각 한 부류의 실패를 막는다¶
| 단락 | 무엇을 쓰나 | 안 쓰면 어떻게 되나 |
|---|---|---|
| 目标(목표) | 한 문장: 무엇을 만드는지, 누가 쓰는지 | 만들어진 게 다른 물건이 된다 |
| 验收标准(검수 기준) | 판정 가능한 조건, 한 줄에 하나. "잘 됐다"는 안 되고, "x를 돌리면 y가 나온다"여야 한다 | 아무도 "다 됐다"를 판정할 수 없다 |
| 边界(경계) | 명시적으로 하지 않을 것 | 범위가 번진다. 이 단락이 뒤의 모든 subagent를 붙든다 |
| 未知与假设(미지와 가정) | 못 물어본 것, 타임아웃으로 무응답이 된 것, 스스로 추측한 것을 한 줄에 하나 | 잘못된 전제가 조용히 묻힌다 |
네 번째 단락은 long-horizon 실행의 퓨즈다. 앞의 세 단락 중 어느 하나가 틀렸더라도, 가정이 네 번째 단락에 명시되어 있으면 뒤에서 읽는 사람이 막을 기회가 생긴다. 묻혀버리면 몇 시간 뒤 산출물이 전부 무용지물이 될 때에야 알게 된다. 잘못된 전제를 완전히 피할 수는 없지만, 명시적으로 만들 수는 있다.
네 단락이 다 갖춰져야 통과하고, 어느 단락이 빠졌는지는 Brief.missing()이 알려준다 —— 반환값은 중국어 단락명이라 그대로 표시할 수 있다.
파싱은 작성 방식에 매우 관대하다. ## 目标 / **目标** / 目标: / 3. 边界를 모두 인식하고, 제목 뒤에 본문이 바로 이어지는 형태 (目标: 做一个 X)도 인식하며, 흔한 별칭도 인식한다(验收条件→验收标准, 不做什么→边界, 未知项与假设→未知与假设). 같은 단락이 여러 번 나오면 내용이 있는 첫 번째를 취한다. 예외 두 가지는 알아둬야 한다:
Brief.parse()는 먼저 펜스 코드 블록을 벗겨낸다. 닫히지 않은 펜스를 만나면 그 지점부터 뒤를 통째로 버린다. 모델 출력이 잘리면 뒤쪽 단락이 전부 파싱되지 않는다 → 네 단락 불충족 →gate가 되돌려 보낸다.Brief.load()는"(未填)"를 빈 값으로 본다. 브리프를 손으로 편집하면서to_markdown()의 플레이스홀더 문구를 그대로 베껴 두면, 그 단락은 여전히 누락으로 친다.
경계: 확인자가 건드릴 수 있는 것¶
제약 없는 확인자를 한 번 돌려봤다(/tmp/probe_ask.py, $0.8908 / 230초): 질문 두 개를 하고 바로 코드를 쓰기 시작했다. 권한에 막히자, 코드 전체를 답변 본문에 붙여 넣었다. 프롬프트에 "코드를 쓰지 마라"라고 써도 이건 못 막는다 —— 당시 시스템 프롬프트에 이미 비슷한 말이 있었다. 그래서 두 겹의 장치가 있다:
하나, hook 하나가 쓰기 도구를 막는다. clarify()의 무승인 목록은 mcp__human__ask에 (can_read=True일 때) Read / Glob / Grep / WebFetch / WebSearch를 더한 것이고, Write / Edit / Bash / Agent는 없다. 실제로 이걸 집행하는 것은 Runtime이 자동으로 다는 whitelist_guard다. 무승인 목록으로부터 Bash / Write / Edit / NotebookEdit 중 막아야 할 것들을 역으로 계산하고, 걸리면 deny한다. "작업하지 말라고 요청받은" 것이 아니라 작업할 수 없는 것이다.
읽기를 주는 것은 남는 장사다. 저장소를 한 번 훑으면 질문 몇 개를 아낄 수 있고, 이 세션은 쓰고 버리므로 읽어서 더러워져도 상관없다 (can_read=False로 읽기조차 주지 않을 수도 있다).
이것은 반드시 hook이어야 하며, allowed_tools에만 기댈 수는 없다. 후자는 무승인 목록이지 배타적 화이트리스트가 아니다 —— 모델은 목록에 없는 도구도 얼마든지 호출할 수 있다. 아직 유효한 실측 증거 두 가지:
- HT002의 "목표 설정" 단계에 있던 저지는 실제로
Bash를 11번 돌렸다.judge()는 기본값이can_run=False이고 목록에Bash가 아예 없는데도 그랬다 (그 실행에는 아직 이 hook이 없었다 —— 오늘 같은 호출은whitelist_guard가 즉시deny한다. 이것이야말로 그걸 막는 게 목록이 아니라 hook이라는 증거다). - $0.1짜리 프로브:
allowed_tools=["Read"]인 에이전트에게 파일을 쓰게 시켰더니 ——Write는 권한 레이어가 거부했고("requested permissions to write ... but you haven't granted it yet"),Bash는 경로 보안이 거부했다("Output redirection was blocked. For security, Claude Code may only write to files in the allowed working directories"). 호출은 나갔고, 다른 레이어가 막은 것이다.
clarify()는 permission_mode를 명시적으로 설정하지 않고, AgentSpec 기본값인 "default"를 상속한다. coordinator()의 기본값은 "acceptEdits"다 —— 이 값을 확인자에게 그대로 전달하는 순간 그 보호는 사라진다.
둘, 프레임워크는 그 네 단락만 파싱하고 나머지는 전부 버린다. Brief.parse()는 먼저 펜스 코드 블록을 떼어낸 뒤 제목을 찾는다 —— 붙여 넣어도 하위로 들어가지 못한다. 이것이 "하위를 오염시킨다"에 대한 마지막 차단문이다.
경계: 질문 통로¶
모델 쪽의 질문 도구 이름은 mcp__human__ask다(파라미터 question, 선택 options). HumanChannel은 프로세스 내 MCP 서버이고, 등록하는 도구는 두 개다 —— mcp__human__ask와 mcp__human__inbox. 확인자의 무승인 목록에는 앞의 것만 있다 (인박스는 코디네이터용이다).
HumanChannel(
on_event=None, # 푸시형 UI의 콜백. Workflow에 걸면 Workflow.run이 자동으로 연결한다
max_asks=None, # 기본은 횟수 무제한
timeout_s=1800.0, # 30분. None = 영원히 대기; <= 0 = 완전 자동
log_path=None, # 질의응답을 이 파일에 append. 컨텍스트를 차지하지 않는다
amend_path=None, # 실행 도중 사람이 한 말을 이 파일에 append(보통은 브리프 그 자체)
over_budget_text=..., timeout_text=..., declined_text=..., # 세 가지 무응답 상황의 문구
)
long-horizon 에이전트의 평상시 상태는 아무도 지켜보지 않는 것이므로, "멈춰서 사람을 기다린다"는 우아하게 실패할 수 있어야 한다:
| 설정 | 동작 |
|---|---|
timeout_s=1800.0(기본) | 30분 기다린다. 시간이 되면 설명 한 줄을 반환한다. 오류가 아니다 |
timeout_s=None | 영원히 기다린다. 사람이 지키고 있는 게 확실할 때만 쓴다(CLI로는 이 값을 줄 수 없다. --timeout은 float이다) |
timeout_s=0(음수도 동일) | 완전 자동: 모든 질문이 즉시 무응답 처리되고, 기다리는 척하지 않는다 |
max_asks=None(기본) | 횟수 무제한 —— 몇 번 물을지는 확인자가 스스로 판단한다 |
max_asks=N | 하드 쿼터. 초과한 질문 도구는 바로 거절한다. 블로킹도 오류도 없다 |
max_asks=0 | 질문 금지(CI / 무인 실행) |
remaining은 max_asks=None일 때 -1을 반환한다(0도 아니고 무한도 아니다). 터미널은 이에 따라 "앞으로 N번 더 물을 수 있음"을 표시하지 않는다.
타임아웃이 반환하는 원문은 이렇다:
응답하는 사람이 없습니다. 스스로의 판단으로 계속 진행하고, 이 질문과 당신이 채택한 가정을 「未知与假设」 단락에 적으세요. 같은 질문을 반복하지 말고, 여기서 멈추지도 마세요.
세 가지 무응답(타임아웃 / 쿼터 소진 / 사람이 직접 건너뛰기)의 문구는 모두 같은 동작을 가리킨다: 가정을 네 번째 단락에 적어라. 이것이 무인 실행에서도 네 번째 단락에 내용이 남는 이유이고, long-horizon 실행이 계속 굴러갈 수 있는 이유다. 쿼터를 프롬프트에 쓰는 것은 권고이고, 숫자가 통로 안에 있어야 보장이 된다.
실측으로 확인한 메커니즘 사실 하나: 프로세스 내 MCP 도구 핸들러 안에서 외부 future를 await해도 데드락이 나지 않는다 —— 핸들러가 걸려 있는 동안에도 이벤트 루프는 계속 돌고, 다른 태스크든 다른 스레드든 답을 채워 넣을 수 있다. 그래서 answer() / decline()은 웹 백엔드나 TUI 입력 스레드에서 바로 호출해도 된다(내부적으로 loop.call_soon_threadsafe를 탄다). 이것은 평상시이지 예외적 상황이 아니다. UI 콜백이 던진 예외는 ui_errors에 모이고, 실행을 중단시키지 않는다 —— 프런트가 죽었다고 세 시간짜리 작업을 같이 데려가서는 안 된다. 전체 멤버 목록은 Python API를 보라.
max_turns를 작게 잡으면 '분명해질 때까지 묻는다'는 빈말이 된다
clarify()의 max_turns 기본값은 None(무제한)이다. 질문 하나가 곧 한 라운드이므로 —— 16으로 잡으면 "많아야 열몇 개만 물어라"와 같고, 그것도 조용히 적용된다. 통로 쪽 max_asks=None은 여전히 "무제한"이라고 적혀 있어서, 누가 잘랐는지 사람은 알 수 없다. 질문을 풀어주려면 HumanChannel.max_asks와 clarify(max_turns=...) 두 기본값 모두 None으로 남겨둬야 한다.
브리프를 어디에 떨구나: 반드시 걸어둔 그 워크벤치여야 한다¶
워크벤치 인덱스는 system prompt에 주입되므로, 코디네이터는 시작하자마자 요구사항 파일이 어디 있는지 알고, 작업을 파견할 때 경로만 넘겨주면 된다. 내용을 태스크 브리프에 베껴 넣을 필요가 없다.
인덱스는 코디네이터까지만 간다
subagent는 자기 자신의 system prompt를 가지며, 세션 수준의 그 부분을 상속받지 못한다(실측 $0.2461, tests/prelude_live.py). 그러니 "코디네이터가 경로를 전달"하는 것이지, "모든 subagent가 자동으로 안다"가 아니다.
핵심은 어느 워크벤치냐다. 맞는 작성법은 하나뿐이다. 직접 만들고, Workflow에 걸고, 드라이버가 같은 객체를 Runtime에 넘기게 하는 것.
wb = Workbench(Path.cwd()).ensure()
wf = Workflow(channel=ch, workbench=wb, steps=[ # ← 걸어둔다
clarify_step(ch, brief_path=wb.notes / "需求.md", prompt="…"),
...,
])
틀린 방식은 두 가지이고, 둘 다 오류를 내지 않으므로 특히 조심해야 한다:
# ✗ 경로를 직접 조립: 프로세스 cwd 기준이라, Runtime(workbench=True)가 만드는 <run_dir>/workbench 와는
# 서로 다른 디렉터리다. 브리프는 A에 쓰이고 주입되는 인덱스는 B를 훑는다 —— 위의 그 약속이 조용히 깨진다.
clarify_step(ch, brief_path=Path(".flower/notes/需求.md"), prompt="…")
# ✗ Runtime 에서 거꾸로 가져오기: cli.py 경유로는 불가능하다. 먼저 main()을 호출해 Workflow를 만들고,
# 그 다음에야 Runtime을 만든다 —— 그때는 brief_path가 이미 확정된 뒤다.
rt = Runtime(workspace="repo", workbench=True); wb = rt.workbench
드라이버를 직접 작성할 때(cli.py를 거치지 않을 때)는 먼저 Workbench를 만들고, 같은 객체를 Workflow(workbench=wb)와 Runtime(workbench=wb)에 동시에 넘긴다. tests/trial_offline.py의 5번 항목은 "브리프가 prompt_block()에 나타난다"를 직접 단언하고, 11번 항목은 그 단언이 회귀를 잡아낸다는 것을 확인한다.
어떤 검증은 돌았고 어떤 것은 안 돌았나¶
오프라인 전부 통과(tests/clarify.py, 52개 항목, 돈이 들지 않음): 질문 통로의 다섯 가지 시맨틱(답을 블로킹 대기 / 쿼터 소진 / 타임아웃 무응답 / 건너뛰기 / 스레드 간 응답), 네 단락 파싱("코드를 붙여 넣은" 샘플 포함), clarify() 역할에 쓰기 도구가 없다는 것, clarify_step의 세 군데 배선.
CLI 경로 오프라인 관통 확인: 완전한 브리프를 미리 놓아둠 → 첫 스텝 건너뜀 → 통로가 표준입력 스레드에 자동 연결 → 브리프가 ctx에 부어짐 → 깨끗하게 종료.
실제 API로는 돌리지 않았다. $0.8908짜리 그 프로브는 실제 요청이었지만, 그것이 측정한 것은 "제약 없는 확인자가 무엇을 하는가"이지 지금의 이 경로가 아니다.
언제 쓰지 말아야 하나¶
요구사항이 이미 얼어붙은 물건일 때. 요구사항이 파일에 적혀 있거나, 상위 시스템이 정해주거나, 이번이 같은 일의 재실행이라면 —— 물을 것이 없다. 요구사항 텍스트를 실제 작업 스텝에 바로 먹이거나, clarify_step을 남겨두고 when으로 건너뛰게 두면 된다 (브리프가 있으면 원래 묻지 않는다).
물을 사람이 없고, 스스로 추측하게 하고 싶지도 않을 때. timeout_s=0이면 모든 질문이 즉시 무응답 처리되고, 네 번째 단락은 그것이 스스로 만든 가정으로 채워진다 —— 설계상 그렇지만, 그 브리프의 신뢰도는 그 가정들의 신뢰도와 같아진다. CI에서 더 깔끔한 방법은 max_asks=0(질문 명시적 금지)이고, 요구사항은 외부에서 전부 주는 것이다.
한 번짜리 작은 일. 확인 스텝 자체가 돈이 든다. HT002에서 "저장소를 clone 해서 macOS에 설치하고 돌아가게 한다" 정도의 일에, 요구사항 확인에 $0.5306 / 9 라운드 / 0.10h가 들었다. 일이 작을수록 이 스텝이 차지하는 비율은 보기 흉해진다. flower once 같은 단일 에이전트 경로에는 이 스텝이 없다.
요구사항을 바꿀 때 다시 대화해서는 안 된다. 브리프는 얼어붙은 물건이고, 디스크에 떨어진 순간부터 요구사항은 파일이 기준이다 —— 올바른 방법은 그 파일을 고치는 것이다. --clarify-only는 이미 확인이 끝난 디렉터리에서는 아무 일도 하지 않는다 (그 워크플로에는 이 스텝밖에 없는데, 이 스텝이 건너뛰어지기 때문이다). 다시 확인받으려면 --new를 같이 주거나, 직접 배선할 때 always_ask=True를 준다.
이것은 "다 됐는지"를 판정하지 않는다. 그건 다른 레이어이고, 목표 가드를 보라. 사전 확인이 막는 것은 "만들어진 게 원하던 게 아니다"이지, "다 됐다고 말했는데 사실은 안 됐다"는 막지 못한다.