コンテンツにスキップ

事前確認

手を動かす前に要件を問い詰めておく。確認者は質問だけをして手は動かさない役割で、 はっきりするまで問い続け、最後にちょうど四セクションの要件確認書を出力し、 ディスクに凍結する。以降のすべてのステップはこの文書を読んで始まり、要件を推測し直すことはない —— そしてその問答は下流のコンテキストに一度も入らない

何を解決するのか

flower がコンテキストを掃除する手段が消しているのは、どれも現場だ:期限切れ、拒否された呼び出しの除去、エラーメッセージの除去、 大きな結果のスピル。現場は捨てても構わない、もう一度走らせれば手に入る。

そうではない種類の誤りがある:目標の理解を間違えている場合だ。これは唯一、コンテキストを掃除すると悪化する種類の誤りである。現場が捨てられたあと、 残るのはまさにその誤った前提の上に建てられた判断であり、しかもそれは正しい判断とまったく同じに見える —— 前提が疑わしいことを示す痕跡はどこにも残らない。

ロングホライズンはこれを最悪の形に増幅する:誤った前提のまま数時間走り、 十数個の subagent を派遣し、ディスクに成果物を積み上げ、そのあとで露見する。 その時点で高くつくのは token ではない、すべての成果物が間違った要件に沿って作られていることだ。 HT001 の会計はこの比率を測れる:要件を確認するステップが $0.3704 / 5 ターン / 0.06h、 そのあと作業するステップが $171.2476 / 31 ターン / 10.44h

だから「止まって聞く」ための通路が必要で、しかもそれは着手のになければならない。

使い方(最小コード)

コードゼロ:コマンドライン

プロジェクトのディレクトリに入って、そのまま走らせる:

cd /path/to/your/project
flower
要做什么? 一句话就够,回车开始(Ctrl-C 退出)
> 帮我做一个 X

ターミナルにはこんな質問が出る:

  ? 这个工具是给命令行用,还是要有 Web 界面?
     1) 纯命令行
     2) Web 界面
     3) 两个都要
你的回答 (回车=跳过,让它自己判断) > 1
  • 番号を打って選択肢を選ぶか、そのまま文章で答える
  • Enter = この質問をスキップ、自分で判断させて仮定を「未知と仮定」に書かせる
  • 四セクションが揃って初めて通過、確認書は .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 に注ぐ。 ここは見落としやすい:whenFalse を返すと Workflow はこのステップを実行せず、 当然 ctx[step.name] も書かれない —— だから clarify_stepwhen の中で既存の確認書を注いでいる。 そうしないと再実行時に下流が KeyError を受け取る。

reduce が渡すのは Brief.prompt_block() であってモデルの原文ではない。原文には余計に書いたものが混ざりうるからだ (実測ではコード全体を返答に貼り付けてきた)。

継続のときは、このステップは別の一言で口を開く —— CLARIFY_RESUME: 「さっき途中までだった要件確認の続き —— やり直しではない……」。この一言を与えないと、 継続時に素の要求を新しいタスクとして送り直してしまい、確認者は既に聞いたことをもう一度聞きかねない。

境界:問答は下流のコンテキストに入らない

要件確認        独立セッション  ────→  ディスク上に凍結された四セクションの確認書
作業(次ステップ)  新セッション(resume_from=None)◄─┘   渡るのはその四セクションだけ

clarify_stepresume_from はデフォルトの None のままなので、次のステップは新しいセッションであり、確認書しか受け取らない。 その問答はコーディネーターのコンテキストに一度も入っていない —— 「入ったあとで刈られた」のではない。この差は実質的だ:刈られたものは sessions.db にまだ残っており、 resume で戻ってくる可能性がある。一度も入っていないものにはその問題がない。

問答そのものは log_path に追記される。この一部はコンテキストを食わず、compact の影響も受けず、マシンを変えても残る —— ワークベンチと同じ発想だ。

四セクションはそれぞれ別種の失敗を止める

セクション 何を書くか 書かないとどうなるか
目標 一文で:何を作るのか、誰のためか 出来上がるのが別物になる
受け入れ基準 判定可能な条件、一行一つ。「ちゃんと動く」は不可、「x を走らせて y が出る」なら可 誰も「完成した」と判定できない
境界 何をやらないかを明示する スコープが広がる。このセクションが後続のすべての subagent を縛る
未知と仮定 聞けなかったこと、タイムアウトで空振りしたこと、自分で推測したこと、一行一つ 誤った前提が黙って埋められる

第四セクションはロングホライズン実行のヒューズだ。前の三つのどれを書き間違えても、仮定が第四セクションに明示されてさえいれば、後から読んだ人が止められる可能性がある。 埋められてしまえば、数時間後に成果物が全部無駄になって初めて気づくことになる。誤った前提を完全に避けることはできないが、明示的にはできる。

四セクションが揃って初めて通過し、どのセクションが欠けているかは 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"] の agent にファイルを書かせてみる —— 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 server で、登録しているのは二つのツール —— mcp__human__askmcp__human__inbox。確認者の事前承認不要リストには前者しか入っていない (受信箱はコーディネーター向けだ)。

HumanChannel(
    on_event=None,        # プッシュ型 UI 向けコールバック。Workflow に渡せば Workflow.run が自動で繋ぐ
    max_asks=None,        # デフォルトは回数無制限
    timeout_s=1800.0,     # 30 分。None = 永遠に待つ;<= 0 = 全自動
    log_path=None,        # 問答をこのファイルに追記する、コンテキストは食わない
    amend_path=None,      # 実行途中に人が言ったことをこのファイルに追記する(通常は確認書そのもの)
    over_budget_text=..., timeout_text=..., declined_text=...,   # 三種類の空振りの言い回し
)

ロングホライズン agent の常態は誰も見ていないことだ。だから「止まって人を待つ」は優雅に失敗できなければならない:

設定 挙動
timeout_s=1800.0(デフォルト) 30 分待つ。時間切れで説明文を一つ返す、エラーではない
timeout_s=None 永遠に待つ。人が確実に張り付いているときだけ使う(CLI からはこの値を与えられない、--timeout は float)
timeout_s=0(負数も同じ) 全自動:すべての質問が即座に空振りする、待つふりをしない
max_asks=None(デフォルト) 回数無制限 —— 何回聞くかは確認者が自分で判断する
max_asks=N 硬い上限。超過分の質問ツールはその場で断られる、ブロックもエラーもしない
max_asks=0 質問禁止(CI / 無人運用)

remainingmax_asks=None のとき -1 を返す(0 でも無限大でもない)。ターミナルはこれを見て「あと N 回聞ける」を表示しない。

タイムアウト時に返る文言はこうだ:

応答なし。自分の判断で続行し、この質問と採用した仮定を「未知と仮定」のセクションに書き込むこと。 質問を繰り返さず、ここで止まらないこと。

三種類の空振り(タイムアウト / 上限使い切り / 人が自分でスキップ)の文言は、どれも同じ動作を指している:仮定を第四セクションに書け。 これが無人運用でも第四セクションに中身が残る理由であり、ロングホライズンな実行が走り続けられる理由でもある。 上限をプロンプトに書くのは助言であって、チャネルの中で数えて初めて保証になる。

実測済みの機構上の事実が一つ:プロセス内 MCP ツールハンドラの中で外部の future を await してもデッドロックしない —— ハンドラが止まっている間もイベントループは回り続け、別のタスクからでも別のスレッドからでも答えを入れられる。 だから answer() / decline() は Web バックエンドや TUI の入力スレッドから直接呼べる(内部では loop.call_soon_threadsafe を通る)。これは常態であって例外的なケースではない。UI コールバックが投げた例外は ui_errors に溜まり、 実行を中断しない —— フロントエンドが落ちたからといって三時間の作業を道連れにすべきではない。メンバの一覧は Python API を参照。

max_turns を小さくすると、「はっきりするまで問い続ける」は空文句になる

clarify()max_turns はデフォルト None(無制限)だ。質問を一つするごとに 1 ターンなので、 16 にすれば「最大でも十数個しか聞けない」ことになる。しかもそれは黙って効く:チャネル側の max_asks=None は相変わらず「回数無制限」と書いてあり、誰が絞ったのか人には見えない。質問を開放したいなら、HumanChannel.max_asksclarify(max_turns=...)両方のデフォルトを None のままにしておくこと。

確認書の置き場所:渡したそのワークベンチでなければならない

ワークベンチのインデックスは system prompt に注入されるので、コーディネーターは開始時点で要件ファイルの場所を知っている。作業を振るときはパスを渡せばよく、 中身をタスクブリーフに書き写す必要はない。

インデックスはコーディネーターまでしか届かない

subagent は自分の system prompt を持っており、セッションレベルのその部分を継承しない(実測 $0.2461tests/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.py52 項目、費用ゼロ):質問チャネルの五つの意味論(ブロックして答えを待つ / 上限使い切り / タイムアウト空振り / スキップ / スレッドをまたいだ回答)、四セクションのパース(「コードを貼ってきた」サンプルを含む)、 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 のような単一 agent の経路にはこのステップが無い。

要件を変えるときに対話をやり直すべきではない。 確認書は凍結物であり、ディスクに落ちた瞬間から要件はファイルが正である —— 正しいやり方はそのファイルを直すことだ。--clarify-only は既に確認済みのディレクトリでは何もしない (そのワークフローはこのステップしか持たず、そのステップはスキップされる)。確認をやり直すには --new を併用するか、 自分で配線している場合は always_ask=True を与える。

「終わったかどうか」は判定しない。 それは別の層で、ゴールガードを参照。事前確認が止めるのは 「作ったものが欲しかったものと違う」ことで、「終わったと言っているが実は終わっていない」ことは止められない。