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>
310 lines
14 KiB
Markdown
310 lines
14 KiB
Markdown
# 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 だけで封じる」保証には届きません。
|