From 81a8abf1f20e5cba5e48dcf70dc55dd410703195 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:20:50 +0900 Subject: [PATCH] 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 --- CLAUDE.md | 859 +----------------------------------------------- todo/TASKS-1.md | 75 +++++ todo/TASKS-2.md | 227 +++++++++++++ todo/TASKS-3.md | 230 +++++++++++++ todo/TASKS-4.md | 74 +++++ todo/TASKS-5.md | 62 ++++ 6 files changed, 683 insertions(+), 844 deletions(-) create mode 100644 todo/TASKS-1.md create mode 100644 todo/TASKS-2.md create mode 100644 todo/TASKS-3.md create mode 100644 todo/TASKS-4.md create mode 100644 todo/TASKS-5.md diff --git a/CLAUDE.md b/CLAUDE.md index 1786e386b..26e08fa1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. - **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 - -- coder-1 (coder): rate limiter 実装 → Phase 2 Step 1 -- scout-1 (scout): pkg/auth の構造調査 - -### Findings - -- pkg/auth は middleware パターン、入口は middleware.go (scout-1) -- セッションストアは存在しない、JWT が有効 (scout-2) - -### Decisions - -- 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)` 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 -- 起動時の `loadSessions()` を遅延ロード化 - -**中期:** -- チェックポイント / ロールバック (`Session.Messages` を append-only immutable に) -- 名前付きセッション (`/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 migration(read 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 switch(feature flag)とデータ整合性チェック - -#### Track G: Tests / Benchmarks -- replay determinism tests -- merge policy golden tests -- concurrency tests (CAS conflict / retry) -- memory & latency benchmark(long-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` コマンド群 +| ファイル | 概要 | +|---|---| +| [`todo/TASKS-1.md`](todo/TASKS-1.md) | **Memory & Performance Optimization** — MemoryStore キャッシュ、FunctionCall/ToolDefinition 型整理、stats フラッシュ最適化 | +| [`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** — 静的配信の汎用化、バンドラ導入、フロントエンドテスト追加 | +| [`todo/TASKS-5.md`](todo/TASKS-5.md) | **Heartbeat Worktree Management** — worktree 一覧・点検・手動 merge/dispose の CLI/Mini App UI | diff --git a/todo/TASKS-1.md b/todo/TASKS-1.md new file mode 100644 index 000000000..f744eebef --- /dev/null +++ b/todo/TASKS-1.md @@ -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) diff --git a/todo/TASKS-2.md b/todo/TASKS-2.md new file mode 100644 index 000000000..78fc45fd3 --- /dev/null +++ b/todo/TASKS-2.md @@ -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) が動作 diff --git a/todo/TASKS-3.md b/todo/TASKS-3.md new file mode 100644 index 000000000..24d508753 --- /dev/null +++ b/todo/TASKS-3.md @@ -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 書き込み頻度が既存比で削減 diff --git a/todo/TASKS-4.md b/todo/TASKS-4.md new file mode 100644 index 000000000..94ec0b50b --- /dev/null +++ b/todo/TASKS-4.md @@ -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 パスの整合性) diff --git a/todo/TASKS-5.md b/todo/TASKS-5.md new file mode 100644 index 000000000..150b91d94 --- /dev/null +++ b/todo/TASKS-5.md @@ -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 ` — 特定 worktree の詳細 (diff, log) +- `/plan worktrees merge ` — main branch に merge を試行 +- `/plan worktrees dispose ` — 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 が操作できる