# Spec: サーヴァントのグループ — 一覧の区分け・絆の地図の表示切り替え・グループ単位の起動 - 起票: 2026-09-05 - 状態: **rev2(利用者裁定 2 点 = D4 据え置き / D6 のドラッグ採用 → 査読 2 系統 17 点 → 採用 15 / 前提を実測で訂正して採用 2。記録は Notes 6)→ 承認(2026-09-05)→ P0 完了 (`data_contract` 4 箇所 = `AgentGroup` の実体 / `AgentSpec.groupId` / `AgentSnapshot.groupId` / `group_contract` 凍結 8 本 + `settings_contract` の `fuseforks.hiddenGroups.v1`)→ P1 完了 (コア。記録は「P1 実装記録」)→ P2 完了(フロント。記録は「P2 実装記録」。 vitest 478・typecheck・build 緑)→ P3 完了(台帳。記録は「P3 台帳記録」)→ P4 完了 = Done** (2026-09-05。実機検収 12 件すべて利用者が確認。**起票から Done まで同日**。 記録は「P4 実機記録」) - 起点: 利用者 —「グループは表示/非表示。非表示は絆に出てこない。またチャットの送信宛先に でてこない。ただし非表示でも会話のメッセージには出てくる(委譲や転送の場合)。 グループは一括起動のスイッチがある。また ON/OFF ボタンもあり、グループ一括で起動もできる。 複数のグループを跨ぐことは今のところできない。1 エージェントが所属できるのは 1 グループ (たぶん跨がせない方がいいと思う。意見を求む)」(2026-09-05)。材料は CLAUDE.md 「サーヴァントのグループ化と地図の表示切り替え(未起票・材料。2026-09-02)」— 起点は Symphony の突き合わせで出た「タスクで配線を切り替える」案を**採らず**、「別系統の ノードとエッジを作ればいい → リストをグループ化して絆に見せる見せないを切り替える」へ 畳んだ裁定 ## Goal サーヴァント一覧を**グループ**(= タスク名)で区分けし、グループごとに **(a) 表示 / 非表示** **(b) 一括起動に含めるかのスイッチ** **(c) グループ全員の起動 / 停止ボタン** を持たせる。 非表示のグループは**絆の地図と発話の宛先から消える**が、**配送・委譲・転送・予定・統計には 1 ミリも触らない**(隠すのは見え方で、線ではない)。 **やらないこと**: 配線の切り替え(2026-09-02 裁定。履歴の `ask_agent_N` と `Memory.md` の 関係の規則が宙に浮き、ツール集合の変更で入力キャッシュも書き直しになる)/ グループ名を プロンプトへ入れる(Spec 14 D6 と同じ — 「調査」「リリース」を読んだ個体はその語に寄る。 名前は人が一覧と地図で見るための分類)/ グループごとの設定の雛形(それは役職 = Spec 14 の 仕事)/ 複数グループへの所属(D2)/ 広場ログ・黒板のグループ分け(境界は村と `work_dir` の まま — 2026-09-02 の材料が数えた 2 つの境界)。 ## 起票時の実測(2026-09-05。コードを読んだ) | 事実 | 場所 | 帰結 | |---|---|---| | 個体を列挙する面は **5 つ**: 一覧(`AgentList.vue` の `inListOrder(state.agents)`)/ 地図のノード(`TopologyMap.vue:67`)/ 地図の辺(`state.edges` を `orderOf` で向き付け)/ 会話ペインの絞り込み `` | 出さない | | Alt+↑↓ の巡回と発話の宛先 | 見えている個体だけを巡る。**隠した瞬間に選択中だった個体は、見えている先頭へ移す**(`useOrchestrator.ts:396` の「選択が一覧に無ければ先頭」と同じ規則を可視の集合に対して)。**可視が 0 体なら選択は `null`** — それは空の村と同じ状態で、宛先欄は「宛先を選択してください」、`nextAgentId` は `null` を返す(既存の挙動。rev2、査読 1-1 / 2-4)。**選択中のカードを畳んだ区分けへ落としたときも同じ規則で選択が移る**(隠れた相手を宛先に残さない) | **効かない**(凍結): 会話の行(名前解決は隠さない — 委譲・転送・束ねで隠れた個体の発話が 出る)/ 配送の全経路(人の発話・`ask_*`・`transfer_to_*`・`plan` の波・スケジュール)/ ツールの提示集合(`connected_agents` は不変なので `ask_agent_N` は隠れていても生える)/ 統計・黒板・予定の宛先・ステータスバー・`fuseforks.log` / **一括起動**(非表示のグループも 全体の ▶ で起きる。休ませるのは D4 のスイッチの仕事 — 「非表示 = 休ませる」ではない。 見出しのスイッチの説明にそう書く。rev2、査読 1-5)。**隠すのは見え方であって、 提示集合と配送経路には触らない** — Spec 20 で同一性を確認した「提示集合 = 実行フィルタ」の 規律をグループが破らない。 **無所属(`group_id` が `None` か、引けない id)はどのフィルタでも隠れない。** 隠す単位が グループなので、グループに居ない個体を隠す口が無いのは構造の帰結。 ### D4: 一括起動は「含めるか」の門が 2 段になる。判定は既存の 1 本 - **`AgentGroup.batch_start: bool`(既定 true)** = 全体の ▶(一覧ヘッダ)がこのグループの 個体を対象にするか。**個体の `batchStart` は据え置き**で、全体 ▶ の対象は `agent.batchStart && (無所属 || group.batchStart)`。2 段にするのは、系統を丸ごと休ませる ときに個体のトグルを N 回押さずに済ませるため(グループが無ければ今までと 1 ミリも 変わらない) - **グループの ▶/■**(見出しに置く)= `batchAction(そのグループの個体)` — **既存の純関数に 部分集合を渡すだけ**。起こせる相手が居れば ▶、全員動いていれば ■、遷移中が居れば無効、 の規則は全体と同じ。**個体の `batchStart` は効かせる**(「含めるか」の軸を 2 つにしない — カードのトグルは全体でもグループでも同じ意味)。**グループのスイッチ(`batch_start`)は グループの ▶ には効かない** — 効かせるとスイッチ OFF のグループの ▶ が死んだボタンになる。 スイッチは**全体 ▶ の門**であってグループ自身の門ではない、と画面の説明に書く - 実行は既存の `runBatch(targets, running)`。新しい IPC は要らない(`set_agent_running` を 1 体ずつ) - **グループの全員が `batchStart` OFF なら、グループの ▶ は無効で、`title` が「対象 0 体」と 言う**(`batchLabel` の `none` の既存の形。rev2、査読 1-6 / 2-6)。D4 の「死んだボタン」の 論点とは別 — あちらはグループのスイッチという**別の場所の設定**でボタンが永久に死ぬ形、 こちらは**すぐ下に並ぶカードのトグル**が理由で、理由が見出しの直下に見えている - **無所属の見出しにも ▶/■ を置く**(無所属の個体だけを対象に)。目とスイッチは置かない — 無所属は隠れず、スイッチの保存先(`AgentGroup`)も無い(rev2、査読 2-2) **採らなかった形**: グループの ▶ が個体の `batchStart` を無視して全員を起こす。 「含めるか」の軸がボタンごとに違う意味になり、カードのトグルが何を意味するか読めなくなる。 **利用者裁定(2026-09-05): この形でよい —「トグルが効くほうが自然」。** ### D5: 削除は所属を巻き込まない。改名は見出しが追従する - **削除** = `groups` から消すだけ。個体の `group_id` は**書き換えない**(役職の削除と同じ。 `remove_role` は `role_id` に触らない)。引けない id は無所属として描かれるので、削除の 次の瞬間から個体は無所属の並びに戻る。**配った村を旧版で開いた場合と同じ見え方**になる (D8)ので、規則が 1 つで済む。**データ上は「引けない id が残る」と「`None`」の 2 つが 並ぶ**が、id を再発行しない(D1)ので取り違えは起きない。**引けない id は次に所属を 書く操作で `None` へ正規化する** — 設定ダイアログの select は引けない id を「無所属」と して表示し保存で `None` を書く / drop の確定(D6)は無所属の箱へ落ちたとき・無所属の箱の 中で動かしたときに `None` を書く(rev2、査読 2-7 — 経路で残り方が割れないように) - **改名** = `name` の書き換え。見出し・地図の要約行が追従する(参照方式の効き所) - **作成時の名前は空を拒む**(`normalize_user_name` と同じ述語で封筒の予約文字を弾く必要は **無い** — 名前はプロンプトに入らない)。重複名は拒まない(id が別なら別のグループ。 タスク名は重なりうる) ### D6: 入口は 3 つ。作る側・所属を変える側・見る側 | 何を | どこで | 前例 | |---|---|---| | グループの作成 | **一覧の最下段の点線エリア**(押すと名前の入力に変わり、Enter で作られた見出しが上に生える。**P4 の実機で利用者が裁定** — 初版はヘッダのボタンだったが「無い方がいい。点線エリアのほうがスッキリする」) | 役職の作成は `RoleDialog` の中だが、グループは**一覧そのものの区分け**なので一覧の中に入口を置く | | グループの改名・削除 | 見出しの鉛筆 → `GroupDialog.vue`(一覧 ⇄ 下書きの二層。dirty 確認なし)。**スイッチはここに置かない**(見出しだけ — 二重管理にしない。rev2、査読 1-9) | `RoleDialog.vue`(Spec 14 P3)。**タイトルバーには置かない** — 役職は「村のカテゴリ」(条例の右)だったが、グループは一覧の区分けそのもの | | 個体の所属 | `AgentSettingsDialog` の select(無所属 + グループ一覧)と、区分けをまたぐドラッグ(下) | 役職の select(Spec 14 P4 の「既存の個体に付ける入口」の教訓 — **作成フォームと編集フォームの両方に置く**) | | 表示 / 非表示・グループの ▶/■・スイッチ | 一覧の各見出し。**グループの見出し** = 目 + ▶/■ + スイッチ + 体数 / **無所属の見出し** = ▶/■ + 体数だけ(rev2、査読 2-2) | 一覧ヘッダの ▶(`batchStart.ts`) | **一覧の描画は「無所属」→ グループ順(配列順)の区分け。** 各区分けの中は `order` 順。 **グループが 1 つも無い村では見出しを出さない**(今の一覧と 1 ミリも変わらない)。 **`order` の意味は「一覧に並ぶ位置」のまま**(`kizunaEdges.ts` の doc が「左ペインの 並びそのもの」と書いている定義を動かさない)。区分けが入ると一覧の並びは 「無所属 → グループ順、各区分けの中は `order`」になるので、**最初のドラッグで `order` が その連結へ書き直される**(一度きりの正規化)。地図への効きは**双方向の辺の描画上の始点 だけ**で、一方向の矢は `drawDirection` が触らない(**査読 1-2 / 2-1b の「辺の向きが 一斉に反転する」は当たらない** — 両端に矢を持つ 1 本の始点が入れ替わるだけで、矢の意味は 変わらない。rev2、前提を実装で訂正して採用: 正規化が起きること自体は明記する)。 **区分けをまたぐドラッグで所属が変わる**(利用者裁定 2026-09-05 —「並び順はグループ順・ グループ内並び順になるので、そのグループのエリアにドロップされたらグループも変わるほうが 自然。動きをみたい」)。形は**区分けごとに 1 つの `VueDraggable`**で、全部に同じ `group` 名を与えて箱をまたげるようにする(Sortable の共有グループ)。**1 本の箱に見出しを 混ぜる形は採らない** — Sortable の `oldIndex` / `newIndex` は `draggable` 選択子に一致する 子だけを数えるので、見出しを配列に混ぜると `update:model-value` の splice 位置がずれる (型検査にも lint にも掛からない種類の結合)。 - **確定は end で 1 回**(Spec 21 の保留コミットの形は据え置き)。`update:model-value` は 箱ごとに保留し(出た箱と入った箱の 2 回飛ぶ)、end の `evt.to` が指す箱の区分けを **動かしたカードの新しい `group_id`** に、全箱の並びを連結したものを新しい `order` にする。 **連結の材料は `state` から組んだ全区分け**(畳んだ区分けの個体も含む — DOM に無くても `state.agents` には居る)で、保留された箱だけをその並びで差し替える。**畳んだ区分けの 個体が `order` から漏れる形は構造的に無い**(rev2、査読 2-1a) - **コアへは新設の IPC 1 本 `commit_agent_drop { order, regroup }`** で渡す — 世界ロックの 中で並びと所属を**一緒に**書いて 1 回で永続化する。`reorder_agents` + `update_agent` の 2 本に割ると、片方だけ通ったとき「並びは新しい区分けの位置、所属は旧グループ」が `world.json` に残る(rev2、査読 1-7)。既存の `reorder_agents` は据え置き(一括切り替えの 再同期など他の呼び手が居る) - **地図のノードで終わった drop は今までどおり絆を張り、並びも所属も確定しない** (差し戻しは `evt.from` と `oldIndex` が箱単位で来るのでそのまま効く)。**共有 `group` の Sortable が地図への drop を横取りしないかは P2 で実測する**(査読 1-8 — 起票時の読みでは 地図は Sortable の箱ではないので横取りされないが、`force-fallback` の分身の座標で `elementFromPoint` を引く経路は箱が増えても同じ) - **畳んだ区分け(非表示のグループ)にも落とせる。** 見出しの直下に薄い落とし場を残し、 落ちたカードは畳まれた中の**末尾**へ入る(見出しの体数が 1 増えるので消えていないと 読める)。**畳んだ中のカードは掴めない**(描画されていない)ので、**出す手段は一度 表示に戻すことだけ** — 非対称だが、隠したまま動かす操作は「見えないものを動かす」 形なので塞いでよい(rev2、査読 1-8)。「見えないところへしまう」は正当な操作なので 塞がない。**動きは P4 で見る** — ここが利用者の「動きをみたい」の本体で、違和感が出たら 落とし場を閉じる側へ倒す - `holdReorder` / `onDragEnd` は箱の数だけ増えない — 純関数 `commitDrop(sections, pendingBySection, toSectionKey, movedId)` が `{ order: AgentId[], regroup: { id, groupId: GroupId | null } | null }` を返し、 コンポーネントはそれを IPC 1 本へ流すだけ。**単体で留めるのは 3 つ**: 同じ箱の中の移動は `regroup` が `null` / 無所属の箱へ落ちたら `regroup.groupId` が `null`(引けない id の 正規化。D5)/ 畳んだ区分けの個体が `order` に残る ### D7: 触る契約と台帳 `data_contract.yaml`: `AgentGroup` の実体(`roles` の隣)/ `AgentSpec.groupId` / `AgentSnapshot.groupId` / `group_contract`(凍結: 所属 1 つ・表示は端末・効く面 5 つと 効かない面・削除は巻き込まない・プロンプトに入れない・実行経路で読まない)/ `settings_contract` の端末側の鍵に `fuseforks.hiddenGroups.v1`。`ipc_contract.rs` の literal(`AgentSnapshot` に 欄)。DETAIL 日英(画面構成の左ペインの行 + 層の説明)/ README 3 言語(「何ができるか」に 1 行)。**grep 網の外**: Qiita 記事は一覧を触れているので画面が変われば古くなる(嘘には ならない — 見出しが増えるだけ)。LP は一覧の見た目に触れていない。 ### D8: 利用者が負う条件 - **旧版のバイナリで村を開くと `groups` と `group_id` が消える**(#112。`#[serde(default)]` は 「欠けても読める」だけで「消さない」を保証しない)。**未知の欄を保持して書き戻す側は 今も未起票** — この Spec で欄を足すので、その判断をここで問う(Notes 3) - **非表示は端末ごと**。村を配っても、別の端末で開いても、全部見える状態から始まる ## Stories - S1 「調査」と「リリース」の 2 グループを作り、個体を振り分けると、一覧が 2 つの見出しで 区切られ、地図には全員が居る - S2 「リリース」を隠すと、地図から「リリース」の個体と、その個体へ向かう辺が消え、 要約行に「隠れているサーヴァント N 体・辺 M 本」が出る。一覧の見出しは残り、畳まれている - S3 隠れている個体に進行役が委譲すると、会話ペインにその個体の発話が普通に出る (隠れているのは見え方で、線ではない) - S4 「リリース」の見出しの ▶ を押すと、そのグループの個体だけが起きる。全体の ▶ は 「リリース」のスイッチが OFF なら「リリース」を飛ばす - S5 共有ワーカーを無所属にしておくと、どちらのグループを隠していても地図に**ノードとして** 残る。隠したグループへ向かう辺は消えて要約行の数に回る(誰と繋がっているかは、そのグループを 出したときに見える) ## Phases - **P0** — `data_contract` の凍結(D7 の契約側) - **P1** — コア: `AgentGroup` / `PersistedWorld.groups` / `World` の CRUD(`upsert_group` / `remove_group` / `group` / **`commit_agent_drop`**)/ `AgentSpec.group_id` + `AgentSnapshot` の投影 / IPC 4 本(`list_groups` / `upsert_group` / `delete_group` / `commit_agent_drop`。設定ダイアログからの所属の変更は既存の `update_agent` に乗る)。 結合 4 本(所属が保存を往復して残る / グループ削除で個体の `group_id` が**変わらない** / 旧い `world.json`(`groups` 無し)が読める / **`commit_agent_drop` は並びと所属を 1 回の 永続化で書く** — 途中で落とす変異で片方だけ残らない)。**実行経路で読まないことは 走査テストで留める**(`orchestrator/` の本体に `group_id` が 0 件 — `pricing` が `budget` を 読まない網と同じ形) - **P2** — フロント: `lib/agentGroups.ts`(純関数: 区分け `sectionize(agents, groups)` / 可視の集合 `visibleAgents(agents, groups, hidden)` / 全体 ▶ の対象 `batchEligible` / 地図の辺のフィルタと隠れた数 / **drop の確定 `commitDrop`**)+ 単体 / `useHiddenGroups.ts` (配列の棚)/ `AgentList.vue` の**区分けごとの `VueDraggable`**(同じ `group` 名)と見出し (目・▶/■・スイッチ・体数)/ `GroupDialog.vue` / `AgentSettingsDialog` の select / `TopologyMap.vue` のノードと辺のフィルタ + 要約行 / `ChatPanel.vue` の `