コンテンツにスキップ

ゴール看守

「終わったのかどうか」を執行者に言わせない。判定者は目標を定めるだけ、 判定するだけ、手は動かさない役割である。走り出す前に需要確認書を判定可能な チェックリストへ変換し、以降は毎ラウンドの作業が終わるたびに独立に一度判定して、判定を出す —— 達成なら先へ進み、未達成なら「どこが足りないか」を添えて差し戻し、 達成不能と判定したら止まって人に聞く。

何を解決するのか

前置確認が防ぐのは「作ったものが欲しかったものと違う」である。この層が防ぐのは別の種類だ: 「実は終わっていないのに、本人は終わったと言っている」。この二つは分けなければならない。失敗の仕方が違うからだ:

失敗はどう見えるか いつ露見するか
要求が誤り あらゆる成果物が誤った要求に沿って作られている 数時間後、成果物が全部無駄になる
完了度の判断が誤り 半分しか走っていないテスト、一箇所直して三箇所漏れ、「たぶん問題ない」 自分で使おうとしたとき

二つ目をなぜ執行者自身に任せられないのか:体系的な楽観バイアスがあるからだ。 不誠実なのではない —— 自分の死角が見えていないのである。自分が何をやったかは知っているが、何を漏らしたかは知らない。

だから判定は作業に参加しておらず、自分のセッションで走る役割に渡す。 見えるのは目標と現場だけで、執行者が何回試したか、どれだけ苦労したかは知らない。だから代わりに言い訳を探すこともない。 確認者が独立セッションで走るのと同じ理屈である。

どう使うか(最小コード)

コードなし:コマンドライン

flower                      # 默认就带目标看守
flower --no-goal            # 关掉:干活跑完就算完
flower --rounds 5           # 最多五轮活(默认 3)
flower --judge-can-run      # 让判定者能跑命令(判定更硬)

自分で配線する

二つの関数が半分ずつ受け持つ。混同しないこと:goal_step()目標を設定する(独立したステップ)、 with_goal() のほうが判定ループである(作業のステップを包む)。

from pathlib import Path
from flower import HumanChannel, Step, Workbench, Workflow, clarify_step, goal_step, with_goal

wb = Workbench(Path.cwd()).ensure()
ch = HumanChannel(log_path=wb.notes / "问答记录.md")   # デフォルトでは質問回数に上限なし
goal_path = wb.notes / "目标.md"

work = Step("干活", spec=协调者, prompt=lambda ctx: f"照这个做:\n{ctx['确认需求']}")

wf = Workflow(channel=ch, workbench=wb, steps=[
    clarify_step(ch, brief_path=wb.notes / "需求.md", prompt="帮我做一个 X"),
    goal_step(ch, goal_path=goal_path),
    with_goal(work, ch, goal_path=goal_path, rounds=3),
])

goal_step(channel, *, goal_path, ...):

引数 デフォルト 説明
goal_path 目標をどこに置くか。ワークベンチnotes/ 配下に置く。理由は確認書と同じ
brief_key "确认需求" ctx のどのキーから確認書を読むか。読めなければ "(没有确认书)" しか得られない
name "设定目标" ステップ名であり、ctx 内のキー名でもある
spec / instructions None / "" 自前の AgentSpec、または判定者へのドメイン指示の追加
always_set False True = 毎回設定し直す
on_fail / retries "stop" / 0 Step と同じ
**spec_kw judge() へ透過:can_run / model / effort / max_turns / max_budget_usd

with_goal() は作業のステップを判定つきループに包む:

with_goal(step, channel, *, goal_path, spec=None, rounds=3,
          instructions="", can_run=False, name=None, **spec_kw)

rounds は総ラウンド数であって追加ラウンド数ではない —— 実装上は retries = max(0, rounds - 1) になるので、 rounds=3 は最大三ラウンド、rounds=1 は「一ラウンド走って一度判定し、通らなければ失敗」である。 完全なシグネチャとフィールドの意味は Python API を参照。

ctx にはキーが三つ増える:

ctx[GOAL_KEY]     # "_goal" —— Goal 对象;ctx["设定目标"] 是它的 markdown
ctx[VERDICT_KEY]  # "_verdict" —— 最近一次 Verdict,给 UI 用
ctx[ROUND_KEY]    # "_goal_rounds" —— 跑了几轮

判定者は ctx["_runtime"] 経由で送り出される —— Workflow.run がランタイムとイベント出口を両方 ctx に入れるので、 gate は自分で agent を起こせるし、判定の過程もそのまま UI に流れる (でなければその十数秒は画面が真っ暗で、固まったように見える)。

うまく回らないときは、まずこのつまみを回す:

症状 どれを回すか
判定が甘く、達成と言うが実際は未達成 --judge-can-run で本当に一度走らせる。または instructions にドメイン判定基準を足す
判定が厳しすぎて差し戻され続ける 目标.md の判定リストが要求より高く書かれていないか見る。そのファイルを直す
ラウンドが空回りする 判定者が「達成不能」を出すべきところで「未達成」を出している。何を達成不能とみなすかを指示に足す
高すぎる --rounds 1、または --no-goal で完全に切る
中断されたくない --timeout 0:達成不能でも人に聞かず、そのまま停止(理由はディスクに残る)

実際に何をしているのか

目標はどんな形か

goal_step は確認書を読み、二つのセクションを出力して .flower/notes/目标.md に凍結する:

# 目标
让 conv.py 能把 md 转成 html。

# 判定清单
- `python conv.py a.md` 产出 a.html
- 输出里含 `<h1>`
- 列表被转成 `<ul><li>`

判定リストがこの層の価値のすべてである。 「実装が完全」は判定できない。「何を走らせて、何が見えるか」なら判定できる。 リストは確認書の「受け入れ基準」から来るが、一条ずつその場で検証できる形に書き換える必要がある —— 曖昧なものは判定者が補う。 両方のセクションが空でない(statement に中身があり、checks が空でない)ときだけ揃ったとみなし、そうでなければこのステップは通さない。

リストの長さは「失敗の仕方が何通りあるか」で決まる

判定者がどれだけ厳密かで決まるのではない。git clone && make && ./app のようなタスクなら、三〜五条で足りる: ビルドが通る、起動する、使える。

実測で失敗した例(HT002):「リポジトリを入れて動かす」という一件のタスクのリストが 15 条になり、 そのうち「使えるかどうか」を検証していたのは 5 条だけ、6 条は「手順を守ったか」の検証 (~/.zshrc の mtime を調べる、.flower/ ディレクトリが変更されていないか調べる —— それはフレームワーク自身のディレクトリである)、 4 条は原理的に検証不能だった。

境界は判定項ではない

あの回の主因はこれである:

何を制約するか どう守るか
境界 どう作業するか(「プロジェクトディレクトリ内にだけ入れる」「業務コードは触らない」) 越えないことで守る。事後の自己証明ではない
判定項 提出されたもの(「動いたか」「結果は正しいか」) その場の検証で確かめる

brew install を走らせていない」を判定項に書くのは、境界を一つ足すたびにチェックが一つ増えるということだ —— そして境界こそ、確認フェーズで書き尽くすよう促されるものである。どうしても説明が要るなら一文で済ませ、六条に分解しない。

検証できない項目は、目標設定の時点で警告される

リスト中に [此环境无法验证:原因] と印がついた項目について、goal_step目標を凍結したその瞬間にリマインダを一本出す:

  # 目标里有 4/15 条在这个环境里验不了 —— 判定时它们必然过不去,会停下来问你。
    现在改 .flower/notes/目标.md 还来得及:
      · 界面截图并实际看图 [此环境无法验证:屏幕录制未授权]
      · ...

なぜ前倒しするのか:これらの項目の運命は目標設定の瞬間に決まっており、判定時には必ず通らない。 HT002 では先に $35.90 の作業 + $1.40 の判定を費やしてから気づいた —— 発見を目標設定のステップまで前倒しすれば、同じ情報のコストは $37 から $0 に下がる。

警告するだけで、止めはしない:人はそのまま走らせることを選べる(HT002 は最終的に「この結果を受け入れる」を選んだ)。 Goal.unverifiable がこの一覧で、イベントの payload には UI 用の構造化データが入っている。

結論は三つであって二つではない

干活 ──> 判定 ──达成────> 往下走
              ├─未达成──> 打回,带上“差在哪”,续跑同一个会话接着做
              └─无法达成─> 停下来问人:接受 / 改目标 / 你判断错了

三つ目の結論が肝である。「達成/未達成」しかないと、実際には達成不能な目標のせいで協調者が ラウンドを空回りさせ、予算が尽きるまで走ってしまう —— それこそ本当に金を燃やす。だから判定者には明確にこう求めている: 「もう一ラウンド回しても無駄」なものだけを達成不能と呼べ(必要な外部条件が欠けている、要求が自己矛盾している、判定項がそもそも検証不能)。 単に「まだ終わっていない」だけなら未達成である。

達成不能のとき、フレームワークは止まって人に聞く:

  ? 目标被判为**无法达成**:缺少 X 依赖,判定项 2 无法验证
    怎么办?
     1) 接受这个结果,就这样往下走
     2) 修改目标
     3) 你判断错了,继续做
  • 受け入れる → このステップは通ったことになり、理由は記録に残る
  • 目標を修正する → 新しい目標を続けて聞き、元の目標の後ろに追記して(何を変えたかが見える)、もう一ラウンド
  • 判断が間違っている(および自分で打ち込んだ任意の自由回答)→ その言い分を添えて差し戻し、もう一ラウンド

誰も応答しないときは止まる。空回りを続けたりはしない —— これは意図的である。達成不能と判定され、聞ける人もいないなら、 走り続けることはラウンドごとに金を燃やすだけで、それこそ最も避けるべきことだ。停止時に投げるのは StepAbort、理由は ctx["_aborted"] に書かれ、目標ファイルも runs/manifest.json も残る。人が戻ってきてから決めればいい。

「できていない」と「ここでは検証できない」は別の結論

Verdict の三つの値は ACHIEVED / NOT_YET / UNREACHABLE である。 UNREACHABLE を通過扱いにするのは絶対に許されない —— これは「止まって人に聞く」経路であって、「もう一ラウンド」ではない。 判定者が書いた「无法验证 / 没法验证 / 验证不了 / 无法判定 / unverifiable」はすべて UNREACHABLE に寄せる。「ここでは検証できない」を「達成」とみなすのは、「たぶん大丈夫そう」の一言で仕事を締めるのと同じであり、 「未達成」とみなすのは、そもそも検証できないことをラウンドごとにやり直させることである。

判定が曖昧 = 未達成

Verdict.parse の判別順序:まず見出しセクションから「結論 / 判定」の段を取る。見出しセクションがない場合、 全体が 1 / true なら達成、0 / false なら未達成(判定者に「0/1 だけ返せ」と求めたとき、 本当に数字一つしか返ってこないことは十分ありうる)。それでも当たらなければ結論テキスト中のキーワードを探す(長い語を先に)。最後に孤立した 1 / 0 を探す。

どれも当たらないときは state を空のままにし、okFalse、フレームワークは未達成として扱う。 これは意図的である: 「判定できない」と「終わった」は別物であり、曖昧なものは一律で未達成とし、デフォルトの理由を一文添える (「判定者没给出明确结论,按未达成处理」)。

判定するのは成果物であって、ソースコードではない

ソースコードしか読まない判定では成果物を判定できない

HT001 では、受け入れ基準の原文は「単体で実行できるバイナリをビルドし、macOS の ターミナルで直接実行する」だったが、判定は Makefile:25-38 に確かに Darwin 分岐があることだけを読んで通過とした —— 納品された成果物は ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux だった。

判定を誤ったのはゴール看守ではない:あの運行にはまだこの仕組みがなく、この項目を判定したのは協調者が自分で その場に送り出した独立監査役だった。だがゴール看守に置き換えても同じく見逃す —— 判定者はデフォルトで can_run=False、 手元にあるのは Read / Glob / Grep だけで、file を走らせられない。結局同じく Makefile を読みにいき、同じく Darwin 分岐を見て達成と判定する。この失敗の要点は「誰が判定するか」ではなく、「何を証拠に判定するか」にある。

この教訓は JUDGE_RULES に書き込まれた:判定するのは成果物であり、「ソースに macOS 分岐があるのだから 動くはずだ」といった推論は受け付けない。

HT002judge_can_run を有効にし、判定者が実際に file / lsof を走らせた回である。 だからこの落とし穴を避けられた —— 第一声は「あの返答を見て結論は出さない。現場へ行く。」だった。そして:

file cppide        → Mach-O 64-bit executable arm64
lsof -p 96040      → 起于 16:10,16:15 仍活着

判定 prompt の中のあの一文も同じ意味だ:自分で現場を見て、判定リストと一条ずつ突き合わせ、証拠が見えない判定項は 通っていない

「差し戻し」は続きをやることであって、やり直しではない

差し戻しは Step.on_reject を使う:次のラウンドは否決されたばかりのそのセッションを resume し、prompt を判定フィードバックに差し替える (Verdict.feedback() は「どこが足りないか」だけを渡し、解決策は渡さない)。だから済ませた作業、読んだファイル、通った回り道は すべてコンテキストに残っており、差分を埋めるだけでよい。

違いはステップ名に書かれ、runs/manifest.json で一目でわかる:

干活            第一轮
干活#round2     被打回后接着做      ← on_reject 生效,resume 上一轮
干活#retry1     普通重试(重头跑)    ← 没有 on_reject 时的老行为

判定者自身は常に新しいセッションである:with_goal の gate は Runtime.run を直接呼び、resume を渡さない。 ステップ名にはラウンドが付き(干活·判定#1)、サフィックス付きの名前はプロセス間の血縁には入らない。 gate で ctx["_runtime"] を取れない場合は StepAbort を投げ、通過を装わない

スキップと再設定

目標ファイルが既に存在し、内容が揃っているときこのステップはスキップされる(確認書と同じ)—— 長距離の 走行が落ちて再起動したとき、前の結論をもう一度計算し直すべきではない。設定し直したければそのファイルを消すか、always_set=True にする。

例外:ウェイク時にもう一言足したとき。 その一言は確認書に追記されるので、このステップは再導出される (always_set=True)。再導出しなければ、判定者が読むのは凍結された古いリストのままで、新しく足した件が終わっているかどうかは そもそも判定に入らない —— 古いリストで「達成」と判定してしまう。再導出のコストは実測で $0.41 / 3 分継続を参照。

判定者はコマンドを走らせられるか

デフォルトでは走らせられないjudge() の承認不要リストは質問ツールに Read / Glob / Grep を加えたもので、 can_run=True のときだけ Bash が加わる。トレードオフ:

  • Bash を与える(CLI の --judge-can-run)→ 受け入れコマンドを本当に走らせられ、判定が硬くなる
  • だがワークスペースを変更できるようにもなる → 「ついでに直して」から通過と判定しかねず、そうなるとその判定は無意味になる

確認者と同じく、Write / Edit / Agent はない。これを実行しているのは whitelist_guard という hook であって、 allowed_tools ではない —— 後者は承認不要リストであって排他的なホワイトリストではなく、モデルはそこにないツールも普通に呼べる。 この二点が今も成り立つ実測証拠:HT002 では「设定目标」のあの判定者が 11 回 Bash を走らせたが、 そのときの承認不要リストには Bash は入っていなかった。また $0.1 のプローブでは、 allowed_tools=["Read"] の agent が普通に WriteBash の呼び出しを出し、止めたのは権限層とパス安全のほうだった ("requested permissions to write ... but you haven't granted it yet" / "Output redirection was blocked...")。今日ではこの二種類の呼び出しは hook がその場で deny する —— 止めているのは hook であって、リストではない

目標を設定するあの判定者はデフォルトで Bash を持たない

goal_step() には can_run の仮引数がなく**spec_kw を通すしかない:goal_step(ch, goal_path=…, can_run=True)。 明示的に渡さなければ Bash はなく、JUDGE_RULES にある「まず uname -a で自分がどこにいるか確かめろ」は実行できない —— その結果、このマシンではそもそも検証できないリストを書いてよこすことがある。with_goal() は別の話で、 こちらには独立した can_run 仮引数がある(デフォルト False)。

なぜラウンド数に上限があり、質問回数にはないのか

質問はほぼ金がかからないが、一ラウンドの作業は本物の金である。だから:

  • 質問は回数無制限(max_asks=None)—— はっきりするまで聞く。判断は確認者自身に任せる
  • ラウンド数には上限(rounds=3)—— ただし本当の歯止めはこの数字ではなく、三つ目の結論「達成不能」である: これが出た時点で止まって人に聞くので、ラウンドを使い切るまで待たない

使うべきでないとき

判定のほうが作業より冗長になるほど小さいタスク。 この層は単純な問題を複雑にする。しかも実測済みだ: HT002 の「リポジトリを clone して、macOS 上でインストールして動かす」という件では、判定リストが 15 条になり、 うち 6 条は手順遵守の検証、4 条は原理的に検証不能だった。あのラウンドの判定自体に $1.4037 / 37 ラウンド / 0.09h かかり、 運行全体では $38.2409 / 0.97h だった。タスクの失敗の仕方がもともと二〜三通りしかないなら、--no-goal のほうが割に合う。

目標を判定可能なリストに書き下せない。 探索的な作業(「このリポジトリがだいたい何なのか見てくる」)には「終わった」の判定基準がなく、 無理に目標を設定しても、綺麗だが判定できないリストが得られるだけである。この種の作業は flower once を使うか、--no-goal にする。

肝心の判定項がこの環境では検証できない。 判定者はデフォルトで can_run=False、ツールは Read / Glob / Grep だけ —— file を走らせられず、ソースを読むことしかできない。HT001 の 「macOS のターミナルで直接実行する」という受け入れ基準では、運行全体が Linux コンテナ内にあった: コンテナ内で macOS バイナリを検証できる判定者は存在しない。自己審査だろうと独立だろうと同じである。 --judge-can-run は一部を救える(少なくとも file は走る)が、救えない部分は目標設定の時点で [此环境无法验证:…] と印を付け、「止まって人に聞く」経路に乗せるべきであって、判定者が賢くなることに期待すべきではない。

無人運用かつ中断が許されない。 達成不能と判定され、しかも誰も応答しないとき、このステップは止まりワークフロー全体がそこで終わる。 「とにかく最後まで走らせたい」なら --no-goal。「止めたいが待たせたくない」なら --timeout 0 —— 質問は即座に空振りし、理由はディスクに書かれる。

要求が正しいかどうかは面倒を見ない。 判定リストは確認書から導かれる。確認書が間違っていれば、判定は間違ったことを正確に検証するだけだ。 それは前置確認の層の仕事である。