docs: split CLAUDE.md tasks into parallel todo tracks

Extract all unimplemented design plans and tasks from CLAUDE.md into
5 independent todo files, each targeting a separate branch for parallel
implementation:

- TASKS-1: Memory & Performance Optimization
- TASKS-2: Subagent Orchestration (Container Model)
- TASKS-3: Session DAG (SQLite Store)
- TASKS-4: Mini App & Static Serving
- TASKS-5: Heartbeat Worktree Management

CLAUDE.md now retains only implemented references, coding guidelines,
and a summary table linking to each todo file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-04 21:20:50 +09:00
parent 5c046cecfc
commit 81a8abf1f2
6 changed files with 683 additions and 844 deletions

859
CLAUDE.md
View file

@ -19,471 +19,14 @@ Lint: `golangci-lint run`
- **Interview tool filtering**: `interviewAllowedTools` in `pkg/agent/loop.go` is the single source of truth for tools available during interview/review phases. Both `filterInterviewTools` (strips definitions before LLM call) and `isToolAllowedDuringInterview` (argument-level gating) reference this map. - **Interview tool filtering**: `interviewAllowedTools` in `pkg/agent/loop.go` is the single source of truth for tools available during interview/review phases. Both `filterInterviewTools` (strips definitions before LLM call) and `isToolAllowedDuringInterview` (argument-level gating) reference this map.
- **History clear**: `/plan start clear` wipes session history and summary on transition to executing. The Mini App review UI offers two sliders: standard approve and approve-with-clear. - **History clear**: `/plan start clear` wipes session history and summary on transition to executing. The Mini App review UI offers two sliders: standard approve and approve-with-clear.
## Mini App — Static File Serving ## Subagent Orchestration (実装済み部分)
現状は `pkg/miniapp/static/` 以下のファイルを `//go:embed` でバイナリに焼いて個別ルートで配信している。 - **Startup flag**: `--orchestration` で on/off。`SubagentsConfig.Enabled` で gate。
- **Conductor identity**: orchestration 有効時に conductor identity + spawn/subagent guidance を system prompt へ注入。
- **Sandbox/Spawn**: `pkg/tools/sandbox.go`, `pkg/tools/spawn.go` 実装済み。
- **AgentReporter**: `orch.AgentReporter` / `orch.Noop` / `orch.Broadcaster` で統一。main/heartbeat/subagent 全セッションが同一 Broadcaster に発火。Mini App は `agentLoop.GetOrchBroadcaster()``handler.SetOrchBroadcaster()` で受信。
``` ## コードの匂い — チェックリスト
pkg/miniapp/static/
index.html ← すべての JS/CSS をインライン
map.js ← オーケストレーションルーム描画
```
Go 側: `//go:embed static/index.html static/map.js` + `/miniapp/map.js` ルート。
### バンドラ導入の検討事項
- ビルドステップVite / esbuild 等)を挟む場合、成果物ディレクトリ(`static/dist/` 等)を embed する形になる
- Go 側は `//go:embed static` でディレクトリごと embed し、`http.FS` で一括配信すれば個別ルートが不要になる
- `serveMapJS` など個別ルートは汎用 static ファイルサーバーに統合できる
- `serveIndex` での `ORCH_ENABLED` 注入は、テンプレートエンジンまたはビルド時の環境変数注入に移行する必要がある
### 現時点でやっておける布石(任意)
- `/miniapp/` 以下を `http.FileServer(http.FS(staticFS))` で一括配信するよう `serveStatic` ハンドラを汎用化する
- これにより `map.js`・将来の `assets/*.js` もルート追加なしに自動配信される
## Known Gaps
- **Mini App log viewer has no frontend tests**: `renderLogs()` in `pkg/miniapp/static/index.html` is inline vanilla JS with no unit/E2E test coverage. Backend (Go) tests cover `RecentLogs`, `SanitizeFields`, and JSON serialization, but nothing verifies the JS rendering. This allowed the Fields display bug (fields sent but not rendered) to ship undetected.
- **No human intervention for heartbeat worktrees**: Heartbeat sessions create git worktrees (`.worktrees/heartbeat-YYYYMMDD/`) but there is no CLI or Mini App command to list, inspect, or manually dispose them. Need a `/plan worktrees` command (or similar) that shows active worktrees with branch/commit info and allows manual merge/dispose. `PruneOrphaned` on startup only removes directories without auto-committing first, so uncommitted changes in orphaned worktrees are silently lost.
---
## Subagent Orchestration Design
> Designed 2026-02-25 on branch `sub-agent-technical-breakdown`.
### なぜオーケストレーションか
単純な指示から可能性の木を広げることが目的。conductor は一人でやり遂げるのではなく、探索・深化・fork をサブエージェントに委ねながら大局観を保つ。
```
without orchestration:
human → conductor → (全部自分でやる) → result
常にボトルネック、逐次処理
with orchestration:
human → conductor ─┬─ scout A ─┐
├─ scout B ─┼─ synthesize → deeper insight
└─ scout C ─┘
conductor は次を考えながら並走
```
**3つの核心原則:**
1. **Fork** — 同じ問いに複数の切り口で同時探索。sequential queue ではなく tree の展開。
2. **管理職の原則** — conductor は subagent の完了を待たない。spawn したら即座に次を設計する。人員を遊ばせないことがポイント。
3. **会話の fork** — main thread (conductor ↔ human) は高レベル・戦略的に保つ。subagent への細かい指示出しは branch thread で行い、main thread を汚染しない。subagent の進捗はサマリーだけ main thread に上げる。
**spawn がデフォルト、subagent は例外:**
```
spawn = conductor が次を考え続けられる (正しい姿)
subagent = conductor が止まる (結果が絶対必要な時だけ)
```
### Architecture Overview
```
Conductor goroutine
│ ContainerRequest (task, preset, environment)
Container goroutine: provision → run → finalize
│ ContainerMessage (question / result / status)
Conductor goroutine
│ answer (question への回答)
▼ (わからなければ human に escalate)
Container goroutine (再開)
```
**escalation chain:**
```
subagent (clarifying) → question → conductor
→ conductor が答えられる: inCh に回答
→ conductor もわからない: message tool で human に投げ、回答を転送
```
### SubagentContainer
goroutine と channel でライフサイクルを表現。goroutine がブロックしている場所が現在の状態。
```go
type ContainerMessage struct {
Type string // "question" | "result" | "status"
Content string
}
type SubagentContainer struct {
inCh chan string // conductor → subagent (回答)
outCh chan ContainerMessage // subagent → conductor (質問・結果・進捗)
cancel context.CancelFunc
}
```
spawn (async) は outCh を返して即リターン。subagent (sync) はその場で result を待つ。
**tasks map 問題の解消:** goroutine 終了時に `defer orchestrator.active.Delete(id)` + `defer close(outCh)` で自動 GC。
### SubagentEnvironment (Context Injection)
conductor は subagent に必要なコンテキストを明示的に渡す。MEMORY.md からの自動注入で冗長な手動記述を排除。
```go
type SubagentEnvironment struct {
// 自動注入 (harness が埋める)
Workspace string // workspace パス
WorktreeDir string // write先、workDir として透過的に機能
// MEMORY.md から自動抽出 (inject_plan_context: true の場合)
PlanTask string // > Task: の内容
PlanContext string // ## Context セクション
Commands string // ## Commands セクション (build/test/lint)
CurrentPhase string // 対象 Phase の内容
// conductor が明示的に追加
Background string // 追加の背景・意図
Constraints string // 制約
ContextFiles []string // 参照すべきファイルリスト
}
```
spawn パラメータ例:
```json
{
"task": "Phase 2 Step 1: implement the rate limiter",
"preset": "coder",
"context_files": ["pkg/ratelimit/ratelimit.go"],
"inject_plan_context": true
}
```
### SandboxConfig
ToolRegistry.Execute() の入口で一括 enforcement。subagent は普通に tool call するつもりで透過的に sandboxed になる。
```go
type SandboxConfig struct {
Preset string
WriteRoot string // write 系ツールのパス制限
AllowedTools map[string]bool
ExecPolicy *ExecPolicy // nil = exec 不可
SpawnablePresets []string // nil = spawn 不可
}
type ExecPolicy struct {
AllowPattern string // 先頭一致 regex; マッチしたコマンドだけ実行可
}
```
**透過的隔離:** workDir = worktreeDir として設定することで、AI は自分が隔離されていることに気づかずに振る舞う。picoclaw 側で CoW 的にファイルを引き渡せる。
### Presets (5種)
| preset | 性格 | write | exec | search | spawn |
|---|---|---|---|---|---|
| `scout` | Exploratory | ✗ | ✗ | ✓ | ✗ |
| `analyst` | Exploratory | ✗ | go test/vet, git log/diff, grep | ✓ | ✗ |
| `coder` | Deliberate | ✓ sandbox | test/lint/fmt 系 | ✓ | ✗ |
| `worker` | Deliberate | ✓ sandbox | build/package manager 系 | ✓ | ✗ |
| `coordinator` | Deliberate | ✓ sandbox | go/pnpm/bun/curl 系 | ✓ | scout〜worker のみ |
**性格の分類:**
- **Exploratory** (scout/analyst): open-ended、見てきて報告。clarifying フェーズなし。
- **Deliberate** (coder/worker/coordinator): 成果物を作る。目標があいまいだと失敗する。clarifying フェーズあり。
**境界:**
- `coder` = 書いて自分で検証できる (package 追加・deploy 不可)
- `worker` = インフラも含めてやりきる (package install, CI pipeline 等)
- `coordinator` = coordinator を spawn できない (深さ自然制限)
**npm は全 preset で禁止:** git worktree に node_modules が作られると大量ファイルが生じるため。pnpm は symbolic link で済む、bun も同様。
**websearch/webfetch は全 preset で許可:** read-only・非破壊のため制限不要。
#### exec allowlist regex
```go
var presetExecPatterns = map[string]string{
"scout": ``,
"analyst": `^(go\s+(test|vet)|git\s+(log|diff|status)|curl|wget|grep|find)\b`,
"coder": `^(` +
`go\s+(test|vet|fmt)|gofmt|goimports|golangci-lint|` +
`prettier|eslint|` +
`black|ruff|` +
`cargo\s+(test|fmt|clippy)|` +
`pnpm\s+(test|run\s+(test|lint|format))|` +
`bun\s+(test|run\s+(test|lint|format))|` +
`uv\s+run\s+` +
`)\b`,
"worker": `^(` +
`go\s+|` +
`pnpm\s+(install|add|run|test|build)|` +
`bun\s+(install|add|run|test|build)|` +
`uv\s+(run|sync|add|pip\s+install)|` +
`pip\s+install|` +
`cargo\s+` +
`)\b`,
"coordinator": `^(go\s+|pnpm\s+|bun\s+|curl|wget)\b`,
}
```
### Subagent Plan Mode
Deliberate な preset (coder/worker/coordinator) は in-memory のミニ plan mode を持つ。MEMORY.md には一切触れない (ファイル参照・編集を避けるため)。
```go
type SubagentPlanState int
const (
PlanStateNone SubagentPlanState = iota // Exploratory preset
PlanStateClarifying // 目的・制約を確認中
PlanStateReview // conductor の承認待ち
PlanStateExecuting // 実行中
)
type SubagentPlan struct {
State SubagentPlanState
Goal string // clarifying で合意した目的
Approach []string // proposed なステップ
QA []QAItem // 質問・回答の履歴
mu sync.Mutex
}
```
SubagentContainer がフィールドとして保持。goroutine 終了とともに消える。
**Deliberate preset の system prompt:**
```
You are in clarifying mode. Before executing, confirm with the conductor:
1. What is the exact goal?
2. What are the constraints and acceptance criteria?
3. Are there relevant files I should know about?
Use the `message` tool to ask questions.
When you have clear answers, propose your approach (steps) for review.
Do NOT start executing until the conductor approves.
```
**Exploratory preset の system prompt:**
```
Explore and return findings. Use your best judgment when encountering ambiguity.
```
**fractal 構造:**
```
human
↕ plan mode (MEMORY.md, file-based, 永続)
conductor
↕ subagent plan mode (in-memory, 揮発)
subagent (deliberate)
```
### MEMORY.md Orchestration Section
executing 中に conductor が自由に書き込める専用エリア。システムはパースしない。
```markdown
## Orchestration
### Delegated
<!-- subagent に委譲したタスクの記録 (spawn 直後に書く) -->
- coder-1 (coder): rate limiter 実装 → Phase 2 Step 1
- scout-1 (scout): pkg/auth の構造調査
### Findings
<!-- subagent の結果から蓄積した知見 (結果受信後に書く) -->
- pkg/auth は middleware パターン、入口は middleware.go (scout-1)
- セッションストアは存在しない、JWT が有効 (scout-2)
### Decisions
<!-- fork の意思決定ログ (方向選択時に書く) -->
- auth: OAuth2 より JWT を選択 (外部依存なし、scout-2 推奨)
```
conductor の guidance に追記:
```
After spawning a subagent, record the assignment in ## Orchestration > Delegated.
When a subagent reports back, move key findings to ## Orchestration > Findings.
When you choose one direction over another, log the rationale in ## Orchestration > Decisions.
```
### Conductor Identity (System Prompt)
`pkg/agent/context.go``getIdentity()` に追加予定:
```
You are picoclaw, a conductor AI agent. Your role is to orchestrate:
break work into tasks, delegate them to subagents, and synthesize results —
rather than doing everything inline yourself.
## Orchestration
You are the conductor, not the performer. Prefer delegation over doing everything inline.
Use `spawn` (non-blocking) when:
- Tasks can run in parallel or in the background
- Multiple independent tasks can run simultaneously (spawn each one)
- You don't need the result to decide the next step
- The operation is long-running (builds, fetches, analysis, file processing)
Use `subagent` (blocking) when:
- You need the result before you can continue
- Correctness of next steps depends on the outcome
Do inline only when:
- It's a single fast tool call (read a file, quick search)
- Delegation overhead clearly outweighs the benefit
Default bias: if a task involves more than 2-3 tool calls or can run
independently, delegate it. When you spawn, immediately plan what comes next —
blocking means you've stopped thinking.
Fork aggressively: explore multiple directions simultaneously.
After spawning a subagent, record the assignment in ## Orchestration > Delegated.
When results come back, synthesize and decide the next fork.
```
### Startup Flag
テスト用途で起動時にオーケストレーション機能を on/off できるようにする。
**変更箇所:**
1. `pkg/config/config.go``SubagentsConfig``Enabled bool` を追加
2. `cmd/picoclaw/cmd_agent.go``--orchestration` フラグを追加 (default: false for now)
3. `pkg/agent/loop.go``registerSharedTools()` で spawn tool 登録を `Enabled` で gate
```go
// pkg/config/config.go
type SubagentsConfig struct {
Enabled bool `json:"enabled"`
AllowAgents []string `json:"allow_agents,omitempty"`
Model *AgentModelConfig `json:"model,omitempty"`
}
// cmd/picoclaw/cmd_agent.go
case "--orchestration":
cfg.Agents.Defaults.Subagents.Enabled = true // or toggle
```
### Implementation Files (予定)
```
pkg/tools/
container.go — SubagentContainer, ContainerRequest, ContainerMessage,
SubagentEnvironment, SubagentPlan
sandbox.go — SandboxConfig, ExecPolicy, preset 定義
orchestrator.go — Orchestrator (SubagentManager を置き換え)
spawn.go — preset / inject_plan_context パラメータ追加
pkg/agent/
context.go — conductor identity + orchestration guidance 追加
```
### AgentReporter 抽象化 (実装済み 2026-02-25)
> branch `sub-agent-technical-breakdown`
`Broadcaster``SubagentManager` 内部で生成する密結合を解消し、
`orch.AgentReporter` インターフェースを中心に置くリファクタリングを実施。
#### オーナーシップ
```
AgentLoop
├─ owns: *orch.Broadcaster (orchBroadcaster — nil when disabled)
└─ holds: orch.AgentReporter (orchReporter = Broadcaster or Noop)
├─ passes to → SubagentManager.reporter
│ └─ passes to → ToolLoopConfig.Reporter
└─ calls directly for main/heartbeat sessions
├─ runAgentLoop: ReportSpawn / ReportGC
└─ runLLMIteration: ReportStateChange
cmd_gateway.go
└─ agentLoop.GetOrchBroadcaster() → handler.SetOrchBroadcaster()
miniapp.Handler
└─ borrows *orch.Broadcaster for Subscribe/Snapshot (WS 配信)
```
#### インターフェース (`pkg/orch/reporter.go`)
```go
type AgentReporter interface {
ReportSpawn(id, label, task string)
ReportStateChange(id, state, tool string)
ReportConversation(from, to, text string)
ReportGC(id, reason string)
}
var Noop AgentReporter = &noopReporter{} // nil-free; 全メソッドが no-op
```
`Broadcaster``AgentReporter` を満たす (`ReportSpawn` 等が `Publish` のラッパー)。
#### Noop パターン
```
--orchestration なし: orchReporter = orch.Noop → 全 Report* が空振り (panic なし)
--orchestration あり: orchReporter = *Broadcaster → WS 配信
```
呼び出し側は `if reporter != nil` チェック不要。
#### イベント発火の責任分担
| 発火元 | イベント | 経由 |
|--------|---------|------|
| `runAgentLoop` | `ReportSpawn` / `ReportGC` | `al.reporter()` |
| `runLLMIteration` | `ReportStateChange("waiting"/"toolcall")` | `al.reporter()` |
| `SubagentManager.Spawn` | `ReportSpawn` | `sm.reporter` |
| `SubagentManager.runTask` | `ReportConversation` / `ReportGC` | `sm.reporter` |
| `RunToolLoop` | `ReportStateChange` | `config.Reporter` |
main / heartbeat / subagent の全セッションが同一 Broadcaster に発火するため、
canvas には全エージェントが統一して表示される。
#### 変更ファイル
- `pkg/orch/reporter.go`**新規** インターフェース + Noop
- `pkg/orch/broadcaster.go``ReportSpawn/StateChange/Conversation/GC` 追加
- `pkg/tools/toolloop.go``OnStateChange func``Reporter AgentReporter + AgentID`
- `pkg/tools/subagent.go` — constructor に `reporter` 受け取り、内部 broadcaster 廃止、`GetBroadcaster()` 削除
- `pkg/agent/loop.go``orchBroadcaster`/`orchReporter` フィールド追加、`SetOrchReporter`/`GetOrchBroadcaster` 追加、`registerSharedTools` シグネチャに `al *AgentLoop` 追加
- `cmd/picoclaw/cmd_gateway.go``GetOrchBroadcaster()``handler.SetOrchBroadcaster()`
---
## Memory Optimization Notes
> Reviewed 2026-02-24 on branch `memory-optimization-review`.
### 設計レベルの根本原因
#### D-1. MemoryStore が「ファイル = 正」でパース済み表現をキャッシュできない
`GetMemoryContext()` 1回で `ReadLongTerm()` が5回以上呼ばれる連鎖。MEMORY.md を外部エディタが直接編集できる設計上、インメモリキャッシュを自然に導入できない。
対策: (a) content パススルー方式 — 高レベルメソッドだけが1回 ReadLongTerm() を呼び、content を private ヘルパーに渡す。(b) `*ParsedPlan` 常駐 — RAM が潤沢なので MemoryStore にパース済み構造体を持たせる。edit_file 後に `InvalidateCache()` を呼ぶ。
#### D-2. `FunctionCall.Arguments` が JSON 文字列のままドメイン型に
ストリーミングループ内の重複 Unmarshal の根本原因。`ToolCall.Arguments map[string]any` のパース済みフィールドも存在するが中途半端に共存している。
#### D-3. `ToolFunctionDefinition.Parameters``map[string]any`
プロバイダーへ送るたびに Marshal が必要。`json.RawMessage` にすれば一度の marshal で済む。
#### D-4. 検索プロバイダーに共通フォーマット抽象がない
`[]string + strings.Join` パターンが3箇所に複製。`Search()` 戻り値を `string` でなく構造体にすれば1箇所で済む。
#### D-5. `Session.Messages` が可変スライスで全コピーが必要
`GetHistory()` / `Save()` での防衛的コピーは意図的設計。COW または append-only immutable 構造で解消できる。
#### D-6. `MemoryStore` のメソッド境界が「ファイル操作単位」
呼び出し側は複数の値が必要でも複数回呼ぶしかない。D-1 の解決策 (ParsedPlan 常駐) と合わせて解消。
### コードの匂い — チェックリスト
新しいコードを書くとき・レビューするときの確認事項: 新しいコードを書くとき・レビューするときの確認事項:
@ -495,388 +38,16 @@ canvas には全エージェントが統一して表示される。
6. **`var x []T` から始まる容量なし append** → ソース長が既知なら `make([]T, 0, n)` 6. **`var x []T` から始まる容量なし append** → ソース長が既知なら `make([]T, 0, n)`
7. **`[]rune(s)` 変換前に長さチェックなし** → `len(s) <= max` で ASCII fast path を先に 7. **`[]rune(s)` 変換前に長さチェックなし** → `len(s) <= max` で ASCII fast path を先に
### ストレージ保護設計 (microSD 寿命)
| データ | 現状 | 推奨戦略 | 削減率 |
|---|---|---|---|
| `sessions/*.json` | メッセージ毎書き込み | write-behind (dirty flag + 5分タイマー) | 80% |
| `state/stats.json` | LLM呼び出し毎 | 定期フラッシュのみ (5分) | 98% |
| `memory/MEMORY.md` | 即時 (変えない) | ターンスコープキャッシュ (読み取りのみ最適化) | — |
**実装ポイント:**
- `SessionManager``dirtyKeys map[string]bool` + バックグラウンドフラッシャー goroutine
- `stats.Tracker``Close()` メソッド追加 (タイマー停止 + 最終 save)
- SIGTERM/SIGINT でシャットダウンフック必須
- MEMORY.md の edit_file 書き込み後にターンキャッシュを無効化 (`InvalidateCache()`)
--- ---
## Session Management (Future) ## 未実装タスク
現状の設計は「正確性」は成熟しているが「ライフサイクル」が欠落している 以下の `todo/` ファイルに分割。各ファイルは互いに依存関係がなく、別ブランチで並列実装可能。
**近期:** | ファイル | 概要 |
- `SessionManager.Delete(key)` + TTL エビクション |---|---|
- `sessionLocks sync.Map` (loop.go) の GC | [`todo/TASKS-1.md`](todo/TASKS-1.md) | **Memory & Performance Optimization** — MemoryStore キャッシュ、FunctionCall/ToolDefinition 型整理、stats フラッシュ最適化 |
- 起動時の `loadSessions()` を遅延ロード化 | [`todo/TASKS-2.md`](todo/TASKS-2.md) | **Subagent Orchestration (Container Model)** — SubagentContainer、Orchestrator、Presets enforcement、Subagent Plan Mode |
| [`todo/TASKS-3.md`](todo/TASKS-3.md) | **Session DAG (SQLite Store)** — セッション管理の SQLite 移行、Turn ベース線形+セッション間 DAG、Fork/Report フロー |
**中期:** | [`todo/TASKS-4.md`](todo/TASKS-4.md) | **Mini App & Static Serving** — 静的配信の汎用化、バンドラ導入、フロントエンドテスト追加 |
- チェックポイント / ロールバック (`Session.Messages` を append-only immutable に) | [`todo/TASKS-5.md`](todo/TASKS-5.md) | **Heartbeat Worktree Management** — worktree 一覧・点検・手動 merge/dispose の CLI/Mini App UI |
- 名前付きセッション (`/new-session`, `/switch-session`, `/list-sessions`)
**長期:**
- `Session.ParentKey` でサブエージェントセッションをグラフ化
- クロスセッション検索 (MEMORY.md の補完として)
設計の哲学: `Session.Messages` (履歴中心) と `MEMORY.md` (知識中心) が現状共存している。どちらを主軸にするかで発展方向が変わる。
---
## Session DAG Migration Plan (Branch/Merge for Subagent Orchestration)
> Drafted 2026-03-04. Goal: make branch/fork/merge first-class in session history,
> instead of injecting subagent completion as ad-hoc system text.
### 背景
thread の有無に依存せず、subagent 運用の本質は「会話コンテキストの分岐と合流」。
現行の `sessionKey -> linear []Message` は分離には強いが、
「どの branch から何を merge したか」を構造化して保持できない。
### 目標
1. `sessionKey` の value を head 参照 (`SessionRef`) に変更し、履歴実体を DAG ノード化する
2. merge を「1回分の記憶」として会話ログに追加git merge commit 相当)
3. チャネル/プロバイダ差分は capability interface で吸収する
4. 既存線形セッションとの後方互換を維持しながら段階移行する
### 提案データモデルv1
```go
type SessionRef struct {
HeadNodeID string
GraphID string // optional shard namespace
Version uint64 // optimistic concurrency (CAS)
}
type ConversationNode struct {
NodeID string
SessionKey string
Kind NodeKind // Message | Merge
Parents []string // 1 parent = append, 2 parents = merge
Events []LogEvent // one-turn memory bundle
SummaryDelta string
CreatedAt time.Time
Author string
Meta map[string]string
}
type LogEvent struct {
Role string // user|assistant|tool|system
Content string
ToolCallID string
ToolCalls []providers.ToolCall
Usage *providers.UsageInfo
Meta map[string]string
}
```
### 提案インターフェースv1
```go
type SessionGraphStore interface {
Resolve(sessionKey string) (SessionRef, bool, error)
CompareAndSwapHead(sessionKey string, expectVersion uint64, next SessionRef) error
AppendNode(node ConversationNode) error
GetNode(nodeID string) (ConversationNode, bool, error)
ForkSession(parentKey, childKey string, atHead bool) error
MergeSession(baseKey, branchKey string, policy MergePolicy) (mergeNodeID string, err error)
}
type MergePolicy interface {
BuildMergeEvents(basePath []ConversationNode, branchPath []ConversationNode) ([]LogEvent, error)
}
```
### Merge 方針
- subagent 結果は `processSystemMessage` で plain text 注入するのではなく、
`NodeKind=Merge` のノードを base セッションに append する。
- `Events` は「1回分の記憶」として扱う例: merge summary, adopted decisions, artifacts
- merge conflict は `MergePolicy` で deterministic ルール優先、必要に応じて LLM 補助。
### Capability 型の整理方針
既存の interface ベース設計を継続し、分岐/合流にも適用する:
- Channel capability (thread, draft, edit, status update)
- Provider capability (streaming, structured output, tool-call fidelity)
capability は type assertion で解決し、非対応時は deterministic fallback。
### 互換移行ステップ
1. 新規 `session/graph` 実装を追加in-memory + file persistence
2. adapter で既存 `GetHistory/SetHistory/AddMessage` を DAG replay に接続
3. 新規 fork/merge API を miniapp/command 層へ段階公開
4. 既存 linear JSON は lazy migrationread old -> write new on mutation
5. 安定後に old-only path を read-compat へ縮退
### 並列作業可能タスク
#### Track A: Core Data Model / Storage
- `SessionRef`, `ConversationNode`, `LogEvent` 型追加
- `SessionGraphStore` と CAS 更新実装
- node persistence + compaction + GC (unreachable branch TTL)
#### Track B: Replay / Prompt Integration
- DAG -> linear message replay 実装
- merge node の replay 表現1-turn memory bundle
- context overflow 時の compression 戦略を DAG 前提に再設計
#### Track C: Orchestration Integration
- subagent completion を merge node 化
- task metadata (`task_id`, `source_session`, `duration`, `tool_calls`) を node meta へ格納
- reporter/broadcaster のイベントと node lifecycle の対応付け
#### Track D: Channel & Provider Capabilities
- channel capability matrix を型化thread/draft/edit/task-status
- provider capability matrix を型化streaming/structured/tool fidelity
- merge policy の capability-aware fallback 追加
#### Track E: API / UI / Commands
- fork/merge/list-branches API 追加
- Mini App: branch graph 可視化、merge preview、conflict explanation
- CLI/command: `/session fork`, `/session merge`, `/session graph`
#### Track F: Migration / Safety / Ops
- old sessions から DAG への lazy migration
- observability: merge success rate, conflict rate, replay latency
- rollback switchfeature flagとデータ整合性チェック
#### Track G: Tests / Benchmarks
- replay determinism tests
- merge policy golden tests
- concurrency tests (CAS conflict / retry)
- memory & latency benchmarklong-history / many-branch scenarios
### 依存関係(高レベル)
- A は全 Track の前提
- B/C は A 完了後に並列可能
- D は B/C と並列可能(インターフェース先行)
- E は B/C/D の API 固定後に進める
- F/G は全フェーズ横断で継続実施
---
## Session DAG Design (Revised 2026-03-04)
> 上記 Session DAG Migration Plan を SBC制約と設計レビューを踏まえて改訂したもの。
### 設計原則
1. **セッション内は線形、セッション間がDAG** — per-message DAGは過剰。ターン間の因果は順序で十分。
2. **SQLite single-file backend** — microSD書き込み最小化、WALモードでクラッシュ耐性。
3. **サブエージェント報告は user role** — system roleの権威性バイアスを回避。conductorが評価・反論できる。
4. **"merge" は特別な操作ではない** — 報告を受けて会話を続ける通常のターン。
### 初期提案からの変更点
| 初期提案 | 改訂 | 理由 |
|---|---|---|
| `ConversationNode` per-message | `Turn` per-turn (複数メッセージをバンドル) | ード爆発回避、microSD保護 |
| CAS + `Version uint64` | `sync.RWMutex` (single process) | SBCはシングルプロセス、CASは過剰 |
| `NodeKind=Merge` + system role注入 | `TurnReport` + user role会話 | LLMの権威性バイアス回避 |
| per-node JSON files | SQLite single file | 書き込み回数削減、インデックス付きクエリ |
| LLM-assisted merge | deterministic (不要に) | mergeという概念自体が消えた |
| 毎回DAG→linear replay | cached linear view + dirty flag | ARM CPU負荷軽減 |
### DAG構造
```
Conductor session: turn1 → turn2 → turn3 → report(scout-1) → turn4 → report(coder-1) → turn5
↓ fork ↑ report
Scout-1 session: turn1 → turn2 → turn3 ──────────┘
↓ fork
Coder-1 session: turn1 → turn2 → turn3 ──────────┘
```
セッション内は `seq INTEGER` で順序管理。DAGの辺はセッション間の `parent_key` / `origin_key`
### SQLite Schema
```sql
CREATE TABLE sessions (
key TEXT PRIMARY KEY,
parent_key TEXT REFERENCES sessions(key),
fork_turn_id TEXT, -- 親のどのturnで分岐したか
status TEXT NOT NULL DEFAULT 'active', -- active|completed|archived
label TEXT NOT NULL DEFAULT '', -- "scout-1", "heartbeat" etc.
summary TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE turns (
id TEXT PRIMARY KEY,
session_key TEXT NOT NULL REFERENCES sessions(key) ON DELETE CASCADE,
seq INTEGER NOT NULL,
kind INTEGER NOT NULL DEFAULT 0, -- 0=normal 1=report 2=fork_point
messages TEXT NOT NULL, -- JSON []providers.Message
origin_key TEXT, -- report: どのセッションの報告か
summary TEXT, -- compaction後。非NULLならmessagesは空
author TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
meta TEXT, -- JSON object, nullable
UNIQUE(session_key, seq)
);
CREATE INDEX idx_turns_session_seq ON turns(session_key, seq);
CREATE INDEX idx_sessions_parent ON sessions(parent_key);
```
### Go Interface
```go
package session
type TurnKind int
const (
TurnNormal TurnKind = iota // user↔assistant 通常ターン
TurnReport // サブエージェント報告 + conductor評価
TurnForkPoint // マーカー: ここで子セッションが分岐した
)
type Turn struct {
ID string
Seq int
Kind TurnKind
Messages []providers.Message
OriginKey string // TurnReport時: 報告元セッションkey
Summary string // compaction済みなら非空、Messagesは空
Author string
CreatedAt time.Time
Meta map[string]string
}
type SessionInfo struct {
Key string
ParentKey string // "" = root
ForkTurnID string
Status string // active, completed, archived
Label string
Summary string
TurnCount int
CreatedAt time.Time
UpdatedAt time.Time
}
type CreateOpts struct {
ParentKey string
ForkTurnID string
Label string
}
type ListFilter struct {
ParentKey string
Status string
}
// SessionStore — SQLite実装が唯一の実装
type SessionStore interface {
// セッション
Create(key string, opts *CreateOpts) error
Get(key string) (*SessionInfo, error)
List(filter *ListFilter) ([]*SessionInfo, error)
SetStatus(key, status string) error
SetSummary(key, summary string) error
Delete(key string) error
Children(key string) ([]*SessionInfo, error)
// ターン (セッション内は線形)
Append(sessionKey string, turn *Turn) error
Turns(sessionKey string, sinceSeq int) ([]*Turn, error)
LastTurn(sessionKey string) (*Turn, error)
TurnCount(sessionKey string) (int, error)
// compaction: seq以前のturnsをsummaryで置換
Compact(sessionKey string, upToSeq int, summary string) error
// fork: 親セッションの現在headから子セッションを作る
Fork(parentKey, childKey string, opts *CreateOpts) error
// 管理
Prune(olderThan time.Duration) (int, error)
Close() error
}
```
### 高レベルラッパー
```go
// SessionGraph — AgentLoopが直接使う層
// SessionStoreをラップし、ターンバッファとlinear viewキャッシュを持つ
type SessionGraph struct {
store SessionStore
buffers sync.Map // sessionKey → *turnBuffer (書き込み中ターン)
views sync.Map // sessionKey → *cachedView (LLM用メッセージ列)
}
// LLM用: 全turnsをフラットな[]Messageに展開
// compaction済みturnsはsummaryをsystem messageとして先頭に置く
func (g *SessionGraph) Messages(sessionKey string) ([]providers.Message, error)
// ターン開始 (user message受信時)
func (g *SessionGraph) BeginTurn(sessionKey string, kind TurnKind) *TurnWriter
// TurnWriter — 1ターン内でメッセージを逐次追加
type TurnWriter struct { ... }
func (tw *TurnWriter) Add(msg providers.Message)
func (tw *TurnWriter) SetOrigin(sessionKey string) // report元を設定
func (tw *TurnWriter) Commit() error // store.Appendして確定
func (tw *TurnWriter) Discard() // 破棄 (エラー時)
```
### サブエージェント報告フロー
```
1. conductor が spawn → store.Fork(conductorSession, subagentSession)
2. subagent 実行中 → store.Append(subagentSession, Turn{Kind: TurnNormal, ...})
3. subagent 完了 → store.SetStatus(subagentSession, "completed")
4. conductor 側に report ターン:
tw := graph.BeginTurn(conductorSession, TurnReport)
tw.SetOrigin(subagentSession)
tw.Add(Message{Role: "user", Content: "[scout-1] 調査結果..."})
// conductorのLLMループが応答を追加してから tw.Commit()
5. conductor が応答:
tw.Add(Message{Role: "assistant", Content: "なるほど、JWTで十分..."})
tw.Commit()
```
conductorは各報告を個別に評価・反論できる。system roleではないので鵜呑みにしにくい。
### 既存コードとの互換アダプター
```go
// 移行期間中、既存の SessionManager interface を満たす
type LegacyAdapter struct {
graph *SessionGraph
}
func (a *LegacyAdapter) GetHistory(key string) ([]providers.Message, error) {
return a.graph.Messages(key)
}
func (a *LegacyAdapter) AddMessage(key, role, content string) error { ... }
func (a *LegacyAdapter) SetHistory(key string, msgs []providers.Message) error { ... }
```
### 移行フェーズ
1. **Phase 0**: SQLite SessionStore 実装 + LegacyAdapter。既存動作を維持したまま裏側を差し替え
2. **Phase 1**: サブエージェントセッション永続化 + Fork/Report ターン導入
3. **Phase 2**: AgentLoop を SessionGraph 直接呼び出しに移行。LegacyAdapter 廃止
4. **Phase 3**: Mini App graph 可視化 + `/session` コマンド群

75
todo/TASKS-1.md Normal file
View file

@ -0,0 +1,75 @@
# TASKS-1: Memory & Performance Optimization
内部リファクタリング。外部APIの変更なし。他トラックへの依存なし。
## タスク一覧
### D-1. MemoryStore のパース済みキャッシュ導入
`GetMemoryContext()` 1回で `ReadLongTerm()` が5回以上呼ばれる連鎖を解消。
**方針 (いずれか):**
- (a) content パススルー — 高レベルメソッドだけが1回 `ReadLongTerm()` を呼び、content を private ヘルパーに渡す
- (b) `*ParsedPlan` 常駐 — MemoryStore にパース済み構造体を持たせ、`edit_file` 後に `InvalidateCache()` を呼ぶ
**対象ファイル:** `pkg/agent/memory.go`
---
### D-2. FunctionCall.Arguments の型整理
`FunctionCall.Arguments` が JSON 文字列のまま。ストリーミングループ内で重複 Unmarshal が発生。
`ToolCall.Arguments map[string]any` のパース済みフィールドと中途半端に共存している。
**やること:** パース済みフィールドに一本化し、JSON 文字列フィールドを内部に隠蔽。
**対象ファイル:** `pkg/providers/protocoltypes/types.go`, ストリーミング処理周辺
---
### D-3. ToolFunctionDefinition.Parameters → json.RawMessage
`Parameters``map[string]any` のため、プロバイダーへ送るたびに Marshal が必要。
`json.RawMessage` にすれば一度の marshal で済む。
**対象ファイル:** `pkg/providers/protocoltypes/types.go`, 各プロバイダー実装
---
### D-4. 検索プロバイダーの共通フォーマット抽象
`[]string + strings.Join` パターンが3箇所に複製。`Search()` 戻り値を構造体にすれば1箇所で済む。
**対象ファイル:** `pkg/search/` 配下
---
### D-6. MemoryStore メソッド境界の再設計
呼び出し側は複数の値が必要でも複数回 `ReadLongTerm()` を呼ぶしかない。
D-1 の解決策と合わせて、メソッド境界を「必要な情報の単位」に再編成。
**対象ファイル:** `pkg/agent/memory.go`
---
### stats.Tracker 定期フラッシュ
`state/stats.json` が LLM 呼び出し毎に書き込まれる。定期フラッシュ (5分) に変更して microSD 寿命を保護。
**やること:**
- `stats.Tracker` にインメモリバッファ + バックグラウンドフラッシャー goroutine を追加
- `Close()` メソッド追加 (タイマー停止 + 最終 save)
- SIGTERM/SIGINT でシャットダウンフック
**対象ファイル:** `pkg/state/state.go`
---
## 完了基準
- `go test ./...` 全パス
- MEMORY.md 読み取りが1ターンあたり1回以下に削減 (D-1)
- FunctionCall の Unmarshal が1回に集約 (D-2)
- ToolFunctionDefinition.Parameters の Marshal がプロバイダー初期化時のみ (D-3)
- stats.json の書き込み頻度が 98% 削減 (stats.Tracker)

227
todo/TASKS-2.md Normal file
View file

@ -0,0 +1,227 @@
# TASKS-2: Subagent Orchestration (Container Model)
SubagentManager を Container ベースの Orchestrator に進化させる。
セッション管理の内部実装 (TASKS-3) には依存しない — 現行の SessionManager 上で動作させ、後から SessionStore に差し替える。
## 設計コンテキスト
### アーキテクチャ
```
Conductor goroutine
│ ContainerRequest (task, preset, environment)
Container goroutine: provision → run → finalize
│ ContainerMessage (question / result / status)
Conductor goroutine
│ answer (question への回答)
▼ (わからなければ human に escalate)
Container goroutine (再開)
```
### escalation chain
```
subagent (clarifying) → question → conductor
→ conductor が答えられる: inCh に回答
→ conductor もわからない: message tool で human に投げ、回答を転送
```
### Presets (5種)
| preset | 性格 | write | exec | search | spawn |
|---|---|---|---|---|---|
| `scout` | Exploratory | x | x | o | x |
| `analyst` | Exploratory | x | go test/vet, git log/diff, grep | o | x |
| `coder` | Deliberate | o sandbox | test/lint/fmt 系 | o | x |
| `worker` | Deliberate | o sandbox | build/package manager 系 | o | x |
| `coordinator` | Deliberate | o sandbox | go/pnpm/bun/curl 系 | o | scout-worker のみ |
**性格の分類:**
- **Exploratory** (scout/analyst): open-ended、見てきて報告。clarifying フェーズなし。
- **Deliberate** (coder/worker/coordinator): 成果物を作る。clarifying フェーズあり。
**npm は全 preset で禁止** (git worktree に node_modules が生じるため)。pnpm/bun は可。
**websearch/webfetch は全 preset で許可** (read-only のため)。
### exec allowlist regex
```go
var presetExecPatterns = map[string]string{
"scout": ``,
"analyst": `^(go\s+(test|vet)|git\s+(log|diff|status)|curl|wget|grep|find)\b`,
"coder": `^(` +
`go\s+(test|vet|fmt)|gofmt|goimports|golangci-lint|` +
`prettier|eslint|` +
`black|ruff|` +
`cargo\s+(test|fmt|clippy)|` +
`pnpm\s+(test|run\s+(test|lint|format))|` +
`bun\s+(test|run\s+(test|lint|format))|` +
`uv\s+run\s+` +
`)\b`,
"worker": `^(` +
`go\s+|` +
`pnpm\s+(install|add|run|test|build)|` +
`bun\s+(install|add|run|test|build)|` +
`uv\s+(run|sync|add|pip\s+install)|` +
`pip\s+install|` +
`cargo\s+` +
`)\b`,
"coordinator": `^(go\s+|pnpm\s+|bun\s+|curl|wget)\b`,
}
```
---
## タスク一覧
### 1. SubagentContainer 実装
goroutine + channel でサブエージェントのライフサイクルを表現。
```go
type ContainerMessage struct {
Type string // "question" | "result" | "status"
Content string
}
type SubagentContainer struct {
inCh chan string // conductor → subagent (回答)
outCh chan ContainerMessage // subagent → conductor (質問・結果・進捗)
cancel context.CancelFunc
}
```
- spawn (async): outCh を返して即リターン
- subagent (sync): その場で result を待つ
- goroutine 終了時に `defer orchestrator.active.Delete(id)` + `defer close(outCh)` で自動 GC
**対象ファイル:** `pkg/tools/container.go` (新規)
---
### 2. Orchestrator 実装
SubagentManager を置き換える上位構造。
- Container の生成・管理
- escalation chain の実装 (subagent → conductor → human)
- spawn/subagent の使い分けロジック
**対象ファイル:** `pkg/tools/orchestrator.go` (新規)
---
### 3. SubagentEnvironment (Context Injection)
conductor が subagent に渡すコンテキストの構造化。
```go
type SubagentEnvironment struct {
Workspace string
WorktreeDir string
PlanTask string // MEMORY.md > Task: から自動抽出
PlanContext string // MEMORY.md ## Context から自動抽出
Commands string // MEMORY.md ## Commands から自動抽出
CurrentPhase string
Background string // conductor が明示的に追加
Constraints string
ContextFiles []string
}
```
`inject_plan_context: true` で MEMORY.md からの自動注入を有効化。
**対象ファイル:** `pkg/tools/container.go` または `pkg/tools/environment.go` (新規)
---
### 4. SandboxConfig enforcement
`ToolRegistry.Execute()` の入口で一括 enforcement。subagent は透過的に sandboxed になる。
```go
type SandboxConfig struct {
Preset string
WriteRoot string
AllowedTools map[string]bool
ExecPolicy *ExecPolicy
SpawnablePresets []string
}
```
workDir = worktreeDir として設定し、AI が隔離に気づかず振る舞う透過的隔離を実現。
**対象ファイル:** `pkg/tools/sandbox.go` (既存拡張), `pkg/tools/registry.go`
---
### 5. Preset enforcement
preset 定義と exec allowlist regex を SandboxConfig 経由で enforce。
`pkg/tools/sandbox.go` に既存の定義があるが、ToolRegistry.Execute() での一括 enforcement がまだ。
---
### 6. Subagent Plan Mode (in-memory)
Deliberate preset 用のミニ plan mode。MEMORY.md には触れない。
```go
type SubagentPlanState int
const (
PlanStateNone SubagentPlanState = iota // Exploratory preset
PlanStateClarifying // 目的・制約を確認中
PlanStateReview // conductor の承認待ち
PlanStateExecuting // 実行中
)
type SubagentPlan struct {
State SubagentPlanState
Goal string
Approach []string
QA []QAItem
mu sync.Mutex
}
```
SubagentContainer がフィールドとして保持。goroutine 終了とともに消える。
**System prompt 使い分け:**
- Deliberate: clarifying → review → executing の3段階
- Exploratory: 即座に探索開始、判断は自律的に
---
### 7. MEMORY.md Orchestration Section
conductor の guidance に Orchestration セクションの使い方を追記。
```markdown
## Orchestration
### Delegated
- coder-1 (coder): rate limiter 実装 → Phase 2 Step 1
### Findings
- pkg/auth は middleware パターン (scout-1)
### Decisions
- auth: JWT を選択 (外部依存なし、scout-2 推奨)
```
conductor は spawn 後に Delegated に記録、結果受信後に Findings に追記、方向選択時に Decisions に記録。
**対象ファイル:** `pkg/agent/context.go` (guidance 追記)
---
## 完了基準
- `go test ./...` 全パス
- scout/analyst/coder preset で spawn → 結果受信 → conductor 応答の E2E フロー動作
- Deliberate preset の clarifying → review → executing フロー動作
- SandboxConfig による exec 制限が全 preset で正しく enforcement
- escalation chain (subagent → conductor → human) が動作

230
todo/TASKS-3.md Normal file
View file

@ -0,0 +1,230 @@
# TASKS-3: Session DAG (SQLite Store)
セッション管理を JSON ファイルベースの線形スライスから SQLite ベースの Turn DAG に移行する。
他トラックへの依存なし — LegacyAdapter で既存コードとの後方互換を維持しながら段階移行。
## 設計原則
1. **セッション内は線形、セッション間が DAG** — per-message DAG は過剰。ターン間の因果は順序で十分。
2. **SQLite single-file backend** — microSD 書き込み最小化、WAL モードでクラッシュ耐性。
3. **サブエージェント報告は user role** — system role の権威性バイアスを回避。conductor が評価・反論できる。
4. **"merge" は特別な操作ではない** — 報告を受けて会話を続ける通常のターン。
## DAG 構造
```
Conductor session: turn1 → turn2 → turn3 → report(scout-1) → turn4 → report(coder-1) → turn5
↓ fork ↑ report
Scout-1 session: turn1 → turn2 → turn3 ──────────┘
↓ fork
Coder-1 session: turn1 → turn2 → turn3 ──────────┘
```
## SQLite Schema
```sql
CREATE TABLE sessions (
key TEXT PRIMARY KEY,
parent_key TEXT REFERENCES sessions(key),
fork_turn_id TEXT,
status TEXT NOT NULL DEFAULT 'active',
label TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE turns (
id TEXT PRIMARY KEY,
session_key TEXT NOT NULL REFERENCES sessions(key) ON DELETE CASCADE,
seq INTEGER NOT NULL,
kind INTEGER NOT NULL DEFAULT 0,
messages TEXT NOT NULL,
origin_key TEXT,
summary TEXT,
author TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
meta TEXT,
UNIQUE(session_key, seq)
);
CREATE INDEX idx_turns_session_seq ON turns(session_key, seq);
CREATE INDEX idx_sessions_parent ON sessions(parent_key);
```
## Go Interface
```go
type TurnKind int
const (
TurnNormal TurnKind = iota
TurnReport
TurnForkPoint
)
type Turn struct {
ID string
Seq int
Kind TurnKind
Messages []providers.Message
OriginKey string
Summary string
Author string
CreatedAt time.Time
Meta map[string]string
}
type SessionInfo struct {
Key string
ParentKey string
ForkTurnID string
Status string
Label string
Summary string
TurnCount int
CreatedAt time.Time
UpdatedAt time.Time
}
type SessionStore interface {
Create(key string, opts *CreateOpts) error
Get(key string) (*SessionInfo, error)
List(filter *ListFilter) ([]*SessionInfo, error)
SetStatus(key, status string) error
SetSummary(key, summary string) error
Delete(key string) error
Children(key string) ([]*SessionInfo, error)
Append(sessionKey string, turn *Turn) error
Turns(sessionKey string, sinceSeq int) ([]*Turn, error)
LastTurn(sessionKey string) (*Turn, error)
TurnCount(sessionKey string) (int, error)
Compact(sessionKey string, upToSeq int, summary string) error
Fork(parentKey, childKey string, opts *CreateOpts) error
Prune(olderThan time.Duration) (int, error)
Close() error
}
```
## 高レベルラッパー
```go
type SessionGraph struct {
store SessionStore
buffers sync.Map // sessionKey → *turnBuffer
views sync.Map // sessionKey → *cachedView
}
func (g *SessionGraph) Messages(sessionKey string) ([]providers.Message, error)
func (g *SessionGraph) BeginTurn(sessionKey string, kind TurnKind) *TurnWriter
type TurnWriter struct { ... }
func (tw *TurnWriter) Add(msg providers.Message)
func (tw *TurnWriter) SetOrigin(sessionKey string)
func (tw *TurnWriter) Commit() error
func (tw *TurnWriter) Discard()
```
## サブエージェント報告フロー
```
1. conductor が spawn → store.Fork(conductorSession, subagentSession)
2. subagent 実行中 → store.Append(subagentSession, Turn{Kind: TurnNormal, ...})
3. subagent 完了 → store.SetStatus(subagentSession, "completed")
4. conductor 側に report ターン:
tw := graph.BeginTurn(conductorSession, TurnReport)
tw.SetOrigin(subagentSession)
tw.Add(Message{Role: "user", Content: "[scout-1] 調査結果..."})
5. conductor が応答:
tw.Add(Message{Role: "assistant", Content: "なるほど、JWTで十分..."})
tw.Commit()
```
---
## タスク一覧
### Phase 0: SQLite SessionStore + LegacyAdapter
既存動作を維持したまま裏側を差し替える。
1. **SQLite SessionStore 実装**
- `pkg/session/sqlite.go` (新規): SessionStore interface の SQLite 実装
- WAL モード、`modernc.org/sqlite` (CGO なし、ARM クロスコンパイル容易)
- schema migration (CREATE TABLE IF NOT EXISTS)
- `go:build` タグなしで常に利用可能
2. **LegacyAdapter 実装**
- `pkg/session/legacy_adapter.go` (新規)
- 既存の `GetHistory` / `SetHistory` / `AddMessage` / `MarkDirty` を SessionGraph 経由で実装
- 既存テストが全パスすること
3. **JSON → SQLite lazy migration**
- 起動時に `sessions/*.json` を検出 → SQLite に import → JSON ファイルをリネーム (.migrated)
- エラー時は JSON にフォールバック
4. **AgentLoop 配線**
- `AgentInstance``SessionStore` を保持、`LegacyAdapter` 経由で既存コードに注入
- 既存の `SessionManager` は Phase 2 で廃止
---
### Phase 1: Fork/Report ターン導入
5. **Fork 操作**
- `SessionStore.Fork()` 実装
- 親セッションに `TurnForkPoint` を追記、子セッションを `parent_key` 付きで作成
6. **Report ターン**
- `TurnReport` の Append/Turns/Messages 対応
- `origin_key` でどのセッションの報告かを追跡
- user role でメッセージ格納 (system role 禁止)
7. **サブエージェントセッション永続化**
- サブエージェント実行中のターンを SQLite に記録
- 完了後にセッション status を "completed" に変更
---
### Phase 2: AgentLoop 直接移行
8. **SessionGraph 直接呼び出し**
- `runAgentLoop()``SessionGraph.BeginTurn()` / `TurnWriter` 経由に変更
- `processSystemMessage()` を Report ターン生成に変更
- LegacyAdapter 廃止
9. **Compaction**
- 古いターンの messages を空にして summary で置換
- context window 管理と連動
10. **セッションライフサイクル**
- `Delete(key)` + TTL エビクション (Prune)
- `sessionLocks sync.Map` の GC
- 起動時の遅延ロード (SQLite なので自然に実現)
---
### Phase 3: UI & Commands
11. **Mini App セッショングラフ可視化**
- WebSocket で session DAG 構造を配信
- fork/report 関係をグラフとして描画
12. **CLI コマンド**
- `/session list` — アクティブセッション一覧
- `/session fork` — 現在のセッションを fork
- `/session graph` — DAG 構造をテキスト表示
---
## 完了基準
- `go test ./...` 全パス
- 既存の JSON セッションが SQLite に自動マイグレーション
- Phase 0 完了時点で既存動作に変化なし (LegacyAdapter 透過)
- サブエージェント報告が user role ターンとして記録
- conductor が報告を個別に評価・応答するフロー動作
- microSD 書き込み頻度が既存比で削減

74
todo/TASKS-4.md Normal file
View file

@ -0,0 +1,74 @@
# TASKS-4: Mini App & Static Serving
Mini App のフロントエンド改善。バックエンドの変更に依存しない。
## 現状
```
pkg/miniapp/static/
index.html ← すべての JS/CSS をインライン
map.js ← オーケストレーションルーム描画
```
Go 側: `//go:embed static/index.html static/map.js` + 個別ルートで配信。
---
## タスク一覧
### 1. Static file serving の汎用化
個別ルート (`serveIndex`, `serveMapJS`) を `http.FileServer(http.FS(staticFS))` に統合。
**やること:**
- `//go:embed static` でディレクトリごと embed
- `/miniapp/` 以下を `http.FileServer` で一括配信
- 新規ファイル追加時にルート登録が不要になる
- `serveIndex``ORCH_ENABLED` 注入は `text/template` に移行
**対象ファイル:** `pkg/miniapp/handler.go`
---
### 2. バンドラ導入 (Vite / esbuild)
inline JS/CSS を外部ファイルに分離し、ビルドステップで bundle する。
**やること:**
- `pkg/miniapp/frontend/` に source を配置
- esbuild (軽量、Go 製) でビルド → `pkg/miniapp/static/dist/` に出力
- Go 側は `//go:embed static/dist` で embed
- `ORCH_ENABLED` はビルド時の環境変数注入 (`define`) で対応
- `Makefile` / `go:generate` でビルドステップを統合
**検討事項:**
- esbuild は Go 製でクロスコンパイル環境に影響しない
- Vite は機能豊富だが Node.js 依存が増える
---
### 3. Log viewer のフロントエンドテスト
`renderLogs()` (index.html 内の inline JS) のテストが皆無。
Fields 表示バグが検出されずに ship された前科あり。
**やること:**
- index.html から JS を外部ファイルに抽出 (タスク 2 と連動)
- `renderLogs()`, `renderFields()` 等の unit test を追加
- DOM 操作のテストは jsdom (vitest) or happy-dom
- CI で `pnpm test` を実行
**最低限のテストケース:**
- メッセージの正常レンダリング
- Fields の表示 (空/1件/複数件)
- サニタイズ (XSS 防止)
- ログのフィルタリング・ページネーション
---
## 完了基準
- 新規 JS/CSS ファイルの追加がルート登録なしで配信される
- `ORCH_ENABLED` の注入がテンプレートまたはビルド時変数で動作
- `renderLogs()` のユニットテストが CI で実行される
- `go build ./...` が変わらず動作 (embed パスの整合性)

62
todo/TASKS-5.md Normal file
View file

@ -0,0 +1,62 @@
# TASKS-5: Heartbeat Worktree Management
ハートビートで作られる git worktree の管理 CLI/UI。他トラックへの依存なし。
## 現状の問題
- ハートビートセッションが `.worktrees/heartbeat-YYYYMMDD/` を作るが、人手で管理する手段がない
- `PruneOrphaned()` は起動時にディレクトリを削除するが、**未コミット変更は通知なく消失**する
- active worktree の一覧・点検・手動 merge/dispose ができない
---
## タスク一覧
### 1. `/plan worktrees` コマンド
worktree の一覧表示と操作を提供する CLI コマンド。
**サブコマンド:**
- `/plan worktrees list` — active worktree の一覧 (branch, last commit, status, uncommitted changes の有無)
- `/plan worktrees inspect <name>` — 特定 worktree の詳細 (diff, log)
- `/plan worktrees merge <name>` — main branch に merge を試行
- `/plan worktrees dispose <name>` — worktree を削除 (未コミット変更がある場合は確認)
**対象ファイル:** `pkg/agent/loop.go` (コマンドハンドラ追加), `pkg/git/worktree.go`
---
### 2. PruneOrphaned の安全化
起動時の orphan prune で未コミット変更を保護する。
**やること:**
- prune 前に `git status` で未コミット変更を検出
- 未コミット変更がある場合: auto-commit (メッセージ: "auto-save before prune") してから dispose
- auto-commit 結果をログに出力
- commit 不可の場合 (conflict 等) はスキップしてログ警告
**対象ファイル:** `pkg/git/worktree.go`
---
### 3. Mini App worktree UI
Mini App から worktree を閲覧・操作できる画面。
**やること:**
- `/api/worktrees` エンドポイント (GET: list, POST: merge/dispose)
- Mini App に worktree 一覧パネルを追加
- 各 worktree: branch 名、最終コミット日時、uncommitted changes badge
- merge/dispose ボタン (確認ダイアログ付き)
**対象ファイル:** `pkg/miniapp/handler.go`, `pkg/miniapp/static/index.html`
---
## 完了基準
- `go test ./...` 全パス
- `/plan worktrees list` で active worktree が表示される
- orphan prune 時に未コミット変更が auto-commit で保護される
- Mini App から worktree の一覧と dispose が操作できる