deterministic-task-protocol リポから miyabi-cli-standalone に統合: - docs/dtp/: PLAYBOOK, PLAN, UML, GIT-RULES, Codex レビュー 3件 - autorun/: Phase 0-8 の TASKS/ASSIGNMENT/GATE + INDEX/HANDOFF/ROLLBACK - project_memory/tasks.json: 全9 Phase の DAG SSOT - skills/: polaris-ops, rust-llm-pitfalls - .codex/instructions.md: Codex 設定 実装は miyabi-core に gate.rs, lock.rs, protocol.rs, store.rs を追加する方針。 既存の dag.rs, github.rs, approval.rs 等は変更不要。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 KiB
State Machine Gates Correctness Review
対象:
- Plan:
/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md - State machine:
/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/state/task-state-machine.ts - Types:
/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/types/task.ts
レビュー観点:
- 9つの GATE が十分か
- LLM が手順を飛ばせる抜け道があるか
- 既存 27 遷移ルールと整合するか
- 追加 state が必要か
- TTL 7200s の妥当性
- 複数マシンでの file lock race
Findings
1. Critical: 現行 state machine は conditions を実評価していないため、plan の GATE を state machine へ載せても素通りする
TaskStateMachine.canTransition() は from -> to の組み合わせしか見ておらず、conditions は一切評価していません。transition() と applyTransition() も同様です。したがって plan の「GATE を state machine の gate として実装」という意図に対して、既存実装を「変更なし」で使う案は成立しません。
根拠:
conditionsが定義されている:task-state-machine.ts:34-80canTransition()はr.to === toしか見ない:task-state-machine.ts:103-106applyTransition()もtransition()の結果だけで更新 task を返す:task-state-machine.ts:149-189
影響:
skip_analysis,dependency_blocked,review_approved,requires_deployment,no_deploymentなど既存 27 ルール側の条件文字列も現在は強制力ゼロです。- plan 側で 9 GATE を追加しても、別経路から
applyTransition(task, 'done', ...)を呼べば通ります。
必要修正:
- state machine 自体に
canTransition(task, to, context)を追加して条件評価を内包する - もしくは protocol を唯一の遷移窓口にして、
TaskStateMachineを外から直接呼べないようにする
2. Critical: 既存呼び出し元は applyTransition() の戻り値 task を保存しておらず、状態遷移そのものが永続化されない
現行コードでは applyTransition() は新しい task を返しますが、主要呼び出し元がそれを map に戻していません。つまり transition が valid でも、実 task は更新されない経路があります。
根拠:
TaskManager.updateTaskState()はresult.validを返すだけ:task-manager.ts:148-158TaskExecutor.execute()でもtransitionResult.taskを採用していない:task-executor.ts:107-126,157-177,186-187
影響:
- plan の gate を増やす前に、現行 state progression が fact になっていない
- 「JSON の状態遷移だけが事実」という North Star と真逆
必要修正:
- 全遷移を store-backed immutable update に統一
applyTransitionで返した task を即 store に commit できないなら、その API を使わせない
3. High: 9 GATE は「必要証跡」は見ているが、「順序の完全性」をまだ保証していない
現行 9 GATE は以下の点で不足があります。
- Step 3 は
impact !== nullだけで、impact が対象 branch / commit / file set に対する最新分析かを検証していない - Step 4 は
lock !== nullを見るが、「lock を取った主体」と「今実行している agent」が一致するかを見ていない - Step 5 は
branchNameの書式だけで、実際に remote / worktree 上に存在するかを見ていない - Step 6 は
prNumber > 0だけで、PR がその branch に紐づくか、Draft/Ready/Closed/Merged のどれかを見ていない - Step 7 は
mergeCommitの SHA 形式だけで、対象 PR が merged 済みか、merge commit がその PR/head を含むかを見ていない - Step 8 は
Issue Closedを見るが、close reason や linked PR による close か、人手クローズかを区別しない
LLM の飛ばし方の例:
- 適当な 40 hex を
mergeCommitに書けば reviewing -> done 条件を満たせる - 別 task の PR 番号を流用して implementing -> reviewing へ進める
- 古い impact 結果を再利用して analyzing -> implementing に進める
追加すべき gate:
impact.analyzedAt >= latestTaskMaterializationAtimpact.fileSetorimpact.inputHashが現在の対象と一致lock.lockedBy === assignee@nodeかつ lock 未期限切れbranchExists && branchHeadCommit !== nullpr.headRefName === branchName && pr.state === OPENpr.reviewDecision === APPROVEDまたは required checks successmergeCommitが GitHub 上で当該 PR の merge commit と一致issue.closedByPullRequest === prNumber相当の検証
4. High: plan の reviewing -> done は既存 27 ルールと衝突する
plan では Step 7 を reviewing -> done にしていますが、既存 state machine では:
reviewing -> doneはreview_approved && no_deploymentdeploying -> doneは sideEffectsclose_issue,merge_pr
根拠:
task-state-machine.ts:57-65
衝突内容:
- plan では merge が done の必須条件になっている
- 既存ルールでは
reviewing -> doneは「デプロイ不要なら done」、deploying -> doneが merge/close を担う
このままだと:
- デプロイ不要タスクの完了 semantic が変わる
- 既存の
deployingstate がほぼ空洞化する
結論:
- 既存 27 ルールと互換にするなら、
reviewing -> doneを廃止してreviewing -> merging|deployingを経由させるべきです - 少なくとも
mergeとdeployは別の irreversible event なので同じ gate にしない方が安全です
5. High: 追加 state が必要。少なくとも ready, locked, merged のどれかを state として持たないと gate が metadata 化して見落とされやすい
現行 state は:
- draft, pending, analyzing, implementing, reviewing, deploying, done, blocked, failed, cancelled
不足している意味上の段階:
ready: dependencies 解消済みだが未着手lockedまたはassigned: 実装開始前に資源確保済みmerged: review 承認済みで merge 完了、ただし post-merge audit/close 未完auditingまたはfinalizing: worklog/skill-bus/issue close 整合を取る終端前処理
理由:
- 現 plan は Step 4, 5 を「implementing 中の metadata」で表しており、state 上は見えません
- そのためオペレーション側が
currentState === implementingだけ見たときに、lock 未取得・branch 未作成・PR 未作成の task を同列に扱ってしまいます
最小提案:
pending -> ready -> analyzing -> locked -> implementing -> reviewing -> merged -> done- deploy が必要なら
reviewing -> deploying -> merged -> doneでもよい
もし state を増やしたくないなら:
- state ではなく
phaseChecklistの必須項目を machine-evaluable に持つ必要があります
6. Medium: pending -> analyzing gate が既存 semantics と少しズレている
plan Step 2 は dependencies.every(done) を pending -> analyzing の gate にしていますが、既存 state machine には:
pending -> blockedwithdependency_blockedblocked -> pendingwithdependency_resolved
根拠:
task-state-machine.ts:38-40,68-69
気になる点:
- dependency 未解決の task は本来
blockedに入る設計 - plan の
checkDependencies(): 'ready' | 'blocked'はあるが、gate table 上はpending -> analyzingしか記述がなく、pendingに留め続ける実装にも見える
提案:
- dependency 未解決時は必ず
blocked - 依存解決後に
blocked -> pendingを踏ませる readyを導入するならpendingは単なる backlog、readyが dispatchable を表す方が明確
7. Medium: pending -> implementing (skip_analysis) ルールとの整合が未定義
既存 27 ルールには pending -> implementing が存在します。
根拠:
task-state-machine.ts:39
plan では Step 3 で impact 記録が implementing の必須条件なので、skip_analysis を事実上禁止する方向に見えます。これは設計としてはよいですが、既存ルールを「変更なし」とは両立しません。
提案:
skip_analysisを削除する- もしくは
analysis_mode: full | waivedを明示し、waived でも human approval 必須にする
8. Medium: TTL 7200s は固定値としては長すぎ、しかも heartbeat 前提がない
7200 秒は 2 時間です。長時間の実装には短すぎることもありますが、「エージェントが死んだのに他ノードが2時間待つ」という意味では長すぎます。
問題:
- lock refresh/heartbeat が plan にない
- 実装中に TTL 失効すると、別ノードが同じファイルを取りに行ける
- 逆に crash 後の回復は最長 2 時間止まる
提案:
- 固定 TTL ではなく short lease + heartbeat 方式
- 例: lease 300s, renew every 60s, stale after 2 missed renewals
- 人手 override 用の
force unlockは残す
7200s が妥当なのは:
- 単一マシン
- 長いジョブ
- 他ノードが少ない
今回は「across machines」が前提なので、固定 7200s は非推奨です。
9. Medium: tasks.json 内 fileLocks 更新はクロスマシン race に弱い
plan は JSONL から tasks.json の fileLocks へ統合しますが、単一 JSON ドキュメントの read-modify-write は append-only より競合に弱いです。
想定 race:
- Machine A, B が同時に load
- 両者とも conflict なしと判断
- 両者が別々に save
- 後勝ち write で片方の lock が消える、または両方が lock 獲得したと誤認する
JSONL append-only より悪化する点:
- append-only は競合検査を後から再生しやすい
- 単一 JSON overwrite は lost update しやすい
最低限必要:
- OS レベルの lock file か atomic rename
- compare-and-swap 用の version check
syncVersionではなく document version で CAS- lock acquisition を「check + write」でなく「single atomic claim」に寄せる
クロスマシンで本当に安全にするなら:
- 共有 filesystem の advisory lock 依存は弱い
- ローカル files しかないなら coordinator 1台に集約すべき
- もしくは GitHub/SQLite/Postgres 等の単一調停者が必要
Are all 9 GATEs sufficient?
結論: 不十分です。
足りていないのは主に 3 系統です。
- Provenance gate
- その証跡が「今の task/branch/PR のものか」を確認していない
- Freshness gate
- impact / lock / PR / merge 情報が最新かを見ていない
- Authority gate
- 誰がその state を進めてよいか、誰の lock か、human approval がどこに記録されているかを見ていない
Can an LLM still skip steps?
はい。現案のままだと skip できます。
代表例:
- metadata を直接埋めて
recordPR()やrecordMerge()を呼ぶ - 既存の
updateTaskState()/applyTransition()経路から protocol をバイパスする - stale な lock を握ったまま別ノードが再取得する
- 実 branch/PR/GitHub state を見ずに JSON だけ整えて done にする
Compatibility with existing 27 rules
結論: そのままでは非互換です。
非互換ポイント:
conditionsが未評価なので既存ルールの意味が実装されていないpending -> implementing (skip_analysis)と plan の mandatory impact gate が衝突reviewing -> done/deploying -> doneの意味が plan の merge 必須完了定義と衝突
互換にしたいなら:
- 27 ルールを単に残すのでなく、条件を実評価する新 API へ移行
- 完了定義を
done = merged + issue closed + audit recordedに寄せるなら、ルール表も更新が必要
Should additional states exist?
結論: 追加した方が安全です。
推奨候補:
readylockedorassignedmergedauditingorfinalizing
最小でも merged は有用です。review approved と merge complete と audit complete は別イベントだからです。
Is TTL 7200s appropriate?
結論: 固定値としては不適切です。
- 単一マシンなら許容
- 複数マシン協調では長すぎる
- heartbeat/renewal がないため safety より liveness も safety も中途半端
推奨:
- lease 300-600s
- renew interval 60-120s
- owner heartbeat 必須
- expired lock reacquire 時に fencing token を使う
Race conditions in file locks across machines
結論: 現案のままでは race があります。
主な懸念:
- lost update
- double-acquire
- stale lock resurrection
- clock skew による TTL 判定ズレ
- network partition 中の二重実行
対策優先順:
- Single writer/coordinator に lock 判定を集約
- atomic write + CAS version check
- lease renewal と fencing token
- monotonic timestamp source を一元化
Recommended design changes
TaskStateMachineを gate-aware にする
canTransition(task, to, context)を導入conditionsを文字列配列でなく predicate 群へ寄せる
DeterministicExecutionProtocolを唯一の state mutation 経路にする
TaskManager.updateTaskState()などの素通し経路は封鎖または内部専用化
- 完了 state を分解する
reviewing -> merged -> done- deploy が必要なら
reviewing -> deploying -> merged -> done
-
lock は fixed TTL でなく renewable lease にする
-
tasks.jsonoverwrite で lock を持たない
- 少なくとも CAS + atomic rename
- 可能なら coordinator/SQLite 等へ分離
- gate を「presence check」から「verified linkage check」に上げる
- PR 番号がある、ではなく「その task の branch を head に持つ open/merged PR がある」
Bottom line
plan の方向性自体は正しいです。ただし現在の state machine を「そのまま使う」前提だと、GATE は仕様書上の強い言葉に留まり、実装上は抜けられます。
最優先で直すべき点は次の 3 つです。
- state machine に条件評価を実装する
- state transition を store に永続反映する
- merge / audit / close を
done前に明示的に分離する
この 3 点を入れない限り、「LLM の揺らぎを JSON state だけで封じる」保証には届きません。