mergegate/docs/dtp/reviews/codex-review-state-machine.md
林 駿甫 (Shunsuke Hayashi) 146fcafc5e [追加] DTP (Deterministic Task Protocol) 設計文書・指示書を移植
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>
2026-04-10 01:07:32 +09:00

310 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-80`
- `canTransition()``r.to === to` しか見ない: `task-state-machine.ts:103-106`
- `applyTransition()``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-158`
- `TaskExecutor.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 >= latestTaskMaterializationAt`
- `impact.fileSet` or `impact.inputHash` が現在の対象と一致
- `lock.lockedBy === assignee@node` かつ lock 未期限切れ
- `branchExists && branchHeadCommit !== null`
- `pr.headRefName === branchName && pr.state === OPEN`
- `pr.reviewDecision === APPROVED` または required checks success
- `mergeCommit` が 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_deployment`
- `deploying -> done` は sideEffects `close_issue`, `merge_pr`
根拠:
- `task-state-machine.ts:57-65`
衝突内容:
- plan では merge が done の必須条件になっている
- 既存ルールでは `reviewing -> done` は「デプロイ不要なら done」、`deploying -> done` が merge/close を担う
このままだと:
- デプロイ不要タスクの完了 semantic が変わる
- 既存の `deploying` state がほぼ空洞化する
結論:
- 既存 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 -> blocked` with `dependency_blocked`
- `blocked -> pending` with `dependency_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 系統です。
1. Provenance gate
- その証跡が「今の task/branch/PR のものか」を確認していない
2. Freshness gate
- impact / lock / PR / merge 情報が最新かを見ていない
3. 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?
結論: 追加した方が安全です。
推奨候補:
- `ready`
- `locked` or `assigned`
- `merged`
- `auditing` or `finalizing`
最小でも `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 中の二重実行
対策優先順:
1. Single writer/coordinator に lock 判定を集約
2. atomic write + CAS version check
3. lease renewal と fencing token
4. monotonic timestamp source を一元化
## Recommended design changes
1. `TaskStateMachine` を gate-aware にする
- `canTransition(task, to, context)` を導入
- `conditions` を文字列配列でなく predicate 群へ寄せる
2. `DeterministicExecutionProtocol` を唯一の state mutation 経路にする
- `TaskManager.updateTaskState()` などの素通し経路は封鎖または内部専用化
3. 完了 state を分解する
- `reviewing -> merged -> done`
- deploy が必要なら `reviewing -> deploying -> merged -> done`
4. lock は fixed TTL でなく renewable lease にする
5. `tasks.json` overwrite で lock を持たない
- 少なくとも CAS + atomic rename
- 可能なら coordinator/SQLite 等へ分離
6. gate を「presence check」から「verified linkage check」に上げる
- PR 番号がある、ではなく「その task の branch を head に持つ open/merged PR がある」
## Bottom line
plan の方向性自体は正しいです。ただし現在の state machine を「そのまま使う」前提だと、GATE は仕様書上の強い言葉に留まり、実装上は抜けられます。
最優先で直すべき点は次の 3 つです。
1. state machine に条件評価を実装する
2. state transition を store に永続反映する
3. merge / audit / close を `done` 前に明示的に分離する
この 3 点を入れない限り、「LLM の揺らぎを JSON state だけで封じる」保証には届きません。