[追加] 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>
This commit is contained in:
林 駿甫 (Shunsuke Hayashi) 2026-04-10 01:07:25 +09:00
parent 2d500c3654
commit 146fcafc5e
50 changed files with 4895 additions and 0 deletions

170
docs/dtp/GIT-RULES.md Normal file
View file

@ -0,0 +1,170 @@
# Git 運用ルール
> このリポジトリにおけるコミットとプルリクエストの出し方の定義。
> 全エージェント・全人間がこのルールに従う。
---
## ローカルコミットのルール
### いつコミットするか
| 状況 | コミットする? |
|------|-------------|
| ファイルを1つ以上変更した | はい、すぐコミット |
| テストを追加した | はい、すぐコミット |
| ドキュメントを更新した | はい、すぐコミット |
| 設定ファイルを変更した | はい、すぐコミット |
| 作業途中だが一区切りついた | はい、途中でもコミット |
| 1時間以上コミットしていない | 危険。今すぐコミット |
**原則: 小刻みにコミット。大きな変更を1コミットにまとめない。**
### コミットメッセージの形式
日本語タグ形式を使用する。
```
[タグ] 何をしたかの要約1行、日本語
(必要なら)詳細・理由・影響範囲
```
### タグ一覧
| タグ | いつ使う | 例 |
|------|---------|-----|
| `[追加]` | 新しい機能・ファイルを加えた | `[追加] DAGエンジンのトポロジカルソート実装` |
| `[修正]` | バグ・不具合を直した | `[修正] ロック競合チェックの off-by-one エラー` |
| `[改善]` | 既存コードの品質・性能を上げた | `[改善] Clippy警告を解消` |
| `[整備]` | 設定・CI・依存など開発環境の手入れ | `[整備] clap 依存を Cargo.toml に追加` |
| `[文書]` | ドキュメント・READMEの更新 | `[文書] HANDOFF プロトコルを追加` |
| `[削除]` | 不要なコード・ファイルを取り除いた | `[削除] 未使用の TaskState::Deploying を削除` |
| `[検証]` | テストの追加・修正 | `[検証] GATE拒否テスト5件追加` |
| `[完了]` | Phase が完了したGATE GREEN | `[完了] Phase 2: ステートマシンGATE統合` |
### コミットの粒度
```
良い例:
[追加] TaskLock に last_heartbeat フィールド追加
[検証] lease 期限切れテスト追加
[修正] is_expired() の閾値計算を修正
悪い例:
[追加] Phase 4 全部実装 ← 大きすぎ。分割すること
```
---
## リモート push のルール
### いつ push するか
| 状況 | push する? |
|------|-----------|
| Phase の GATE が GREEN になった | はい、必ず push |
| HANDOFF_NOTE.md を書いた | はい、必ず push次のエージェントが pull する) |
| 作業の区切りがついた3コミット以上溜まった | はい、push |
| 1日の作業終了時 | はい、必ず push |
| テストが RED のまま | push してよい(ただしコミットメッセージに明記) |
**原則: push は頻繁に。ローカルに溜め込まない。**
---
## プルリクエストのルール
### いつ PR を出すか
このリポジトリでは **Phase 単位で PR を出す**
| 状況 | PR を出す? |
|------|-----------|
| Phase の GATE.md が全て GREEN | はい、PR を出す |
| Phase の作業が途中 | 出さないmain に直接 push でよい) |
| 複数 Phase をまとめて | 出さない1 PR = 1 Phase |
### PR のタイトル形式
```
[完了] Phase {N}: {Phase の要約}
```
例:
```
[完了] Phase 1: 型定義のハードニング
[完了] Phase 2: ステートマシンにGATE predicate統合
[完了] Phase 6: Protocol統合全GATEを1クラスに結合
```
### PR の本文テンプレート
```markdown
## 概要
Phase {N} の全タスクを完了。
## 完了したタスク
- [x] タスク1
- [x] タスク2
- [x] タスク3
## テスト結果
- cargo test: {N}/{M} GREEN
- cargo clippy: 警告ゼロ
## GATE 状態
autorun/phase-{N}-*/GATE.md の全条件 GREEN
## 次の Phase
Phase {N+1} に引き継ぎ可能。
HANDOFF_NOTE.md を autorun/phase-{N}-*/ に配置済み。
```
### PR のマージ条件
| 条件 | 必須? |
|------|--------|
| cargo test GREEN | 必須 |
| cargo clippy 警告ゼロ | 必須 |
| GATE.md 全条件 GREEN | 必須 |
| HANDOFF_NOTE.md 作成済み | 必須 |
| 人間のレビュー承認 | Phase 6, 8 のみ必須。他は自動マージ可 |
| Codex レビュー | 推奨(必須ではない) |
### ブランチ戦略
```
main ← 全ての作業はここに直接 push
← Phase 完了時に PRmain → main ではなく、記録用)
feature/phase-{N}-{slug} ← Phase の作業ブランチ(任意)
例: feature/phase-2-state-machine-gates
worktree ← Codex が並列実行時に使用
```
**原則: main に直接 push してよい。ブランチは並列作業時のみ使用。**
---
## 現在のコミット履歴
```
b3dcbfa [整備] autorun/ をトップレベルに移動
5f8b4e2 [文書] Auto Run Playbook 階層化 + Handoff Protocol 完成
c88f218 [追加] Rust 基盤: types + state machine with GATE predicates
c030294 [追加] 初期条件: SSOT ドキュメント + ワークスペース構造
```
---
## 禁止事項
| 禁止 | 理由 |
|------|------|
| `git push --force` | 他エージェントの作業を消す |
| `git reset --hard` | ローカルの未コミット変更を消す |
| 1日以上 push しない | 他エージェントとの同期が切れる |
| コミットメッセージを英語で書く | このリポは日本語タグ形式 |
| 10ファイル以上を1コミットにまとめる | 分割すること |

414
docs/dtp/PLAN-v1.md Normal file
View file

@ -0,0 +1,414 @@
# Deterministic Task Execution Protocol — Implementation Plan
## Context
### 問題
LLM エージェントは確率的にトークンを生成するため、「完了しました」と言いながら Issue を Close しない、impact 分析を飛ばす、別エージェントと同じファイルを同時編集する、といった **揺らぎ** が発生する。現在、この揺らぎを防ぐ仕組みが **3箇所に分散して不完全に存在** している。
### ゴール
`project_memory/tasks.json`**確定的 DAG + ステートマシン + ファイルロック** の三位一体として実装し、LLM の発言ではなく **JSON の状態遷移だけが事実** となる実行プロトコルを構築する。ゴールに対して確定的な手順を踏めば、確実に成果物1点にたどり着く。
### North Star一文
> LLM の揺らぎを許さない。tasks.json の状態遷移が許可し、GitHub が確定し、git merge が不可逆にする。この三段階だけが「完了」を定義する。
---
## 既存ピースの統合マップ
```
統合先: @miyabi/task-manager (Miyabi/packages/task-manager/)
① ステートマシン(既存・そのまま使う)
src/state/task-state-machine.ts
- 10状態、27遷移ルール
- applyTransition() で状態遷移を検証・適用
- conditions/sideEffects 付き
② DAG移植元: KOTOWARI/skills/openclaw-crowd/src/scheduling/
task-dependency-graph.ts
- Kahn のトポロジカルソート
- 循環検出DFS
- hard/soft 依存の区別
- getReadyTasks() で実行可能タスクを返す
③ ファイルロック(移植元: agent-skill-bus/src/queue.js
PromptRequestQueue
- JSONL ベースのロック管理
- TTL 付きロック(デフォルト 7200秒
- startExecution() でロック獲得 + 競合チェック
- releaseExpiredLocks() で TTL 切れを自動解放
- getDispatchable() で DAG 依存 + ロック競合を同時チェック
```
---
## 実装計画
### Phase 1: ManagedTask 型の拡張
**ファイル**: `Miyabi/packages/task-manager/src/types/task.ts`
追加するフィールド:
```typescript
export interface TaskLock {
lockedBy: string; // "{agentName}@{nodeName}"
lockedAt: string; // ISO 8601
ttl: number; // 秒
affectedFiles: string[]; // ロック対象ファイルパス
}
export interface TaskImpact {
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
affectedSymbols: number;
depth1: string[]; // 直接影響シンボル名
analyzedAt: string; // GNI 分析タイムスタンプ
}
// ManagedTask に追加
export interface ManagedTask extends BaseTask {
// ...既存フィールド...
// NEW: DAG
dependents: string[]; // このタスクが done になったら解放されるタスク
softDependencies: string[]; // あれば待つが、なくても進められる
// NEW: ロック
lock: TaskLock | null;
// NEW: ImpactC&I Phase A の結果)
impact: TaskImpact | null;
// NEW: 確定的参照
branchName: string | null; // feature/issue-{N}-{slug}
prNumber: number | null;
mergeCommit: string | null; // merge 後のハッシュ
}
```
### Phase 2: TaskDependencyGraph の移植
**新ファイル**: `Miyabi/packages/task-manager/src/dag/task-dag.ts`
KOTOWARI の `task-dependency-graph.ts` から以下を移植:
- `addTask()`, `removeTask()`, `addDependency()`
- `topologicalSort()` (Kahn のアルゴリズム)
- `detectCycle()` (DFS)
- `getReadyTasks()` — dependencies が全て done のタスクを返す
- `getParallelGroups()` — 同一レベルのタスク群を返すDAG levels
**変更点**:
- `ScheduledTask``ManagedTask` に型を合わせる
- `hard`/`soft` 依存を `dependencies`/`softDependencies` にマップ
- シリアライズ/デシリアライズを JSON 対応に
### Phase 3: FileLockManager の移植
**新ファイル**: `Miyabi/packages/task-manager/src/lock/file-lock-manager.ts`
agent-skill-bus の `queue.js` から以下を移植:
- ロック獲得: `acquireLock(taskId, agent, node, files, ttl)`
- ロック解放: `releaseLock(taskId)`
- TTL 切れ自動解放: `releaseExpiredLocks()`
- 競合チェック: `hasConflict(files): { conflicting: boolean, heldBy: string, taskId: string }`
**変更点**:
- JSONL → tasks.json 内の `fileLocks` フィールドに統合
- `prId``taskId` にリネーム
- TypeScript 化
### Phase 4: DeterministicExecutionProtocol — 確定的シーケンスの実装
**新ファイル**: `Miyabi/packages/task-manager/src/protocol/deterministic-execution.ts`
これが核心。全ステップを **ステートマシンのゲート** として実装:
```typescript
export class DeterministicExecutionProtocol {
private stateMachine: TaskStateMachine;
private dag: TaskDAG;
private lockManager: FileLockManager;
private store: TaskStore; // tasks.json の読み書き
/**
* Step 0: タスク登録 — Issue が存在しなければ拒否
* GATE: githubIssueNumber が null → 登録拒否
*/
registerTask(input: CreateTaskInput & { githubIssueNumber: number }): ManagedTask
/**
* Step 1: DAG に登録 — 循環依存があれば拒否
* GATE: detectCycle() が non-null → 登録拒否
*/
addToDAG(taskId: string, dependencies: string[]): void
/**
* Step 2: 依存解決チェック — 全依存が done でなければ blocked
* GATE: dependencies.every(d => store.get(d).currentState === 'done')
*/
checkDependencies(taskId: string): 'ready' | 'blocked'
/**
* Step 3: Impact 分析記録 — impact が null なら implementing に遷移不可
* GATE: task.impact !== null && task.impact.riskLevel !== undefined
* GATE: HIGH/CRITICAL → humanApprovalRequired = true
*/
recordImpact(taskId: string, impact: TaskImpact): void
/**
* Step 4: アサイン + ロック獲得 — 競合があれば拒否
* GATE: lockManager.hasConflict(files) === false
* EFFECT: lock フィールドに書き込み、fileLocks テーブルに登録
* EFFECT: 状態遷移 analyzing → implementing
*/
assignAndLock(taskId: string, agent: string, node: string, files: string[]): void
/**
* Step 5: ブランチ作成 — branchName が null なら PR 作成不可
* GATE: branchName matches /^(feature|fix|hotfix)\/issue-\d+-/
*/
recordBranch(taskId: string, branchName: string): void
/**
* Step 6: PR 作成 — prNumber が null なら reviewing に遷移不可
* GATE: prNumber > 0 && branchName !== null
* EFFECT: 状態遷移 implementing → reviewing
*/
recordPR(taskId: string, prNumber: number): void
/**
* Step 7: Merge — mergeCommit が null なら done に遷移不可
* GATE: mergeCommit matches /^[0-9a-f]{40}$/
* EFFECT: 状態遷移 reviewing → done
* EFFECT: ロック解放
* EFFECT: dependents の blocked → pending 遷移
* EFFECT: GitHub Issue Close 確認
*/
recordMerge(taskId: string, mergeCommit: string): void
/**
* Step 8: 監査記録
* EFFECT: worklog.md に追記
* EFFECT: agent-skill-bus に record-run
*/
recordAudit(taskId: string): void
}
```
**重要**: 各メソッドは **GATE 条件を満たさなければ例外を投げる**。LLM が「大丈夫です」と言っても、GATE が閉じていれば実行不可能。
### Phase 5: TaskStore — tasks.json の永続化
**新ファイル**: `Miyabi/packages/task-manager/src/store/task-store.ts`
```typescript
export class TaskStore {
private filePath: string; // project_memory/tasks.json
load(): TasksDocument
save(doc: TasksDocument): void
getTask(id: string): ManagedTask | null
updateTask(id: string, patch: Partial<ManagedTask>): void
getFileLocks(): Record<string, string> // file → taskId
setFileLock(file: string, taskId: string): void
removeFileLock(file: string): void
}
export interface TasksDocument {
version: number;
lastUpdated: string;
tasks: ManagedTask[];
fileLocks: Record<string, string>;
dagLevels: string[][]; // トポロジカルソート結果のキャッシュ
}
```
### Phase 6: GitHub 同期の強化
**既存ファイル**: `src/sync/bidirectional-sync.ts`
変更:
- `syncTasks()`**merge commit 検証** を追加
- ローカルで done なのに GitHub で Issue が Open → **矛盾検出、done → blocked に巻き戻し**
- GitHub で Closed なのにローカルで implementing → **pull で done に更新**
- `conflictStrategy``'deterministic'` を追加
- GitHub の状態を常に優先LLM のローカル判断を信用しない)
### Phase 7: CLI コマンド
**新ファイル**: `Miyabi/packages/task-manager/src/cli/deterministic-cli.ts`
```bash
# タスク登録Issue 必須)
miyabi task register --issue 45 --title "JWT→セッション移行" --deps task-000
# DAG 可視化
miyabi task dag
# 依存解決 + ディスパッチ可能タスク一覧
miyabi task dispatchable
# アサイン + ロック
miyabi task assign task-001 --agent kotowari-dev --node macbook-pro --files "src/auth/*.ts"
# 状態確認JSON出力、人間可読
miyabi task status [task-id]
# ロック一覧
miyabi task locks
# 強制ロック解放TTL切れ or 人間判断)
miyabi task unlock task-001
# 同期GitHub → ローカル → GitHub
miyabi task sync
```
---
## ファイル構成(最終)
```
Miyabi/packages/task-manager/src/
├── types/
│ ├── task.ts # ← Phase 1: TaskLock, TaskImpact, 拡張フィールド追加
│ ├── config.ts # ← Phase 6: 'deterministic' conflict strategy 追加
│ └── index.ts
├── state/
│ └── task-state-machine.ts # 既存(変更なし)
├── dag/ # ← Phase 2: NEW
│ └── task-dag.ts
├── lock/ # ← Phase 3: NEW
│ └── file-lock-manager.ts
├── protocol/ # ← Phase 4: NEW核心
│ └── deterministic-execution.ts
├── store/ # ← Phase 5: NEW
│ └── task-store.ts
├── sync/
│ ├── bidirectional-sync.ts # ← Phase 6: merge commit 検証追加
│ └── ...
├── execution/
│ └── ... # 既存(変更なし)
├── decomposition/
│ └── ... # 既存(変更なし)
├── cli/ # ← Phase 7: NEW
│ └── deterministic-cli.ts
├── task-manager.ts # ← DeterministicExecutionProtocol を統合
└── index.ts # ← 新モジュールの export 追加
```
---
## 成果物: tasks.json の確定的スキーマ
```jsonc
{
"version": 1,
"lastUpdated": "2026-04-10T00:00:00Z",
"tasks": [
{
"id": "task-001",
"title": "JWT→セッション認証移行",
"description": "...",
"type": "refactor",
"priority": 80,
"githubIssueNumber": 45,
"currentState": "implementing",
"stateHistory": [
{ "from": "draft", "to": "pending", "timestamp": "...", "triggeredBy": "user" },
{ "from": "pending", "to": "analyzing", "timestamp": "...", "triggeredBy": "system" },
{ "from": "analyzing", "to": "implementing", "timestamp": "...", "triggeredBy": "system" }
],
"assignedAgent": "kotowari-dev",
"dependencies": ["task-000"],
"softDependencies": [],
"dependents": ["task-002", "task-003"],
"lock": {
"lockedBy": "kotowari-dev@macbook-pro",
"lockedAt": "2026-04-10T00:00:00Z",
"ttl": 7200,
"affectedFiles": ["src/auth/auth.controller.ts", "src/middleware/auth.middleware.ts"]
},
"impact": {
"riskLevel": "HIGH",
"affectedSymbols": 12,
"depth1": ["LoginHandler", "RefreshHandler", "LogoutHandler"],
"analyzedAt": "2026-04-10T00:00:00Z"
},
"branchName": "feature/issue-45-jwt-to-session",
"prNumber": null,
"mergeCommit": null,
"syncVersion": 3,
"retryCount": 0
}
],
"fileLocks": {
"src/auth/auth.controller.ts": "task-001",
"src/middleware/auth.middleware.ts": "task-001"
},
"dagLevels": [
["task-000"],
["task-001"],
["task-002", "task-003"]
]
}
```
---
## 確定的ゲート一覧LLM の揺らぎを封じる全チェックポイント)
| Step | 遷移 | GATEこれが false なら遷移不可) |
|------|------|------|
| 0 | → draft | `githubIssueNumber > 0` |
| 1 | draft → pending | `title.length > 0 && description.length > 0 && dependencies は明示的配列` |
| 2 | pending → analyzing | `dependencies.every(d => tasks[d].currentState === 'done')` |
| 3 | analyzing → implementing | `impact !== null && (impact.riskLevel !== 'HIGH' && !== 'CRITICAL' \|\| humanApproved)` |
| 4 | implementing 開始 | `lock !== null && fileLocks に競合なし` |
| 5 | implementing 中 | `branchName matches /^(feature\|fix\|hotfix)\/issue-\d+-/` |
| 6 | implementing → reviewing | `prNumber > 0` |
| 7 | reviewing → done | `mergeCommit matches /^[0-9a-f]{40}$/` |
| 8 | done 後 | `worklog に記録済み && Issue が Closed` |
**全 GATE は JSON フィールドの値チェック。LLM の自然言語判断は一切介在しない。**
---
## 検証方法
1. **ユニットテスト**: 各 GATE の境界値テストvitest、既存テスト基盤を使用
2. **統合テスト**: draft → done の全シーケンスを1タスクで通す
3. **競合テスト**: 2つのタスクが同じファイルをロックしようとして拒否されることを検証
4. **DAG テスト**: 循環依存を検出して拒否、依存解決後に正しい順序でディスパッチ
5. **GitHub 同期テスト**: ローカル done ↔ GitHub Closed の整合性検証
6. **TTL テスト**: ロック TTL 切れ時の自動解放と failed 遷移
```bash
cd Miyabi/packages/task-manager
npm test # 全テスト
npm run test -- --grep "deterministic" # プロトコルテストのみ
```
---
## 先行研究・設計根拠
- **Temporal.io / Durable Execution**: ワークフローの各ステップを永続化し、障害後も再開可能にする。tasks.json はこれのファイルベース簡易版。
- **Kubernetes Controller Pattern**: Desired Statetasks.jsonと Actual StateGitHubの差分を検出して reconcile する。bidirectional-sync がこれに相当。
- **Database MVCC / Pessimistic Locking**: ファイルロックは悲観的ロック。TTL はデッドロック防止。
- **Apache Airflow DAG**: タスク間の依存関係をDAGで表現し、トポロジカルソートで実行順序を決定。task-dag.ts がこれに相当。
- **Harness Engineeringdocs/design/harness-engineering.md**: 「制約をファイルに書き込み、会話ではなくハーネスで強制する」— 本プロトコルの思想的根拠。
---
## 実装順序とリスク
| Phase | 工数目安 | リスク | 依存 |
|-------|---------|--------|------|
| 1. 型拡張 | 小 | LOW | なし |
| 2. DAG 移植 | 中 | LOW | Phase 1 |
| 3. ロック移植 | 中 | MEDIUMTTL設計 | Phase 1 |
| 4. プロトコル | 大 | HIGH核心 | Phase 1,2,3 |
| 5. TaskStore | 中 | LOW | Phase 1 |
| 6. GitHub同期強化 | 中 | MEDIUMAPI制限 | Phase 4,5 |
| 7. CLI | 小 | LOW | Phase 4,5 |

921
docs/dtp/PLAYBOOK.md Normal file
View file

@ -0,0 +1,921 @@
# Deterministic Task Execution Protocol — Maestro Implementation Playbook
_Version: 0.1.0 | Created: 2026-04-10 | Status: PLAN (未実装)_
---
## 0. North Star
> LLM の揺らぎを許さない。tasks.json の GATE が許可し、GitHub が確定し、git merge が不可逆にする。この三段階だけが「完了」を定義する。
---
## 1. 前提条件チェックリスト(実装開始前に全て GREEN であること)
### 1.1 リポジトリ・インデックス
| # | 条件 | 検証コマンド | 状態 |
|---|------|------------|------|
| 1 | openclaw-workspace GNI インデックス fresh | `npx gitnexus status --repo openclaw-workspace` | GREEN (50,130 nodes) |
| 2 | miyabi-private GNI インデックス fresh | `npx gitnexus status --repo miyabi-private` | GREEN (12,448 nodes) |
| 3 | rust-ai-pipeline GNI インデックス fresh | `npx gitnexus status --repo rust-ai-pipeline` | GREEN (813 nodes) |
| 4 | agent-skill-bus GNI インデックス fresh | `npx gitnexus status --repo agent-skill-bus` | GREEN (154 nodes) |
### 1.2 既存テスト GREEN
| # | パッケージ | コマンド | 状態 |
|---|-----------|---------|------|
| 5 | @miyabi/task-manager | `cd Miyabi/packages/task-manager && npm test` | 要確認 |
| 6 | rust-ai-pipeline | `cd ~/dev/products/rust-ai-pipeline && cargo test` | 要確認 |
| 7 | agent-skill-bus | `cd ~/dev/tools/agent-skill-bus && npm test` | 要確認 |
### 1.3 Codex 3体レビュー反映
| # | 指摘 | 対応方針 | 反映先 Phase |
|---|------|---------|-------------|
| R1-1 | canTransition() が conditions を評価していない | Protocol を唯一の遷移窓口にする | Phase 2 |
| R1-2 | applyTransition() の結果が store に永続化されない | store-backed immutable update に統一 | Phase 3 |
| R1-3 | GATE が「存在チェック」で「出自検証」がない | GitHub API で PR/merge を実検証 | Phase 5 |
| R1-4 | reviewing → done が既存 27 ルールと衝突 | `merged` 状態を追加 | Phase 2 |
| R1-5 | TTL 7200s 固定は不適切 | lease 300s + heartbeat 60s に変更 | Phase 4 |
| R2-1 | tasks.json の read-modify-write がクロスマシン race | atomic write + CAS version check | Phase 3 |
| R2-2 | assignAndLock() の TOCTOU 脆弱性 | OS flock + re-read + re-check + write | Phase 4 |
| R2-3 | softDependencies を DAG level に入れると過度に直列化 | soft は dispatch priority にのみ使用 | Phase 2 |
| R2-4 | dagLevels キャッシュは永続化しない方が安全 | 読み込み時再計算に変更 | Phase 2 |
| R2-5 | JSONL event log + snapshot JSON が安全 | task-events.jsonl + tasks.snapshot.json 構成 | Phase 3 |
| R3-1 | tasks.json を事実の正に昇格させすぎ | execution ledger に下げ、SSOT は GitHub | Phase 5 |
| R3-2 | GitHub API 障害時の degraded mode 未定義 | awaiting_github_sync 状態 + retry queue | Phase 5 |
| R3-3 | Issue closed → done は危険 | PR merged + issue linked で確定 | Phase 5 |
| R3-4 | mergeCommit の取得経路が未仕様 | PR API の merge_commit_sha から取得 | Phase 5 |
| R3-5 | PRなし正当完了の escape hatch がない | completionMode = manual/github-pr/external-op | Phase 6 |
---
## 2. アーキテクチャ概要
### 2.1 二層 SSOT + Execution Ledger
```
+------------------------------------------+
| GitHub (Fact SSOT) |
| Issue 状態 / PR merged / Review status |
+-----+------------------------------------+
| pull (authoritative facts)
| push (proposed state)
v
+------------------------------------------+
| tasks.snapshot.json |
| (Execution Ledger / Gate Cache) |
| ← materialized view of event log |
+-----+------------------------------------+
^
| replay / rebuild
|
+------------------------------------------+
| task-events.jsonl |
| (Append-only Event Log) |
| state_transition / lock_acquired / |
| lock_released / dag_changed / |
| github_synced / gate_passed / |
| gate_rejected / human_approved |
+------------------------------------------+
```
### 2.2 状態遷移図Codex R1-4 反映: merged 追加)
```
[*] ──GATE 0──→ draft
GATE 1
v
pending ←──── blocked (dep未解決)
│ ^
GATE 2 │
(deps done) │
│ │
v │
analyzing ─────→ blocked (lock競合)
GATE 3
(impact + human approval)
v
implementing ──── GATE 4 (lock + heartbeat)
GATE 5+6
(branch + PR)
v
reviewing
GATE 7
(PR merged verified via GitHub API)
v
merged ←── NEW STATE
GATE 8
(Issue closed + worklog)
v
done
```
### 2.3 コンポーネントマップ(全既存資産の統合先)
```
+-----------------------------------------------------------------------+
| DeterministicExecutionProtocol (NEW: ~200 lines) |
| 唯一の状態遷移窓口。全 GATE をここに集約。 |
+--+--+--+--+--+--+--+-------------------------------------------------+
| | | | | | |
v v v v v v v
+------+ +------+ +------+ +------+ +------+ +------+ +------+
|State | |DAG | |Lock | |Store | |Sync | |Impact| |Audit |
|Machin| |Engine| |Mgr | | | | | | | | |
+------+ +------+ +------+ +------+ +------+ +------+ +------+
既存 移植 統合 NEW 改造 NEW 既存
Miyabi KOTOW miyabi events bidirec GNI→ skill-
task- ARI -priv .jsonl tional Task bus
mgr crowd lock.ts +snap -sync Impact record
+a-s-b +a-s-b shot adapter -run
queue queue
```
---
## 3. Phase 定義(全 8 Phase
### Phase 0: 前提条件の確定(実装前)
**目的**: 全テスト GREEN、全インデックス fresh、Codex レビュー反映方針確定
**入力**: なし
**出力**: 上記チェックリスト全項目 GREEN
**手順**:
1. `cd ~/dev/ops/openclaw-workspace/Miyabi/packages/task-manager && npm test`
2. `cd ~/dev/products/rust-ai-pipeline && cargo test`
3. `cd ~/dev/tools/agent-skill-bus && npm test`
4. 全テスト GREEN でなければ修正してから Phase 1 に進む
**完了条件**: チェックリスト 1.1 + 1.2 の全項目 GREEN
---
### Phase 1: 型定義の拡張
**目的**: ManagedTask に DAG/Lock/Impact/確定的参照フィールドを追加
**対象ファイル**:
- `Miyabi/packages/task-manager/src/types/task.ts`
**追加する型**:
```typescript
// --- NEW: ファイルロック ---
export interface TaskLock {
lockedBy: string; // "{agentName}@{nodeName}"
lockedAt: string; // ISO 8601
leaseDurationSec: number; // R1-5: 固定 TTL ではなく lease
lastHeartbeat: string; // R1-5: heartbeat timestamp
affectedFiles: string[];
}
// --- NEW: Impact 記録 ---
export interface TaskImpact {
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
affectedSymbols: number;
depth1: string[];
analyzedAt: string;
analyzedCommit: string; // R1-3: 分析時の HEAD commit
inputHash: string; // R1-3: 対象ファイルセットのハッシュ
}
// --- NEW: GitHub 証跡 ---
export interface GitHubEvidence {
prNumber: number;
prHeadRef: string; // R1-3: PR の head branch
prState: 'OPEN' | 'MERGED' | 'CLOSED';
mergeCommitSha: string | null;
mergedAt: string | null;
reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | null;
issueState: 'OPEN' | 'CLOSED';
issueClosedByPR: boolean;
}
// --- NEW: 完了モード (R3-5) ---
export type CompletionMode = 'github-pr' | 'manual' | 'external-op';
// --- TaskState 拡張 (R1-4: merged 追加) ---
export type TaskState =
| 'draft' | 'pending' | 'analyzing' | 'implementing'
| 'reviewing' | 'merged' // ← NEW
| 'deploying' | 'done'
| 'blocked' | 'failed' | 'cancelled'
| 'awaiting_github_sync'; // ← NEW (R3-2)
```
**ManagedTask への追加フィールド**:
```typescript
export interface ManagedTask extends BaseTask {
// ...既存フィールド...
// DAG (R2-3: soft は別扱い)
dependents: string[];
softDependencies: string[];
// Lock
lock: TaskLock | null;
// Impact
impact: TaskImpact | null;
// 確定的参照
branchName: string | null;
githubEvidence: GitHubEvidence | null; // R1-3: 単なる prNumber/mergeCommit ではなく検証済み証跡
// 完了モード (R3-5)
completionMode: CompletionMode;
// 人間承認 (R1-3)
humanApproval: {
required: boolean;
approvedBy: string | null;
approvedAt: string | null;
reason: string | null;
} | null;
}
```
**createManagedTask() の更新**: 新フィールドのデフォルト値追加
**テスト**: 既存テストが壊れないことを確認 + 新フィールドの存在テスト
**Rust 連携**: `rust-ai-pipeline/src/pipeline.rs``PipelineReport` 構造体を参考に、`StepResult` パターンname/success/duration/stdout/stderrを GATE の通過記録に流用
**完了条件**: `npm test` GREEN、新型が compile 通る
---
### Phase 2: ステートマシン拡張 + DAG 統合
**目的**:
- `merged` 状態と `awaiting_github_sync` 状態を追加
- conditions を実評価するように変更
- DAG エンジンを移植・統合
**対象ファイル**:
- `Miyabi/packages/task-manager/src/state/task-state-machine.ts`(改造)
- `Miyabi/packages/task-manager/src/dag/task-dag.ts`NEW: 移植元 KOTOWARI
#### 2.1 ステートマシン改造
**R1-1 対応**: `conditions` を文字列配列から **predicate 関数** に変更
```typescript
export interface StateTransitionRule {
from: TaskState;
to: TaskState;
predicates?: ((task: ManagedTask, context: TransitionContext) => boolean)[];
sideEffects?: string[];
}
export interface TransitionContext {
store: TaskStore; // 他タスクの状態を参照するため
triggeredBy: 'user' | 'agent' | 'system' | 'github-webhook';
reason?: string;
humanApproval?: boolean;
}
```
**追加する遷移ルール**:
```typescript
// merged (NEW)
{ from: 'reviewing', to: 'merged', predicates: [
(task) => task.githubEvidence !== null,
(task) => task.githubEvidence?.prState === 'MERGED',
(task) => task.githubEvidence?.mergeCommitSha !== null,
(task) => /^[0-9a-f]{40}$/.test(task.githubEvidence?.mergeCommitSha ?? ''),
]},
{ from: 'merged', to: 'done', predicates: [
(task) => task.githubEvidence?.issueState === 'CLOSED',
]},
// awaiting_github_sync (NEW, R3-2)
{ from: 'reviewing', to: 'awaiting_github_sync', predicates: [] },
{ from: 'merged', to: 'awaiting_github_sync', predicates: [] },
{ from: 'awaiting_github_sync', to: 'merged', predicates: [
(task) => task.githubEvidence !== null,
]},
{ from: 'awaiting_github_sync', to: 'reviewing', predicates: [] },
// skip_analysis を削除 (R1-7 対応)
// { from: 'pending', to: 'implementing' } ← 削除
```
**R1-2 対応**: `applyTransition()` を store-backed に変更
```typescript
applyTransition(
task: ManagedTask,
to: TaskState,
context: TransitionContext
): TransitionResult & { task?: ManagedTask } {
// 1. predicates を全て評価
const rule = this.getTransitionRule(task.currentState, to);
if (!rule) return { valid: false, ... };
for (const pred of rule.predicates ?? []) {
if (!pred(task, context)) {
return { valid: false, error: `Predicate failed for ${task.currentState} → ${to}` };
}
}
// 2. 新 task を生成
const updatedTask = { ...task, currentState: to, stateHistory: [...] };
// 3. store に即座に永続化R1-2
context.store.updateTask(task.id, updatedTask);
return { valid: true, task: updatedTask };
}
```
#### 2.2 DAG エンジン移植
**移植元**: `KOTOWARI/skills/openclaw-crowd/src/scheduling/task-dependency-graph.ts`
**新ファイル**: `Miyabi/packages/task-manager/src/dag/task-dag.ts`
**変更点**:
- `ScheduledTask``ManagedTask` 型変換
- `hard`/`soft``dependencies`/`softDependencies` にマップ
- R2-3: soft deps は level 計算に含めないdispatch priority のみ)
- R2-4: `dagLevels` は永続化せず、`computeLevels()` で毎回再計算
- `getReadyTasks(store)`: `dependencies` が全て `done` or `merged` のタスクを返す
- `getDispatchScore(task)`: priority × age × softSatisfied × lockAvailability の合成スコア
**テスト**: KOTOWARI の既存テストを移植 + ManagedTask 型でのテスト追加
**完了条件**: DAG テスト GREEN、循環検出テスト GREEN、soft deps が level に影響しないことのテスト GREEN
---
### Phase 3: Event StoreJSONL + Snapshot
**目的**: R2-1/R2-5 対応。tasks.json を monolithic truth から event log + snapshot に分離
**新ファイル**:
- `Miyabi/packages/task-manager/src/store/event-store.ts`
- `Miyabi/packages/task-manager/src/store/snapshot-store.ts`
#### 3.1 Event Store
```typescript
export interface TaskEvent {
id: string; // ascending ID
ts: string; // ISO 8601
type: 'state_transition' | 'lock_acquired' | 'lock_released' | 'lock_heartbeat'
| 'dag_changed' | 'github_synced' | 'gate_passed' | 'gate_rejected'
| 'human_approved' | 'impact_recorded' | 'branch_created' | 'pr_created'
| 'merge_verified' | 'audit_recorded';
taskId: string;
agent: string;
node: string;
payload: Record<string, unknown>;
version: number; // R2-1: CAS 用バージョン
}
export class EventStore {
private filePath: string; // project_memory/task-events.jsonl
append(event: TaskEvent): void; // append-only
replay(since?: string): TaskEvent[]; // 全イベント再生
replayForTask(taskId: string): TaskEvent[];
}
```
#### 3.2 Snapshot Store
```typescript
export interface TasksSnapshot {
version: number; // R2-1: CAS バージョン
generatedAt: string;
generatedFromEventId: string; // どこまで replay したか
tasks: ManagedTask[];
fileLocks: Record<string, { taskId: string; agent: string; node: string; expiresAt: string }>;
}
export class SnapshotStore {
private filePath: string; // project_memory/tasks.snapshot.json
private lockFilePath: string; // project_memory/.tasks.lock
// R2-1: atomic write with OS flock + CAS
load(): TasksSnapshot;
save(snapshot: TasksSnapshot, expectedVersion: number): void; // version mismatch → throw
rebuild(eventStore: EventStore): TasksSnapshot; // event log から再構築
}
```
**R2-1 対応: atomic write の実装**:
```typescript
save(snapshot: TasksSnapshot, expectedVersion: number): void {
// 1. OS flock を取得fd-level
const lockFd = fs.openSync(this.lockFilePath, 'w');
flock(lockFd, LOCK_EX); // blocking
try {
// 2. 現在の version を再読込CAS
const current = this.load();
if (current.version !== expectedVersion) {
throw new Error(`CAS conflict: expected v${expectedVersion}, got v${current.version}`);
}
// 3. atomic rename で書き込み
const tmpPath = this.filePath + '.tmp';
fs.writeFileSync(tmpPath, JSON.stringify({ ...snapshot, version: expectedVersion + 1 }, null, 2));
fs.renameSync(tmpPath, this.filePath);
} finally {
flock(lockFd, LOCK_UN);
fs.closeSync(lockFd);
}
}
```
**Rust 連携**: `rust-ai-pipeline``PipelineReport` の JSON シリアライズパターン(`serde::Serialize`を参考に、event の JSON 形式を統一。将来的に Rust 側で event log を消費する際に互換性を持たせる。
**完了条件**: event append テスト、snapshot rebuild テスト、CAS conflict テスト、concurrent write テストfork して同時書き込み)
---
### Phase 4: File Lock Managerlease + heartbeat
**目的**: R1-5/R2-2 対応。固定 TTL → renewable lease に変更
**新ファイル**: `Miyabi/packages/task-manager/src/lock/file-lock-manager.ts`
**移植元**:
- `miyabi-private/packages/miyabi/src/util/lock.ts`RWLock パターン)
- `agent-skill-bus/src/queue.js`TTL + 競合チェック)
```typescript
export interface LeaseConfig {
leaseDurationSec: number; // default: 300
heartbeatIntervalSec: number; // default: 60
staleAfterMissedHeartbeats: number; // default: 2
}
export class FileLockManager {
constructor(
private eventStore: EventStore,
private snapshotStore: SnapshotStore,
private config: LeaseConfig
) {}
// R2-2: 原子的な lock 獲得
acquireLock(taskId: string, agent: string, node: string, files: string[]): void {
// 1. OS flock で snapshot をロック
// 2. snapshot を再読込
// 3. 競合チェックfiles が他タスクにロック中か)
// 4. stale lock の自動解放heartbeat 2回 miss
// 5. lock 書き込み + event 記録
// 6. OS flock 解放
}
// heartbeatlease 更新)
renewLease(taskId: string, agent: string, node: string): void {
// event: lock_heartbeat を append
// snapshot の lastHeartbeat を更新
}
// 解放
releaseLock(taskId: string): void {
// event: lock_released を append
// snapshot から fileLock エントリを削除
}
// stale 検出
releaseExpiredLeases(): { released: string[]; active: string[] } {
// lastHeartbeat + leaseDuration + staleAfterMissed × heartbeatInterval < now stale
}
// 競合チェック
hasConflict(files: string[]): { conflicting: boolean; heldBy?: string; taskId?: string } {
// snapshot の fileLocks をチェック
}
}
```
**Rust 連携**: `rust-ai-pipeline/src/signal.rs``Signal.is_fresh(max_age_ms)` パターンを heartbeat の鮮度判定に流用。同じ「timestamp + threshold → fresh/stale」の設計思想。
**テスト**:
- lease 獲得 → heartbeat → 解放 のライフサイクル
- 競合検出同一ファイルに2タスク
- stale 検出heartbeat なしで lease 期限切れ)
- concurrent acquire2プロセスが同時に acquireLock→ 1つだけ成功
**完了条件**: 全テスト GREEN
---
### Phase 5: GitHub 同期の再設計
**目的**: R3-1/R3-2/R3-3/R3-4 対応。二層 SSOT を正しく実装
**対象ファイル**:
- `Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts`(大幅改造)
- `Miyabi/packages/task-manager/src/sync/github-evidence-fetcher.ts`NEW
#### 5.1 GitHub Evidence Fetcher
```typescript
export class GitHubEvidenceFetcher {
constructor(private octokit: Octokit, private owner: string, private repo: string) {}
// R3-4: PR API から merge commit を取得
async fetchPREvidence(prNumber: number): Promise<GitHubEvidence> {
const { data: pr } = await this.octokit.pulls.get({
owner: this.owner, repo: this.repo, pull_number: prNumber
});
return {
prNumber,
prHeadRef: pr.head.ref,
prState: pr.merged ? 'MERGED' : pr.state === 'closed' ? 'CLOSED' : 'OPEN',
mergeCommitSha: pr.merge_commit_sha,
mergedAt: pr.merged_at,
reviewDecision: await this.fetchReviewDecision(prNumber),
issueState: await this.fetchLinkedIssueState(prNumber),
issueClosedByPR: await this.checkIssueClosedByPR(prNumber),
};
}
// R3-3: Issue が PR merge で close されたか検証
private async checkIssueClosedByPR(prNumber: number): Promise<boolean> {
// GitHub GraphQL: pullRequest.closingIssuesReferences
}
}
```
#### 5.2 同期の再設計R3-1: authority-aware bidirectional
```typescript
export type SyncDirection =
| 'push_proposal' // ローカル → GitHub提案
| 'pull_authoritative'; // GitHub → ローカル(事実の取得)
export class DeterministicSync {
// R3-1: 何の事実はどちらが authoritative かを状態ごとに定義
private authority: Record<string, 'github' | 'local'> = {
'taskCompletion': 'github', // done/merged は GitHub が authority
'executionLock': 'local', // lock/DAG はローカルが authority
'reviewStatus': 'github', // review 結果は GitHub が authority
'assignedAgent': 'local', // アサインはローカルが authority
};
// R3-2: GitHub API 障害時
async syncWithDegradedMode(tasks: ManagedTask[]): Promise<SyncResult> {
try {
return await this.fullSync(tasks);
} catch (error) {
if (this.isGitHubAPIError(error)) {
// awaiting_github_sync に遷移
// retry queue に追加
// event: github_sync_failed を記録
return { degraded: true, retryAt: new Date(Date.now() + 60000) };
}
throw error;
}
}
}
```
**Rust 連携**: `rust-ai-pipeline/src/remote.rs``ssh_exec()` + `RemoteResult` パターンを GitHub API 呼び出しの結果型に流用。success/stdout/stderr の三つ組は `GitHubAPIResult` にそのまま適用可能。
**テスト**:
- PR merged → evidence 取得 → merged 遷移
- Issue close by non-PR → done に遷移しないことを検証
- GitHub API 障害 → awaiting_github_sync 遷移
- retry 後の復旧
**完了条件**: 全テスト GREEN
---
### Phase 6: DeterministicExecutionProtocol核心
**目的**: 全 GATE を1つのクラスに集約。唯一の状態遷移窓口。
**新ファイル**: `Miyabi/packages/task-manager/src/protocol/deterministic-execution.ts`
**依存**: Phase 1-5 の全コンポーネント
```typescript
export class DeterministicExecutionProtocol {
private stateMachine: TaskStateMachine; // Phase 2
private dag: TaskDAG; // Phase 2
private lockManager: FileLockManager; // Phase 4
private eventStore: EventStore; // Phase 3
private snapshotStore: SnapshotStore; // Phase 3
private sync: DeterministicSync; // Phase 5
private evidenceFetcher: GitHubEvidenceFetcher; // Phase 5
// ==========================================
// GATE 0: タスク登録Issue 必須)
// ==========================================
registerTask(input: CreateTaskInput & {
githubIssueNumber: number;
completionMode: CompletionMode;
}): ManagedTask {
// GATE: githubIssueNumber > 0
// GATE: completionMode は明示的に指定
// EVENT: state_transition (→ draft)
// STORE: snapshot に追加
}
// ==========================================
// GATE 1: DAG 登録(循環依存拒否)
// ==========================================
addToDAG(taskId: string, dependencies: string[], softDependencies: string[]): void {
// GATE: dag.detectCycle() === null
// GATE: dependencies は全て既存タスク ID
// EVENT: dag_changed
}
// ==========================================
// GATE 2: 依存解決チェック
// ==========================================
checkDependencies(taskId: string): 'ready' | 'blocked' {
// GATE: dependencies.every(d => ['done', 'merged'].includes(store.get(d).currentState))
// blocked → pending は dep 解決後に自動遷移
// EVENT: state_transition (pending → analyzing) or (pending → blocked)
}
// ==========================================
// GATE 3: Impact 分析記録
// ==========================================
recordImpact(taskId: string, impact: TaskImpact): void {
// GATE: impact.analyzedCommit === current HEAD
// GATE: impact.inputHash matches current file set
// GATE: HIGH/CRITICAL → humanApproval.required = true
// EVENT: impact_recorded
}
// ==========================================
// GATE 3.5: 人間承認HIGH/CRITICAL のみ)
// ==========================================
recordHumanApproval(taskId: string, approvedBy: string, reason: string): void {
// GATE: task.impact.riskLevel in ['HIGH', 'CRITICAL']
// GATE: task.humanApproval.required === true
// EVENT: human_approved
}
// ==========================================
// GATE 4: アサイン + ロック獲得(原子的)
// ==========================================
assignAndLock(taskId: string, agent: string, node: string, files: string[]): void {
// GATE: impact !== null
// GATE: HIGH/CRITICAL → humanApproval.approvedAt !== null
// GATE: lockManager.hasConflict(files) === false
// ATOMIC: OS flock → re-read → re-check → write → release
// EVENT: lock_acquired
// EVENT: state_transition (analyzing → implementing)
}
// ==========================================
// Heartbeat実装中に定期実行
// ==========================================
heartbeat(taskId: string, agent: string, node: string): void {
// GATE: task.lock.lockedBy === `${agent}@${node}`
// EVENT: lock_heartbeat
}
// ==========================================
// GATE 5: ブランチ記録
// ==========================================
recordBranch(taskId: string, branchName: string): void {
// GATE: /^(feature|fix|hotfix)\/issue-\d+-/.test(branchName)
// GATE: git branch --list で実在確認
// EVENT: branch_created
}
// ==========================================
// GATE 6: PR 記録
// ==========================================
async recordPR(taskId: string, prNumber: number): Promise<void> {
// GATE: prNumber > 0
// GATE: branchName !== null
// VERIFY: evidenceFetcher.fetchPREvidence(prNumber)
// GATE: evidence.prHeadRef === task.branchName
// GATE: evidence.prState === 'OPEN'
// EVENT: pr_created
// EVENT: state_transition (implementing → reviewing)
}
// ==========================================
// GATE 7: Merge 検証
// ==========================================
async verifyMerge(taskId: string): Promise<void> {
// FETCH: evidenceFetcher.fetchPREvidence(task.githubEvidence.prNumber)
// GATE: evidence.prState === 'MERGED'
// GATE: evidence.mergeCommitSha matches /^[0-9a-f]{40}$/
// GATE: evidence.reviewDecision === 'APPROVED'
// EVENT: merge_verified
// EVENT: state_transition (reviewing → merged)
// EFFECT: lockManager.releaseLock(taskId)
// EFFECT: dependents の blocked → pending 遷移
}
// ==========================================
// GATE 8: 完了確定
// ==========================================
async confirmDone(taskId: string): Promise<void> {
// FETCH: evidence.issueState
// GATE: evidence.issueClosedByPR === true (github-pr mode)
// OR: completionMode === 'manual' + humanApproval
// OR: completionMode === 'external-op' + humanApproval
// EVENT: state_transition (merged → done)
// EVENT: audit_recorded
// EFFECT: worklog に記録
// EFFECT: agent-skill-bus に record-run
}
// ==========================================
// Escape HatchesR3-5: 監査可能な例外経路)
// ==========================================
forceUnlock(taskId: string, reason: string, operator: string): void {
// EVENT: lock_released (forced=true, reason, operator)
}
reconcileFromGitHub(taskId: string): Promise<void> {
// GitHub の状態を pull して snapshot を上書き
// EVENT: github_synced (reconcile)
}
manualComplete(taskId: string, reason: string, operator: string): void {
// GATE: completionMode === 'manual' or 'external-op'
// EVENT: state_transition (→ done) + human_approved
}
}
```
**Rust 連携**: `rust-ai-pipeline` の Phase 1 パイプラインbuild → clippy → test、最初の失敗で打ち切りと同じパターン。GATE 0 → 1 → ... → 8 を順に実行し、最初の GATE 失敗で打ち切る。`PipelineReport``steps[]``failure_kind` をそのまま GATE の通過/拒否記録に流用。
将来的には、Rust で Phase 1 パイプラインbuild/clippy/testを GATE 4.5 として組み込み、「コンパイルが通らないコードは implementing → reviewing に遷移できない」という品質ゲートにする。
**テスト**:
- happy path: draft → ... → done の全シーケンス
- GATE 拒否: 各 GATE で条件不足 → 遷移不可
- 競合: 2タスクが同じファイルをロック → 1つだけ成功
- escape hatch: forceUnlock → 理由記録
- GitHub 障害: verifyMerge → awaiting_github_sync → retry → merged
**完了条件**: 全テスト GREEN
---
### Phase 7: CLI コマンド
**目的**: Protocol を CLI から操作可能にする
**新ファイル**: `Miyabi/packages/task-manager/src/cli/deterministic-cli.ts`
```bash
miyabi task register --issue 45 --title "JWT移行" --deps task-000 --mode github-pr
miyabi task dag # DAG 可視化(ターミナル)
miyabi task dispatchable # 実行可能タスク一覧
miyabi task assign task-001 --agent kotowari-dev --node macbook-pro --files "src/auth/*.ts"
miyabi task heartbeat task-001 # lease 更新
miyabi task status [task-id] # 状態確認JSON or human-readable
miyabi task locks # ロック一覧
miyabi task unlock task-001 --reason "agent crashed" --operator hayashi
miyabi task verify-merge task-001 # GitHub から merge 証跡を取得
miyabi task confirm-done task-001 # 完了確定
miyabi task sync # GitHub 同期
miyabi task events [task-id] # event log 表示
miyabi task rebuild-snapshot # snapshot を event log から再構築
miyabi task reconcile task-001 # GitHub から強制同期
```
**Rust 連携**: `rust-ai-pipeline` の CLI パターン(`parse_cli_args()``CliMode` enum → `run_*()` 関数)を参考に、サブコマンドを enum で定義。`ensure_cargo_project()` のような前提条件チェックを各コマンドの冒頭に配置。
**完了条件**: 全サブコマンドのヘルプ表示 + register → assign → verify-merge → confirm-done の E2E テスト
---
### Phase 8: 統合テスト + Agent Skill Bus 記録
**目的**: 全 Phase を結合して1タスクの draft → done を通す
**テストシナリオ**:
```
1. gh issue create → Issue #N
2. miyabi task register --issue N --title "test" --mode github-pr
3. miyabi task dag → Level 0 に task 表示
4. miyabi task dispatchable → task が ready
5. GNI impact 実行 → miyabi task record-impact
6. miyabi task assign --agent test --node local --files "src/test.ts"
7. git checkout -b feature/issue-N-test
8. miyabi task record-branch feature/issue-N-test
9. echo "// test" >> src/test.ts && git commit
10. gh pr create → PR #M
11. miyabi task record-pr M
12. gh pr merge M
13. miyabi task verify-merge
14. miyabi task confirm-done
15. miyabi task status → done ✅
16. gh issue view N → Closed ✅
17. npx agent-skill-bus record-run → recorded ✅
```
**rust-ai-pipeline 統合テスト(将来)**:
```
Step 6.5: ai-pipeline phase1 --project . --format json
→ all_passed === true なら GATE 4.5 通過
→ failure_kind !== null なら implementing に留まる
```
**完了条件**: E2E テスト GREEN、event log に全ステップの記録あり、snapshot が正しく再構築可能
---
## 4. 実装順序と依存関係 DAG
```
Phase 0 ──→ Phase 1 ──→ Phase 2 ──→ Phase 3 ──→ Phase 4
│ │
└─────┬─────┘
v
Phase 5 ──→ Phase 6 ──→ Phase 7 ──→ Phase 8
```
| Phase | 依存 | 新規コード量 | リスク |
|-------|------|-------------|--------|
| 0 | なし | 0 | LOW |
| 1 | 0 | ~80 行 | LOW |
| 2 | 1 | ~250 行 | MEDIUMステートマシン改造 |
| 3 | 1 | ~200 行 | MEDIUMatomic write |
| 4 | 3 | ~150 行 | MEDIUMlease + heartbeat |
| 5 | 3 | ~200 行 | HIGHGitHub API 依存) |
| 6 | 2,3,4,5 | ~300 行 | HIGH核心の統合 |
| 7 | 6 | ~150 行 | LOWCLI ボイラープレート) |
| 8 | 7 | ~100 行 | LOWE2E テスト) |
| **合計** | | **~1,430 行** | |
---
## 5. エージェント割り当てMaestro Playbook 実行時)
| Phase | 推奨エージェント | 理由 |
|-------|---------------|------|
| 0 | Claude Codeローカル | テスト実行 + インデックス確認 |
| 1 | Codexworktree | 型定義のみ、副作用なし |
| 2 | Claude Codeローカル | ステートマシン改造は対話が必要 |
| 3 | Codexworktree | event store は独立実装可能 |
| 4 | Codexworktree | lock manager も独立実装可能 |
| 5 | Claude Codeローカル | GitHub API の挙動確認が必要 |
| 6 | Claude Codeローカル | 全コンポーネント統合は対話が必要 |
| 7 | Codexworktree | CLI ボイラープレート |
| 8 | Claude Codeローカル | E2E テストは実環境が必要 |
**並列実行可能**:
- Phase 3 と Phase 4 は同時実行可能Codex 2体
- Phase 2 は Phase 3/4 と並行可能(依存は Phase 1 のみ)
---
## 6. リスク軽減策
| リスク | 軽減策 |
|--------|--------|
| ステートマシン改造で既存テストが壊れる | Phase 2 開始前に既存テストを全部 GREEN にする |
| atomic write の OS flock がクロスプラットフォームで動かない | macOS + Linux でテスト。Windows は SMB 経由のため advisory lock は非保証 → 単一 writer に fallback |
| GitHub API レート制限 | secondary rate limit 対応の exponential backoff + 5000 req/hr の予算管理 |
| event log が肥大化 | 30日以上前の event は archive + snapshot rebuild で圧縮 |
| Rust pipeline 統合が遅れる | Phase 6 の GATE 4.5 は optional。なくても draft → done は通る |
---
## 7. 検証基準(全 Phase 完了後)
| # | 検証項目 | 方法 |
|---|---------|------|
| 1 | 全 GATE が条件不足で拒否する | 各 GATE の境界値ユニットテスト |
| 2 | LLM が GATE をバイパスできない | Protocol 以外の経路で applyTransition を呼ぶテスト → 失敗 |
| 3 | 2エージェントが同一ファイルをロックできない | concurrent acquire テスト |
| 4 | 依存未解決タスクが analyzing に入れない | DAG + ステートマシン結合テスト |
| 5 | merge commit なしに done に遷移できない | GATE 7 のユニットテスト |
| 6 | GitHub 障害時に安全に停止する | mock API + awaiting_github_sync テスト |
| 7 | event log から snapshot を正確に再構築できる | rebuild + diff テスト |
| 8 | E2E: draft → done の全シーケンス | Phase 8 の統合テスト |
---
_This playbook is the single source of truth for implementation. Do not start coding until Phase 0 is GREEN._

427
docs/dtp/UML.md Normal file
View file

@ -0,0 +1,427 @@
# Deterministic Task Execution Protocol — UML Diagrams
---
## 1. As-Is vs To-Be: タスク完了までのシーケンス比較
### As-Is現状: LLM の自己申告ベース)
```mermaid
sequenceDiagram
participant H as Human
participant LLM as Agent (LLM)
participant GH as GitHub
H->>LLM: 「認証機能を直して」
LLM->>LLM: (考える)
LLM->>LLM: ファイルを編集
Note over LLM: impact分析なし
Note over LLM: ロックなし
Note over LLM: 別エージェントも同時編集可能
LLM-->>H: 「完了しました!」
H->>GH: Issue確認
Note over GH: Issue: Open ❌
Note over GH: PR: なし ❌
Note over GH: merge: なし ❌
H-->>LLM: 「完了してないじゃん」
LLM->>LLM: (やり直し...)
```
### To-Be実装後: GATE による確定的制御)
```mermaid
sequenceDiagram
participant H as Human
participant P as Protocol (tasks.json)
participant LLM as Agent (LLM)
participant GNI as GitNexus
participant GH as GitHub
H->>GH: Issue #45 作成
H->>P: registerTask(issue=45)
P->>P: GATE 0: issue存在? ✅
P->>P: GATE 2: 依存解決? ✅
P->>P: draft → pending → analyzing
LLM->>GNI: gitnexus_impact("AuthController")
GNI-->>P: {riskLevel: HIGH, symbols: 12}
P->>P: GATE 3: impact記録済 ✅
P->>H: ⚠️ HIGH risk — 承認必要
H-->>P: 承認
P->>P: GATE 4: ファイルロック競合なし? ✅
P->>P: lock獲得 + analyzing → implementing
LLM->>LLM: ブランチ作成 + コード修正
LLM->>P: recordBranch("feature/issue-45-auth")
P->>P: GATE 5: ブランチ名検証 ✅
LLM->>GH: gh pr create → PR #78
LLM->>P: recordPR(78)
P->>P: GATE 6: prNumber > 0 ✅
P->>P: implementing → reviewing
H->>GH: Review → Approve → Merge
GH-->>P: mergeCommit = "a1b2c3d4..."
P->>P: GATE 7: 40文字hex ✅
P->>P: reviewing → done
P->>P: ロック解放 + 後続タスク解放
P->>GH: Issue #45 → Closed ✅
```
---
## 2. ステートマシン: 既存 vs 拡張
### As-Is既存: 条件チェックなし)
```mermaid
stateDiagram-v2
[*] --> draft
draft --> pending
pending --> analyzing
pending --> implementing: skip_analysis
analyzing --> implementing
implementing --> reviewing
reviewing --> done
reviewing --> implementing: needs_revision
note right of pending: 依存チェックなし\n誰でも遷移可能
note right of analyzing: impact なしでも\nimplementingに行ける
note right of reviewing: PRなくても\ndoneに行ける
```
### To-Be拡張: 全遷移に GATE
```mermaid
stateDiagram-v2
[*] --> draft: GATE 0\ngithubIssueNumber > 0
draft --> pending: GATE 1\ntitle + description + deps[]
pending --> analyzing: GATE 2\ndeps.every(done)
pending --> blocked: deps未解決
analyzing --> implementing: GATE 3 + 4\nimpact != null\nlock獲得済\nHIGH→人間承認
analyzing --> blocked: ロック競合
implementing --> reviewing: GATE 5 + 6\nbranchName valid\nprNumber > 0
reviewing --> done: GATE 7\nmergeCommit = /[0-9a-f]{40}/
done --> [*]: GATE 8\nworklog記録 + Issue Closed
blocked --> pending: 依存解決 or ロック解放
note right of draft: Issue がなければ\n登録すら不可能
note right of analyzing: GNI分析が空なら\n実装に入れない
note right of implementing: ロックがなければ\nコード編集不可
note right of reviewing: merge commitなしに\n完了は不可能
```
---
## 3. ファイルロック: 競合防止の仕組み
### As-Isロックなし
```mermaid
sequenceDiagram
participant A as Agent A (MacBook)
participant F as auth.controller.ts
participant B as Agent B (Windows)
A->>F: 編集開始
B->>F: 編集開始(同時)
Note over F: 💥 競合!
A->>F: コミット
B->>F: コミット(上書き)
Note over F: Agent A の変更が消失
```
### To-Beファイルロック付き
```mermaid
sequenceDiagram
participant A as Agent A (MacBook)
participant T as tasks.json
participant B as Agent B (Windows)
participant F as auth.controller.ts
A->>T: assignAndLock(task-001, files=[auth.controller.ts])
T->>T: fileLocks["auth.controller.ts"] = "task-001" ✅
T-->>A: ロック獲得OK
B->>T: assignAndLock(task-002, files=[auth.controller.ts])
T->>T: fileLocks["auth.controller.ts"] 既にロック中 ❌
T-->>B: Error: "task-001がロック中"
A->>F: 編集 → commit → PR → merge
A->>T: recordMerge(task-001, "a1b2c3...")
T->>T: fileLocks["auth.controller.ts"] 解放
B->>T: assignAndLock(task-002, files=[auth.controller.ts])
T->>T: fileLocks 空 ✅
T-->>B: ロック獲得OK
B->>F: 編集開始(安全)
```
---
## 4. DAG 依存関係: 実行順序の強制
```mermaid
graph TD
subgraph "DAG Level 0最初に実行"
T0[task-000<br/>DBスキーマ変更<br/>state: done ✅]
end
subgraph "DAG Level 1Level 0 完了後)"
T1[task-001<br/>API実装<br/>state: implementing 🔒]
end
subgraph "DAG Level 2Level 1 完了後)"
T2[task-002<br/>フロントエンド<br/>state: blocked ⏳]
T3[task-003<br/>テスト作成<br/>state: blocked ⏳]
end
T0 -->|"hard dep"| T1
T1 -->|"hard dep"| T2
T1 -->|"hard dep"| T3
style T0 fill:#22c55e,color:#fff
style T1 fill:#3b82f6,color:#fff
style T2 fill:#f59e0b,color:#fff
style T3 fill:#f59e0b,color:#fff
```
```mermaid
sequenceDiagram
participant DAG as DAG Engine
participant SM as State Machine
participant T0 as task-000 (DB)
participant T1 as task-001 (API)
participant T2 as task-002 (Frontend)
Note over DAG: Level 0 実行
T0->>SM: pending → implementing → done ✅
DAG->>DAG: task-000 done → Level 1 解放
Note over DAG: Level 1 実行
T1->>SM: blocked → pending依存解決
T1->>SM: pending → analyzing → implementing
Note over T2: task-001 がまだ done じゃない
T2->>SM: pending → analyzing を試みる
SM-->>T2: ❌ GATE 2 拒否: task-001 が done ではない
T1->>SM: implementing → reviewing → done ✅
DAG->>DAG: task-001 done → Level 2 解放
Note over DAG: Level 2 実行(並列可能)
T2->>SM: blocked → pending ✅
```
---
## 5. 全体アーキテクチャ: 3ピース統合
### As-Is分散・未接続
```mermaid
graph LR
subgraph "Miyabi task-manager"
SM[State Machine<br/>27 rules]
SYNC[GitHub Sync<br/>bidirectional]
EXEC[Task Executor<br/>worktree]
end
subgraph "KOTOWARI openclaw-crowd"
DAG[DAG Engine<br/>topological sort]
SCHED[Scheduler<br/>priority queue]
end
subgraph "agent-skill-bus"
QUEUE[JSONL Queue<br/>prompt requests]
LOCK[File Locks<br/>TTL-based]
end
SM -.->|"未接続"| DAG
SM -.->|"未接続"| LOCK
DAG -.->|"未接続"| LOCK
style SM fill:#ef4444,color:#fff
style DAG fill:#ef4444,color:#fff
style LOCK fill:#ef4444,color:#fff
```
### To-Be統合: DeterministicExecutionProtocol
```mermaid
graph TB
subgraph "DeterministicExecutionProtocol"
direction TB
subgraph "GATE Layer門番"
G0[GATE 0: Issue存在]
G2[GATE 2: 依存解決]
G3[GATE 3: Impact記録]
G4[GATE 4: ロック獲得]
G6[GATE 6: PR作成]
G7[GATE 7: Merge確定]
end
subgraph "Engine Layer実行"
SM[State Machine<br/>27 rules + GATE条件]
DAG[DAG Engine<br/>Kahn sort + 依存解決]
LOCK[File Lock Manager<br/>TTL + 競合検出]
end
subgraph "Store Layer永続化"
TJ[tasks.json<br/>確定的スキーマ]
GH[GitHub Issues/PR<br/>Fact SSOT]
end
end
G0 --> SM
G2 --> DAG
G3 --> SM
G4 --> LOCK
G6 --> SM
G7 --> SM
SM --> TJ
DAG --> TJ
LOCK --> TJ
TJ <-->|"bidirectional sync"| GH
style G0 fill:#f59e0b,color:#000
style G2 fill:#f59e0b,color:#000
style G3 fill:#f59e0b,color:#000
style G4 fill:#f59e0b,color:#000
style G6 fill:#f59e0b,color:#000
style G7 fill:#f59e0b,color:#000
style SM fill:#22c55e,color:#fff
style DAG fill:#22c55e,color:#fff
style LOCK fill:#22c55e,color:#fff
style TJ fill:#3b82f6,color:#fff
style GH fill:#3b82f6,color:#fff
```
---
## 6. マルチマシン協調: Before / After
### As-Is
```mermaid
graph LR
subgraph MacBook
A1[Agent A<br/>auth.ts 編集中]
M1[MEMORY.md<br/>v3]
end
subgraph Windows
A2[Agent B<br/>auth.ts 編集中]
M2[MEMORY.md<br/>v2 ← 古い!]
end
subgraph mainmini
A3[Agent C<br/>何をしてるか不明]
M3[MEMORY.md<br/>v1 ← もっと古い!]
end
A1 -.->|"💥 競合"| A2
M1 -.->|"❌ ズレ"| M2
M2 -.->|"❌ ズレ"| M3
```
### To-Be
```mermaid
graph TB
subgraph "tasks.json共有 SSOT"
TJ["task-001: implementing<br/>lock: Agent A @ MacBook<br/>files: [auth.ts] 🔒<br/><br/>task-002: blocked ⏳<br/>depends: task-001<br/><br/>task-003: pending<br/>files: [user.ts] — 別ファイルなので並行OK"]
end
subgraph MacBook
A1[Agent A<br/>auth.ts 編集 ✅<br/>ロック保持]
end
subgraph Windows
A2[Agent B<br/>auth.ts 触れない 🚫<br/>task-002 blocked]
end
subgraph mainmini
A3[Agent C<br/>user.ts 編集 ✅<br/>task-003 別ロック]
end
A1 -->|"read/write"| TJ
A2 -->|"read only"| TJ
A3 -->|"read/write"| TJ
TJ <-->|"sync"| GH[GitHub Issues<br/>Fact SSOT]
```
---
## 7. GATE チェーン: 1タスクの完全ライフサイクル
```mermaid
graph LR
START((Start)) --> G0{GATE 0<br/>Issue?}
G0 -->|No| REJECT0[❌ 登録拒否]
G0 -->|Yes| DRAFT[draft]
DRAFT --> G1{GATE 1<br/>title+desc?}
G1 -->|No| REJECT1[❌]
G1 -->|Yes| PENDING[pending]
PENDING --> G2{GATE 2<br/>deps done?}
G2 -->|No| BLOCKED[blocked ⏳]
G2 -->|Yes| ANALYZING[analyzing]
ANALYZING --> G3{GATE 3<br/>impact?}
G3 -->|No| REJECT3[❌ GNI回せ]
G3 -->|HIGH| HUMAN{人間承認?}
G3 -->|LOW/MED| G4
HUMAN -->|No| REJECT_H[❌]
HUMAN -->|Yes| G4
G4{GATE 4<br/>lock?} -->|競合| REJECT4[❌ 待て]
G4 -->|OK| IMPL[implementing 🔒]
IMPL --> G5{GATE 5<br/>branch?}
G5 -->|No| REJECT5[❌]
G5 -->|Yes| G6{GATE 6<br/>PR?}
G6 -->|No| REJECT6[❌]
G6 -->|Yes| REVIEW[reviewing]
REVIEW --> G7{GATE 7<br/>merge?}
G7 -->|No| REJECT7[❌]
G7 -->|Yes| DONE[done ✅]
DONE --> G8{GATE 8<br/>logged?}
G8 -->|Yes| FIN((End))
BLOCKED -->|"dep解決"| PENDING
style REJECT0 fill:#ef4444,color:#fff
style REJECT1 fill:#ef4444,color:#fff
style REJECT3 fill:#ef4444,color:#fff
style REJECT_H fill:#ef4444,color:#fff
style REJECT4 fill:#ef4444,color:#fff
style REJECT5 fill:#ef4444,color:#fff
style REJECT6 fill:#ef4444,color:#fff
style REJECT7 fill:#ef4444,color:#fff
style DONE fill:#22c55e,color:#fff
style BLOCKED fill:#f59e0b,color:#000
style IMPL fill:#3b82f6,color:#fff
```
---
_Generated: 2026-04-10 | Deterministic Task Execution Protocol UML_

View file

@ -0,0 +1,194 @@
# DAG Integrity / Distributed Coordination Review
対象:
- Plan: `/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md`
- DAG 実装: `KOTOWARI/skills/openclaw-crowd/src/scheduling/task-dependency-graph.ts`
- Queue/Lock 実装: `/Users/shunsukehayashi/dev/tools/agent-skill-bus/src/queue.js`
## Findings
### 1. Critical: `tasks.json` を単一の truth + lock table に集約すると、分散書き込みで整合性が壊れます
- Plan は `fileLocks``dagLevels``tasks.json` に統合する前提です (`snuggly-bouncing-turtle.md:115-117`, `:217-223`, `:345-353`)。
- しかし現行 queue は JSONL を append 中心で扱い、少なくとも lock 取得だけは append-only に寄せています (`queue.js:25-27`, `:149-157`)。一方で status 更新は全体書き換えで、ここでも既に lost update リスクがあります (`queue.js:241-246`)。
- Plan の TaskStore は `load() -> mutate -> save()` 型に見え、分散ノードが同時に `tasks.json` を更新すると、lock 取得・状態遷移・dagLevels 再計算が互いを上書きしやすいです (`snuggly-bouncing-turtle.md:204-223`)。
影響:
- 2 エージェントが同時に lock 取得できたように見える
- `currentState``fileLocks` が食い違う
- `dagLevels` が古いまま残る
- GitHub sync の巻き戻しが他のローカル更新を消す
推奨:
- `tasks.json` を monolithic source of truth にするなら、必須で「単一 writer」か「OS ファイルロック + compare-and-swap + revision 番号」を入れるべきです。
- そうしないなら、`tasks.json` は snapshot/read model に留め、競合しやすいイベントは JSONL event log に分離する方が安全です。
- 最低でも `syncVersion` を gate に使い、`save()``expectedVersion` 不一致なら失敗させるべきです。
### 2. Critical: lock 競合判定が TOCTOU で、分散協調の核心がまだ閉じていません
- queue 実装でも `getDispatchable()` の判定と `startExecution()` の lock 獲得は別ステップです (`queue.js:79-131`, `:134-161`)。これは単機能 queue としては妥当ですが、分散 coordinator の最終形には不十分です。
- Plan の `assignAndLock()``hasConflict(files) === false` を gate にしていますが (`snuggly-bouncing-turtle.md:159-164`)、チェックと書き込みが原子的である保証が計画上ありません。
影響:
- 2 ノードが同時に `hasConflict=false` を観測して両方進む
- 同一ファイルの逐次タスクが並行実行される
推奨:
- `assignAndLock()` は「read-check-write」を 1 つの原子操作にするべきです。
- 具体案:
- `flock` 相当で store 全体に短時間のメタロックを取る
- 再読込
- conflict 再判定
- lock 書き込み + state 遷移 + version increment
- fsync/atomic rename
- sequential task は「同じファイルを使うから level を落とす」のではなく、dispatch 時に lock が直列化する設計で十分です。ただしその lock は原子的である必要があります。
### 3. High: `softDependencies` を DAG edge として扱うと、parallel levels が過度に直列化されます
- 現行 DAG は soft edge を保持しても、実行可否では hard のみブロックし、soft は待ちません (`task-dependency-graph.ts:92-129`, `:272-279`)。
- Plan は `hard/soft``dependencies`/`softDependencies` に分けると言いながら (`snuggly-bouncing-turtle.md:100-103`)、`dagLevels` をトポロジカルソート結果のキャッシュとして持ちます (`:217-223`)。
問題:
- soft dep を level 計算に混ぜると、「本来は並列に走れるが、できれば後にしたい」タスクまで別レベルへ押し出されます。
- 逆に soft dep を level 計算から完全に外すと、UI の見た目と dispatch 順がずれる可能性があります。
推奨:
- hard DAG と soft preference を分離してください。
- `dagLevels` は hard dependencies のみで計算する。
- soft deps は level を変えず、同一 ready set 内の優先順位付けにだけ使う。
- ルール例:
- hard dep 未完了: `blocked`
- hard dep 完了かつ soft dep 未完了: `ready_with_soft_wait`
- dispatcher は `ready` より後ろに置くが、空いていれば実行可
### 4. High: `dagLevels` キャッシュは無効化条件が広く、現行計画だと stale になりやすいです
- Plan は `dagLevels` を永続キャッシュとして持ちます (`snuggly-bouncing-turtle.md:217-223`, `:349-353`)。
- しかし DAG level は「依存辺の変更」だけでなく、「タスク追加/削除」「hard/soft の切替」「依存先の存在消失」「reopen/retry に伴う実行可能性の再評価」の影響を受けます。
- さらに level 自体は構造キャッシュであり、`done`/`blocked` は構造ではなく状態です。両者を同じ配列に期待すると意味が混ざります。
推奨:
- `dagLevels` を durable state にしない方がよいです。読み込み時再計算、または memoized cache に留めるのが安全です。
- 永続化するなら `dagHash` を併記し、次の変更で必ず invalidate:
- task add/remove
- dependency add/remove
- hard/soft type change
- import/sync による DAG 修正
- task status 変更では `dagLevels` 自体は invalidate せず、別に `readySetComputedAt` などを持つ方が責務分離できます。
### 5. Medium: GitHub を常に優先すると DAG truth と execution truth がねじれます
- Plan は `conflictStrategy: deterministic` で GitHub を常に優先し、ローカル done を blocked に巻き戻す方針です (`snuggly-bouncing-turtle.md:230-235`)。
- ただし GitHub Issue の open/closed は DAG 実行状態の完全な proxy ではありません。merge 後に close が遅れる、手動 reopen、Issue と PR が 1:1 でない、などが普通に起きます。
影響:
- `recordMerge()` で done にした後、sync が blocked に戻す
- downstream 解放済みなのに upstream を blocked に戻し、DAG の一貫性が崩れる
推奨:
- 完了確定は Issue 状態単独ではなく、`mergeCommit` と PR merge state を優先してください。
- GitHub Issue open/closed は advisory signal に留めるべきです。
## Direct Answers
### Is Kahn algorithm the right choice?
はい、`hard dependency` の DAG 構造検証と level 計算には妥当です。
ただし用途は限定すべきです。
- 向いている:
- cycle のない順序の算出
- hard deps ベースの parallel levels
- ready set の基礎計算
- 向いていない:
- soft deps を含む dispatch policy
- file lock を含む最終 dispatch 判定
- retry/reopen/lease expiry を含む実行状態管理
結論:
- `Kahn for structure`
- `state machine for lifecycle`
- `lock manager for actual dispatch`
この三層分離がよいです。
### How should soft deps work with DAG levels?
soft dep は level を作る edge ではなく、同一 level 内の優先順ヒントとして扱うのが安全です。
推奨モデル:
- `hardDependencies`: 実行可否を決める
- `softDependencies`: 実行順 preference を決める
- `dagLevels`: hard のみで計算
- `dispatch score`: priority, age, softSatisfied, lockAvailability を合成
状態名を増やせるなら `ready``ready_soft_blocked` を分けると観測しやすいです。
### dagLevels cache invalidation?
永続化しないのが第一候補です。
永続化するなら DAG 構造専用キャッシュにしてください。
invalidate 条件:
- task add/remove
- dependency add/remove
- dependency type hard/soft change
- sync/import による DAG repair
invalidate 不要:
- `pending -> analyzing -> implementing -> done` などの状態遷移だけ
- lock acquire/release
### File lock for sequential tasks sharing files?
必要です。しかも task-level ではなく file-set lease として扱うべきです。
推奨:
- lock key は正規化済みファイルパス集合
- `assignAndLock()` で原子的に lease 取得
- heartbeat/renew を入れる
- TTL expiry は即 failed 固定ではなく、`orphaned_lock` として再取得可能にする
同じファイルを触る sequential tasks は DAG level が同じでも問題ありません。
実行順は lock で直列化し、必要なら soft dep で順序 preference を足します。
### JSONL vs monolithic JSON for concurrent writes?
結論:
- 高頻度イベント: JSONL が向いています
- snapshot/read model: monolithic JSON が向いています
- 単一 writer なしで monolithic JSON だけに寄せるのは危険です
おすすめ構成:
- `task-events.jsonl`
- state transition
- lock acquired/released/expired
- DAG changed
- sync reconciled
- `tasks.snapshot.json`
- 現在状態の materialized view
- 起動時または定期で再生成
もし `tasks.json` 1ファイルにこだわるなら、少なくとも:
- OS lock
- atomic rename
- version check
- crash recovery
この 4 点が必要です。
## Suggested Design Adjustment
最小変更で守りを固めるなら、以下の形がよいです。
1. DAG は hard deps のみで Kahn。
2. soft deps は dispatch priority にだけ使う。
3. lock/state/sync は event log に記録。
4. `tasks.json` は snapshot とし、壊れても再構築可能にする。
5. `assignAndLock()` を唯一の dispatch commit point にする。
6. GitHub は external confirmation であって、唯一の truth にはしない。
## Residual Risk
この計画は「LLM の揺らぎを封じる」方向性自体は非常に良いです。
ただし現状のまま `tasks.json` に lock table と cached dagLevels を押し込むと、DAG の正しさより先に分散更新の競合で壊れる可能性が高いです。最初に固めるべきはアルゴリズム選定より commit/lock の原子性です。

View file

@ -0,0 +1,139 @@
# Review: Deterministic Task Execution Protocol
## Findings
### 1. High: 計画は二層SSOTをまだ完全には実装しておらず、`tasks.json` を「事実の正」に昇格させすぎています
- 計画は「JSON の状態遷移だけが事実」と置いていますが、ビジョン文書と仕様文書はタスク状態のファクトSSOTを GitHub Issue / Projects に置いています。ここが正面衝突しています。
- Plan: [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L8) [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L12)
- Vision: [`agent-execution-and-memory-vision.md`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/docs/design/agent-execution-and-memory-vision.md#L87) [`agent-execution-and-memory-vision.md`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/docs/design/agent-execution-and-memory-vision.md#L110)
- Spec: [`context-and-impact-spec.md`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/docs/design/context-and-impact-spec.md#L54) [`context-and-impact-spec.md`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/docs/design/context-and-impact-spec.md#L67)
- 現状の設計だと `tasks.json` は execution ledger と gate engine であるべきなのに、完了判定の authority まで持っています。二層SSOTを守るなら、
- GitHub: 受入・完了・人間承認の正
- `tasks.json`: ローカル実行状態、ロック、依存、再試行、監査
に役割を分ける必要があります。
- `done` の必要条件は「`tasks.json``mergeCommit` があること」ではなく、「GitHub 上で受理された完了証跡を同期済みであること」にすべきです。`tasks.json` はそのミラーであるべきです。
### 2. High: GitHub 障害時の劣化モードが未定義で、決定性より停止性を失う可能性があります
- 計画は GitHub を強いゲートにしていますが、「GitHub API が落ちた」「レート制限」「ネットワーク断」のときに何が許可され、何が禁止されるかが定義されていません。
- Plan merge/close gating: [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L179)
- Current sync has no degraded mode, retry journal, or outage classification: [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L105) [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L163)
- このままだと障害時に次の2択になります。
- `done` を絶対に付けられず運用停止
- 人が裏口で進めて決定性崩壊
- 必要なのは「安全に止まる」中間状態です。少なくとも以下が必要です。
- `awaiting_github_sync` または `externally_completed_unverified`
- GitHub API 障害と論理矛盾を分ける error class
- pull/push の retry queue と backoff
- 最後に確認できた GitHub 証跡の timestamp
- 明示的な human override と理由記録
### 3. High: 双方向同期の計画が危険で、Issue close をそのまま `done` に写像すると誤完了になります
- 計画は「GitHub で Closed なのにローカルで implementing → pull で done に更新」としていますが、Issue は PR merge 以外の理由でも閉じられます。これをそのまま `done` とみなすのは強すぎます。
- Plan: [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L230)
- 逆方向の「ローカル done なのに Issue Open → blocked に巻き戻し」も不自然です。現行状態機械では `done -> blocked` は無効で、許されるのは `done -> pending` だけです。
- State machine: [`task-state-machine.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/state/task-state-machine.ts#L76)
- Plan rollback idea: [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L231)
- しかも既存 `BidirectionalSync.sync()` は pull 結果をローカル task/store に適用しておらず、衝突を集めるだけです。計画の「pull で done に更新」は、既存の延長では実現されません。
- [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L254)
- 推奨は one-way authority を明確化することです。
- GitHub -> ローカル: 受理・クローズ・PR merge の事実を pull
- ローカル -> GitHub: 提案された状態を push
- ただし `done` は PR merge 証跡つき close のときだけ確定
### 4. Medium: merge commit hash の取得方法と検証経路が仕様化されていません
- 計画は `recordMerge(taskId, mergeCommit)` を置いていますが、その hash をどこから、どうやって、何と突き合わせて取得するかが未定義です。
- Plan: [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L179)
- 現状の sync 実装には PR 取得や merge SHA 取得の処理がありません。Issue/Label/Project しか見ていません。
- [`github-label-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/github-label-sync.ts#L216)
- [`projects-v2-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/projects-v2-sync.ts#L344)
- 取得方法は次のように固定するのがよいです。
- `recordPR()` 時に `prNumber` を必須保存する
- merge 検証時は Issue API ではなく PR API を見る
- 第一候補: REST `GET /repos/{owner}/{repo}/pulls/{pull_number}``merge_commit_sha`
- 代替: GraphQL `pullRequest.mergeCommit.oid`
- 追加で `mergedAt`, `merged`, `state`, `baseRefOid`, `headRefOid` を保存する
- 重要なのは「Issue close から merge hash を推測しない」ことです。`prNumber` がないタスクは merge-based completion を確定できません。
### 5. Medium: 正当な例外経路が不足しており、現実運用で手動回避が増えそうです
- 仕様は HIGH/CRITICAL に human approval を要求していますが、実際の例外経路が定義されていません。
- Plan: [`snuggly-bouncing-turtle.md`](/Users/shunsukehayashi/.claude/plans/snuggly-bouncing-turtle.md#L152)
- 必要なのは「抜け道」ではなく「監査可能な escape hatch」です。例えば:
- GitHub 障害時に一時的に `reviewing` のまま凍結する
- 外部で既に merge/close されたタスクを `reconcile` で取り込む
- ドキュメント作業や運用作業のように PR を伴わない legitimate completion を `completionMode=manual|github-pr|external-op` で区別する
- 強制 unlock / reopen / superseded / abandoned の理由欄を必須化する
- 今の計画はコード変更中心の happy path には強いですが、実運用で必ず出る「PRなしで正当に終わる仕事」「外部作業」「GitHub障害中の継続作業」を吸収できません。
### 6. Medium: 現行同期実装の前提を踏まえると、Phase 6 は「強化」ではなく再設計に近いです
- 現在の `BidirectionalSync` は conflict strategy も `local-wins` がデフォルトで、`newest-wins` も未実装同然です。
- [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L23)
- [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L40)
- [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L384)
- さらに current sync は `ManagedTask` を mutate せず、store 連携もありません。
- [`task-manager.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/task-manager.ts#L240)
- [`bidirectional-sync.ts`](/Users/shunsukehayashi/dev/ops/openclaw-workspace/Miyabi/packages/task-manager/src/sync/bidirectional-sync.ts#L258)
- そのため deterministic sync を本当にやるなら、必要なのは conflict strategy 追加だけではなく、
- state reconciliation engine
- persistent sync cursor / version
- per-task sync status
- webhook event idempotency
- API failure taxonomy
の実装です。
## Direct Answers
### What if GitHub API is down?
- 今の計画だけでは未対応です。
- 推奨は fail-closed ですが、完全停止ではなく `awaiting_github_sync` へ遷移させることです。
- 実行中の作業は続けてもよいですが、`done` と lock release の一部は「GitHub未検証」のまま分離保存すべきです。
- 監査上は `completionRequestedAt`, `githubVerifiedAt`, `githubSyncError` を残すべきです。
### One-way push vs bidirectional?
- 完全双方向より「authority-aware bidirectional」が適切です。
- Push はローカルの提案状態を GitHub に反映するために使う。
- Pull は GitHub の受理状態をローカルへ反映するために使う。
- ただし authority は対称ではありません。`done`/`accepted` は GitHub 優先、実行中ロックや DAG はローカル優先です。
- 要するに transport は bidirectional、SSOT は asymmetric にすべきです。
### How to get mergeCommit hash?
- Issue ではなく PR から取得します。
- `recordPR()` 時に `prNumber` を保存し、reconcile 時にその PR を取得します。
- 実装候補:
- REST: `pulls.get(...).data.merge_commit_sha`
- GraphQL: `pullRequest.mergeCommit.oid`
- `mergeCommit` だけでなく `merged`, `mergedAt`, `headSha`, `baseSha`, `mergeMethod` も一緒に保持すると検証が安定します。
### Does the plan fully implement two-layer SSOT?
- いいえ、未完成です。
- 現状は `tasks.json` を事実の正に寄せすぎており、Vision/Spec が要求する「GitHub=ファクトSSOT、repo/tasks.json=文脈・実行ミラー」という分離が崩れています。
### Escape hatches for legitimate work?
- まだ不十分です。
- 少なくとも `manual-completion`, `external-completion`, `awaiting-github-sync`, `force-unlock-with-reason`, `reconcile-from-github` は必要です。
- どれも「自由に bypass」ではなく、理由・操作者・時刻・承認者を残すべきです。
## Recommended Changes
1. `tasks.json` の位置づけを「execution ledger / gate cache」に下げ、ファクトSSOTは GitHub のまま明記する。
2. `done` を単純 state ではなく `accepted=true` を含む受理イベントで確定させる。
3. sync を再設計し、`push proposal``pull authoritative facts` を分ける。
4. `Issue closed => done` を廃止し、`PR merged + issue closed/linked accepted` を確定条件にする。
5. GitHub 障害時の `awaiting_github_sync` 系ステートと retry journal を追加する。
6. manual/external completion の監査可能な escape hatch を先に仕様化する。
## Overall
計画の方向性自体はかなり良いです。特に DAG、ロック、ゲートを `tasks.json` に集約して LLM の自然言語を無力化する発想は強いです。ただし GitHub Sync と end-to-end deterministic guarantee の観点では、いまの文面は「ローカル deterministic engine」は強い一方で、「GitHub を authority とした分散整合」はまだ甘いです。
決定的にしたいなら、`tasks.json` と GitHub のどちらも同じように信じるのではなく、「何の事実はどちらが authoritative か」を状態ごとに切り分ける必要があります。

View file

@ -0,0 +1,310 @@
# 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 だけで封じる」保証には届きません。