Files
maestro/docs/maintenance-checklist.md
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

829 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# メンテナンスチェックリスト
コード変更時に連動して修正が必要な箇所のリスト。
過去に実際に発生した不整合をもとに作成。
---
## 0-A. ツールモジュールの自己申告(TOOL_MODULE)— まずここを読む
**2026-08-15(#1126)から、モジュールは自分の属性を自分で申告できる。**新しくツールを足すときは、離れた 4 ファイルを追いかける前に、まずモジュールの中に申告を 1 つ書く。
### 書き方
自分のモジュール(`src/engine/tools/<name>.ts`)で、`TOOL_DEFS` の隣に置く。
```ts
import type { ToolModuleManifest } from './module-manifest.js';
export const TOOL_MODULE: ToolModuleManifest = {
id: 'youtube', // ファイル名(拡張子なし)と揃える
category: 'youtube', // 分類。MODULE_SPECS と同じ語彙
rawSaveTools: ['GetYouTubeTranscript', 'SearchYouTube'],
};
```
### 書ける項目
| 項目 | 意味 | 以前の書き場所 |
|---|---|---|
| `id` | モジュールの識別子。診断とテストの照合に使う | (なし) |
| `category` | 分類 | `tool-categories.ts` の `MODULE_SPECS` |
| `metaTools` | ワークスペース設定に関係なく常時載せる | `tool-categories.ts` と `index.ts` の 2 か所 |
| `rawSaveTools` | 結果を `logs/raw/` に生のまま残す | `raw-save.ts` の `RAW_SAVE_TOOLS` |
| `rawLogOnlyTools` | 保存先パスだけ記録する | `raw-save.ts` の `RAW_LOG_ONLY_TOOLS` |
| `sensitiveTools` | 分類が既定 ON でも個別に既定 OFF | `tool-categories.ts` の `SENSITIVE_TOOLS` |
**書かない項目もある。**分類そのものを慎重扱いにする指定(`SENSITIVE_CATEGORIES`)は分類の属性なので申告には載せない。ワークフローの同梱は次の段の話で、まだ入れていない。
### 守ること
- **申告に書いた名前は、そのモジュールの `TOOL_DEFS` に必ずあること。**無い名前は収集時に落とされ、`phantoms` に記録される(採用されない)
- **同じ属性を中央の一覧と申告の両方に書かない。**両方にあると、申告を消しても実効値が変わらず、申告が飾りになる。移すときは中央の一覧から必ず消す
- **読む側は必ず解決関数を通すこと。**中央の一覧を直接読むと、申告した分が落ちる
| 解決関数(`src/engine/tools/module-manifest.ts`) | 返すもの |
|---|---|
| `resolveMetaToolNames()` | 常時利用の実効集合 |
| `resolveRawSaveToolNames()` | 生データ保存の実効集合 |
| `resolveRawLogOnlyToolNames()` | パスだけ記録の実効集合 |
| `resolveSensitiveToolNames()` | 既定 OFF の実効集合 |
| `resolveDeclaredCategories()` | 申告された分類(指定 → 分類) |
どれも「中央の一覧 ∪ 全モジュールの申告」を返す。**申告を持たないモジュールは、いままでどおり中央の一覧だけで決まる。**一度に全部を移す必要はない。
### 中央の一覧は残っている
`tool-categories.ts` の `META_TOOLS` / `SENSITIVE_TOOLS`、`raw-save.ts` の `RAW_SAVE_TOOLS` / `RAW_LOG_ONLY_TOOLS` は消していない。**申告を持たないモジュールの受け皿**として残す。移すときだけ、そのモジュールの分を中央から申告へ動かす。
### 確認方法
```bash
# 申告の収集・実効値・配線をまとめて見る
npx vitest run src/engine/tools/module-manifest.test.ts
```
この試験には**登録済みモジュールが 1 本残らず読み込めることを見る検査**が入っている。`index.ts` の `tryLoadModule` は読み込み失敗を握り潰すので、モジュールが壊れてもツールが黙って消えるだけで試験は緑のままになる。その穴を塞ぐための検査なので、**中に try/catch を足さないこと。**
### 既知の食い違い(未修正・意図的)
`workspace-task-search.ts` と `package-request.ts` は `index.ts` が実行時に読み込んでいるのに `MODULE_SPECS` に載っていない。載っているツールが全部 META なので実行時には届いてしまい、抜けに気づけない形になっている。**`/api/tools` のツール一覧には出ていない。**`MODULE_SPECS` に足すと設定画面の見た目が変わるため、この段では足さず、`module-manifest.ts` の `EXTRA_MODULE_SPECIFIERS` に置いて読み込み検査の対象にだけ入れてある。
---
## 0-B. 群(パック)の台帳 — 環境全体で群ごと止める
台帳: **`src/engine/tools/tool-pack-ledger.ts`**(データだけ。import を持たない)
関所: **`src/engine/tools/tool-pack-gate.ts`**(設定を読んで判定を凍らせる)
`config.yaml` の `tool_packs.disabled` に群の名前を書くと、その群のモジュールを**読み込む前に断る**。ツール定義・分類・申告の収集・`/api/tools` の一覧が同時に消える。
### 台帳に足すときの条件(両方満たすこと)
1. **外の相手をその群だけが持っている。**外部 CLI・外部サーバー・外部 API・外部ライブラリのいずれかが、その群の中からしか呼ばれていない
2. **代わりを置ける。**同じ用途を別の事業者・別の MCP サーバーで満たせる
**分類が同じという理由だけで束ねないこと。**文字起こし(`speech`)と読み上げ(`speak`)は別の群にしてある——接続先のサーバーが別だから。
条件を満たさないものを足すと「外したのに縁が切れていない」群ができて、段 F(実体と依存を捨てる)で必ず詰まる。載せなかったものとその理由は台帳のコメントにある(`mermaid` / `calendar` / `data` / `review` / `app-test`)。
### 守ること
- **設定の変更通知(`ConfigManager` の `config-changed`)に関所を繋がないこと。**走行中にツール一覧が変わると継続ジョブのプロンプトキャッシュが全損する。反映は再起動時のみ
- **モジュール指定を呼び出し側で `replace` しないこと。**`normalizeToolModuleSpecifier()` を通す。`MODULE_SPECS` は `../engine/tools/x.js`、`index.ts` は `./x.js` の 2 通りがあり、片方だけ素通りするのが一番起きやすい壊れ方
- **台帳の群のツールを `META_TOOLS` / `SENSITIVE_TOOLS` に書かないこと。**中央の一覧は群と無関係に効くので、書くと止めた群の名前が許可一覧に残る。`tool-pack-gate.test.ts` の歯止めが落ちる
- 台帳に**載っていない**群はいままでどおり全部使える。これが既定
### 関所を通す先(6 か所。1 つでも抜けると片側だけ素通りする)
| 場所 | 何が消えるか |
|---|---|
| `tools/index.ts` の `tryLoadModule` | エージェントに渡るツール定義と、実行の口 |
| `module-manifest.ts` の `listRegisteredToolModuleSpecifiers` | 自己申告の収集・分類の地図・生データ保存の対象 |
| `tool-categories.ts` の `listToolCategories` | 設定画面に並ぶ分類のトグル |
| `bridge/tools-api.ts` の `loadBuiltinEntries` | `/api/tools` のツール一覧 |
| `workspace-tool-policy.ts` の個別 sensitive ループ | ワークスペース側から入れ直す道(層の向き) |
| `worker.ts` の `ensureKeepaGraphs` 呼び出し | 成果物に差し込まれる価格グラフ(外部の画像 URL) |
最後の 1 行が要る理由は、**群の中の関数を群の外から静的に呼んでいる**から。`import` で直に持ってくる経路はモジュール読み込みの関所を通らないので、呼ぶ側が自分で `isToolPackDisabled()` を見るしかない。ツール定義が消えても差し込みが残ると、成果物を開いた利用者の画面が毎回その外部の相手を叩く——「群を外せば縁が切れる」が嘘になる。**群の中の関数を群の外から import したら、必ずこの形で関所を通すこと。**
### 逆に、関所を通してはいけない 2 か所(保存の検査)
保存済みの設定に、いま止めた群の名前が残っていることがある。それを綴り間違いと同じ扱いで弾くと、**その名前を送ってくるクライアントの保存が全部落ちる。**
送ってくるのは次の 2 つ。画面から普通に開き直した場合は、分類の一覧に無いので送られない。
- 群を止める前に開かれたままのタブ
- API を直接叩くクライアント
自動プロンプトの規則だけは事情が違い、**画面から編集しても必ず送られる**(ツール名は規則そのものの中身で、ツール一覧から組み直されないため)。落ちるのは、その群のツールを名指ししている規則の保存だけ。規則は 1 本ずつ送るので、他の規則は保存できる。
| 場所 | 使うもの |
|---|---|
| `space-tool-policy-api.ts` の保存検査 | `listToolCategories({ includeEnvDisabled: true })` |
| `space-auto-prompter-api.ts` の `knownToolNames` | `getToolCategoryMap({ includeEnvDisabled: true })` |
**「知っている名前か」を判定する側だけ**が `includeEnvDisabled` を使う。実際にどのツールが使えるかを決める側で使うと、層の向きが崩れる。ツール名やカテゴリ名を allowlist で検査する場所を新しく足すときは、ここに並べること。
### 画面に出ていない名前を、保存で消さないこと
`space-tool-policy-api.ts` の保存(PUT)は、**保存済みの名前のうちいま画面に出ていないものを、送られてこなくても残す**(`hiddenStoredNames`)。
画面が送る名前は GET が返した分類一覧から組み立てられる。群を止めるとその分類は一覧から消えるので、以後どの保存でも送られない。送られないことを「入れ直した」と読むと、そのワークスペースが**明示的に切っていた分類**が黙って消え、管理者があとで群を戻したときに既定 ON で復活する。切ったという意思のほうが失われるので、見えない名前は保存済みの値を残す。
残す対象は「既知(環境で止めた分を含む)だが、いまの画面には出ていない」名前だけ。綴り間違いの残骸はこれまでどおり掃除される。ツール名やカテゴリ名を丸ごと置き換える保存 API を新しく足すときは、同じ形にすること。
### 判定の記録
「いつ・なぜ・誰の設定で」は `getToolPackDecisions()` に全部載る。起動時に `[tool-packs] resolved` の 1 行がログに出る。群ごとの記録を足したくなったらここを読む——**新しい記録の仕組みを作らないこと。**
### 確認方法
```bash
npx vitest run src/engine/tools/tool-pack-gate.test.ts \
src/engine/tools/tool-pack-gate.wiring.test.ts \
src/bridge/space-api.tool-policy.pack-gate.test.ts
```
台帳に群を足したら、`config.yaml.example` の一覧・`ui/src/content/help/19-admin.md` の表・更新履歴も合わせて直す。あわせて `pieces/*.yaml` をその群のツール名で grep すること。既定のワークフローが「必ず〜を使う」の形でそのツールを名指ししていたら、群を止めた環境ではその指示が存在しないツールを指す。名指ししているワークフロー名はヘルプの注意書きに出す。
---
## 0. ツールのカテゴリ区分(workspace tool policy)
新しいツールを追加した場合、または既存ツールのカテゴリを変更した場合は、**`src/engine/tools/tool-categories.ts`** の `MODULE_SPECS` に登録し、安全(標準)かセンシティブかの区分を決める。workspace tool policy はこの定義を参照して、ワークスペースごとにツールの利用可否を解決する。
> **先に 0-A を読むこと。**新しいモジュールは、分類も既定 OFF も自分の `TOOL_MODULE` 申告に書ける。以下は申告を持たない既存モジュールの経路。
- `SENSITIVE_CATEGORIES`(現在: `ssh` / `browser`)に含まれるカテゴリのツールはデフォルト無効
- `SENSITIVE_TOOLS`(現在: `Bash` / `SpawnSubTask`)に列挙されたツールは、カテゴリが `core` であっても個別にセンシティブ扱い。`SpinOffTask` は `task-spawn.ts` の申告側にある
- 新カテゴリは安全側(デフォルト有効)が既定。意図的にセンシティブにする場合のみ `SENSITIVE_CATEGORIES` に追加する
**対象ファイル:**
- `src/engine/tools/tool-categories.ts` — `MODULE_SPECS` / `SENSITIVE_CATEGORIES` / `SENSITIVE_TOOLS`
- `src/engine/tools/index.ts` — `tryLoadModule` でのカテゴリ指定
- `src/bridge/tools-api.ts` — カテゴリ情報を返す `/api/tools` エンドポイント
- `ui/src/content/help/16-tools.md` — カテゴリ別の概要テーブルに行を追加
**確認方法:**
```bash
# tool-categories.ts のカテゴリ定義とツール一覧が揃っているか
npx vitest run src/engine/tools/tool-categories.test.ts
# センシティブ区分が resolver に正しく伝わるか(regression guard)
npx vitest run src/engine/tools/resolver.test.ts
```
---
## 1. ツールモジュールを新規追加した場合
**対象ファイル:**
- `src/engine/tools/<module>.ts` — 新モジュール本体
- `src/engine/tools/index.ts` — `tryLoadModule` で動的ロード追加
- `src/bridge/tools-api.ts` — `/api/tools` のモジュール一覧に追加
- `CLAUDE.md` — 「ツールモジュール構成」テーブルに行を追加
**なぜ必要か:**
`index.ts` はエージェント実行時のツール dispatch に使われ、`tools-api.ts` は Settings UI のピース編集画面でツール候補を返す API。
`tools-api.ts` への追加を忘れると、エンジンでは使えるのに UI のドロップダウンに表示されないという不整合が起きる(実際に amazon, speech, checklist, knowledge の4モジュールで発生した)。
**確認方法:**
```bash
# index.ts でロードしているモジュール数と tools-api.ts のモジュール数を比較
grep -c 'tryLoadModule' src/engine/tools/index.ts
grep -c "engine/tools/" src/bridge/tools-api.ts
# 数が一致していなければ漏れがある
```
---
## 2. 既存モジュールにツールを追加した場合
**対象ファイル:**
- `src/engine/tools/tool-categories.ts` — 新ツールを適切なカテゴリに割り当てる(sensitive は既定 OFF)。ツールの利用可否は workspace tool policy(設定→ツール)が決めるので、**piece 側に追加する必要はない**
- `CLAUDE.md` — 「ツールモジュール構成」テーブルの該当モジュール行に新ツール名を追加
- `docs/tools/{name}.md` — 新ツールの詳細ドキュメント(推奨)
- ツール `description` — 1 文 + 「詳細は ReadToolDoc({ name: "XXX" })」を末尾に記述
**なぜ必要か:**
ツールの提示は workspace tool policy が決める(カテゴリ単位。sensitive 以外は既定 ON)。カテゴリ未割り当てだとどのワークスペースでも出ない。
CLAUDE.md のテーブルが古いと、Claude Code 自身が既存ツールを認識せずに新規実装してしまうリスクがある。
ツール description は毎 LLM 呼び出しに乗るため 1 文に絞り、詳細は ReadToolDoc 経由で必要時のみ読み込む(agent-loop が movement 開始時に description サマリを自動カタログ化する)。
**確認方法:**
`tool-categories.ts` で新ツールがカテゴリに割り当てられているか確認する。
CLAUDE.md のモジュールテーブルに新ツール名が含まれているか確認する。
**実例: TestWorkspaceApp(2026-06-24):**
`src/engine/tools/app-test.ts` に追加 → `tools/index.ts` で `tryLoadModule` 追加 → `src/bridge/tools-api.ts` のモジュール一覧に追加 → `tool-categories.ts` でカテゴリ割り当て → `src/engine/tools/docs.ts` の `TOOL_DOC_ALIASES` に `testworkspaceapp: 'testworkspaceapp'` を追加 → `docs/tools/testworkspaceapp.md` を新規作成。(旧手順の「piece の allowed_tools に追加」は撤去済み)
---
## 3. ツールをリネーム・削除した場合
**対象ファイル:**
- `src/engine/tools/tool-categories.ts` — カテゴリ定義・`SENSITIVE_TOOLS` から旧名を削除/リネーム
- `src/engine/tools/raw-save.ts` — `RAW_SAVE_TOOLS` に旧名が残っていないか
- `ui/src/components/settings/ToolsWebForm.tsx` / `ToolsMediaForm.tsx` / `ToolsExternalForm.tsx` — ヘルプテキスト等にツール名の言及がないか
- `CLAUDE.md` — ツールモジュール構成テーブル
**なぜ必要か:**
BrowserAction を BrowseWeb に統合した際、5つの piece と UI ヘルプテキストに旧名の参照が残っていた。
**確認方法:**
```bash
grep -r '旧ツール名' pieces/ src/ ui/src/ CLAUDE.md
```
---
## 4. config.yaml に新しい設定キーを追加した場合
**対象ファイル:**
- `src/config.ts` — `ToolsConfig` / `AppConfig` / `LlmConfig` / `StorageConfig` 等のインターフェースにフィールド追加
- `src/config-normalize.ts` — v1 → v2 normalizer に新フィールドの mirror が要るか確認 (storage / llm 配下に置く場合)
- `config.yaml.example` — YAML キー(スネークケース)でサンプル・コメント追加。`config_version: 2` を必ず先頭に保つ
- `ui/src/components/settings/` — 該当セクションの Form コンポーネントに UI フィールド追加
- `CLAUDE.md` — 設定セクションに説明追加(必要に応じて)
**なぜ必要か:**
config.ts と config.yaml.example の不一致は「設定が効かない」「ドキュメントと実動作が違う」を招く。
Settings UI への追加を忘れると、YAML を直接編集しないと設定できなくなる。
v2 layout (`llm.*` / `storage.*`) では normalizer 側の backfill / migrate path も触らないと、v1 ファイルでの読み取り互換が壊れる。
**確認方法:**
```bash
# config.ts のフィールド数と config.yaml.example のキー数を目視比較
# Settings UI の各 Form ファイルで未対応フィールドがないか確認
# normalize の v1 fixture が落ちていないか
npx vitest run src/config-normalize.test.ts
```
---
## 4-B. 新しい Settings Form コンポーネントを追加した場合
**対象ファイル (両方必須):**
- `ui/src/components/settings/<Name>Form.tsx` — Form 本体
- `ui/src/components/settings/ConfigForm.tsx` — `renderActiveForm()` の `switch(section)` に新 `case 'new-id': return <NewForm {...formProps} />;` を追加
- `ui/src/components/settings/SettingsSidebar.tsx` — `CONFIG_GROUPS[].sections[]` に `{ id: 'new-id', label: '...' }` を追加。admin 限定なら親グループに `adminOnly: true`
- `ui/src/lib/urlState.ts` — section id の許可リストにも追加 (URL state からの復元)
- 旧 id をリネームした場合は `LEGACY_SECTION_REDIRECT` (SettingsSidebar.tsx) にエントリ追加してブックマーク互換を維持
**なぜ必要か:**
ConfigForm の switch と SettingsSidebar の section リストは独立に管理されており、片方だけ追加すると「サイドバーには項目があるが何も描画されない」「描画はされるがサイドバーから飛べない」状態になる。Step 3 (sidebar 再編) / Step 7-9 (form 分割) で両方を毎回触る前提で設計されている。
**禁止事項:**
- 削除した旧 Form (`ProviderForm.tsx`, `WorkersForm.tsx`, `WorkspaceForm.tsx` のような単一巨大 form) を復活させない。LLM Workers / Paths & Storage / Execution / Tools-* の分割は v2 design の前提
- カンマ区切り入力で配列フィールドを作らない。chip エディタ / textarea / multiselect のいずれかを使うこと (値に `,` を含めるケースを壊さないため)
- Secret 入力で `'********'` の magic string 判定を再導入しない。`SecretInput` (4-state: `unchanged` / `literal` / `env_ref` / `cleared`) を再利用すること
**確認方法:**
```bash
# section id が両方のファイルで揃っているか
grep -E "'[a-z-]+'" ui/src/components/settings/SettingsSidebar.tsx | grep "id:"
grep "case '" ui/src/components/settings/ConfigForm.tsx
# legacy redirect が漏れていないか
grep -A 20 'LEGACY_SECTION_REDIRECT' ui/src/components/settings/SettingsSidebar.tsx
```
---
## 4-C. TopBar のトップレベルタブ (page) を追加・削除した場合
`PageId` は複数ファイルに分散しているので、追加・削除のどちらでも下記をすべて触る。
**対象ファイル (すべて必須):**
- `ui/src/lib/urlState.ts` — `PAGES` 配列 (PageId の唯一の定義源)
- `ui/src/components/layout/TopBar.tsx` — `NAV_ITEMS` (タブ表示順 + adminOnly / requiresAuth)
- `ui/src/components/layout/NavDrawer.tsx` — `NAV_ICONS` は `Record<PageId, ReactNode>` の網羅型。**PageId を削除したら同名キーも消さないと TS が excess-property で落ちる**
- `ui/src/App.tsx` — `{page === '...' && <... />}` のレンダー分岐 (+ 不要になった import の削除)
- 外部からそのページへ飛ばす導線 (`url.searchParams.set('page', '...')` 等) の付け替え
- i18n `layout.nav.<id>` (`ui/src/i18n/locales/{en,ja}/layout.json`)
**履歴 (2026-06-19):** 旧「ユーザーフォルダ」(`userfolder`) タブは廃止。中身は個人ワークスペースの **設定** タブ (AGENTS.md / メモリ / Pieces / スキル / MCP / SSH / ブラウザ。`SpaceSettings.tsx`) に集約し、孤立していた **Pets** はグローバル設定の `pets` セクション (`settings/PetsForm.tsx`) へ移設した。`userfolder/` 配下のパネル (AgentsMdPanel / MemoryPanel / McpPanel / SshConnectionsPanel / SkillsPanel / AddBrowserSessionDialog / MonacoFileEditor 等) は SpaceSettings / SpaceBrowserPanel / 設定フォームが再利用するため残置。
---
## 4-D. タスク spawn に新しいフィールドを追加した場合(二重実装ミラー)
タスク/ジョブの spawn 経路は **2 系統の並行実装** になっている(統合は別課題):
- 通常作成: `src/bridge/local-tasks-crud-api.ts`(POST /api/local/tasks → createLocalTask + createJob)
- スケジュール実行: `src/scheduler.ts` の `executeAgentScheduledTask()`(createLocalTask + createJob を独自に呼ぶ。Router Phase 2 は通らない・metadata block はハードコード)
タスクやジョブに新しいフィールド(例: issue #974 の `llmWorkerId`/`requiredWorkerId`)を追加したら、**両方の経路に配線されているか必ず確認する**。片方だけだと「UI から作ると効くがスケジュール実行だと落ちる」型の欠落になる(#974 がまさにこれ)。
## 5. SSRF 保護に関わる変更をした場合
**対象ファイル:**
- `src/engine/tools/shared/ssrf.ts` — SSRF チェックロジック本体 + ブラウザへの装着(`installContextSsrfGuard`)
- `src/engine/tools/browser.ts` — BrowseWeb(`ensureContextHooks` / `openIsolatedGuardedPage` + `ssrfCheck`)
- `src/engine/tools/web.ts` — WebFetch / DownloadFile / 検索・スクショが借りる共有ブラウザ
- `src/engine/tools/x.ts` — X の card 画像取得(同じ共有ブラウザを借りる)
- `ui/src/components/settings/ToolsWebForm.tsx` — SSRF Allowed Hosts のヘルプテキスト
**なぜ必要か:**
`webfetchAllowedHosts` は WebFetch と BrowseWeb の両方で使われている。片方だけ修正すると保護に穴が開くか、意図しないブロックが発生する。
---
## 6. ログ出力に関わる変更をした場合
**対象ファイル:**
- `src/engine/tools/raw-save.ts` — `RAW_SAVE_TOOLS` / `RAW_LOG_ONLY_TOOLS` の更新
- 各ツールモジュール内の `appendXxxHistory()` 関数
**なぜ必要か:**
新ツールを追加したとき、`RAW_SAVE_TOOLS` への追加を忘れると `logs/raw/` に出力が保存されない。
ツール側で独自に raw 保存する場合は `RAW_SAVE_TOOLS` に含めない(二重保存回避)。
**保存先の整理:**
| ログ種別 | パス | 内容 |
|---------|------|------|
| 生データ | `logs/raw/{tool}-{timestamp}.txt/.json` | ツール実行結果の生出力 |
| WebFetch 履歴 | `logs/webfetch-history.jsonl` | URL, ステータス, サイズ |
| ダウンロード履歴 | `logs/downloads.jsonl` | 保存パス, サイズ |
| チェックリスト | `logs/checklists/{name}.json` | チェックリスト状態 |
| rawdata インデックス | `logs/rawdata-history.jsonl` | raw 保存のメタデータ |
---
## 7. ツールの詳細ドキュメントを書く場合
**対象ファイル:**
- `docs/tools/{toolname-lowercase}.md` — ツールの詳細な使い方ガイド
- 該当ツールの `description` — 概要 + `ReadToolDoc({ name: "XXX" })` で参照可能と明記
- `src/engine/tools/docs.ts` の `TOOL_DOC_ALIASES` — 関連ツール名を同じ doc にマップ(例: `checkitem: 'checklist'`)
- 既存 piece に同じガイダンスが書かれていれば削除する(重複解消)
**なぜ必要か:**
Tool description は毎回 LLM のコンテキストに乗るため肥大化させたくない。詳細手順・ワークフロー例は `docs/tools/{name}.md` に置き、`ReadToolDoc` ツールで必要時に取得する設計。
ReadToolDoc は META_TOOLS として常時利用可能(workspace tool policy でも常時 ON)。
関連ツール(CheckItem / CreateChecklist / GetChecklist 等)は1つの doc にまとめてエイリアス経由で引けるようにする。
**確認方法:**
- ファイル名は小文字(例: `BrowseWeb` → `browseweb.md`)
- description の末尾に「詳細は ReadToolDoc(...) で取得可能」を明記
- 全 piece から対応する重複ガイダンスを削除
---
## 8. piece の movement 構造を変更した場合
**対象ファイル:**
- 対象 `pieces/*.yaml` — `rules` に遷移先を明示追加
**なぜ必要か:**
`default_next` は機械的フォールバック専用。LLM が選べる遷移先は `rules[].next` に明示されたものだけ。
`rules` に追加せず `default_next` だけに頼ると、LLM が遷移先を選択できない。
---
## 9. Reflection に新しい memory `type` を追加した場合
**対象ファイル:**
- `src/engine/reflection/types.ts` — `ReflectionMemoryType` の union を拡張
- `src/engine/reflection/reflection-schema.ts` — LLM tool schema の `type` enum
- `src/engine/reflection/reflection-prompt.ts` — システムプロンプトの type 説明
- `src/engine/reflection/semantic-validator.ts` — `ALLOWED_TYPES` セット
- `src/bridge/memory-api.ts` — PUT のバリデーション
- `ui/src/components/settings/MemoryLearningForm.tsx` — type ドロップダウン
**なぜ必要か:**
4 種類 (user / feedback / project / reference) はメモリ意味論の単位。LLM は schema で型強制されるので、schema・prompt・validator のいずれかを忘れると LLM が「未知の type」を吐いて全 reflection が `rejected_unknown_type` で落ちる。UI ドロップダウンの更新を忘れるとユーザーが手動編集できなくなる。
---
## 10. Reflection に新しい rejection code を追加した場合
**対象ファイル:**
- `src/engine/reflection/types.ts` — `ReflectionRejectionCode` の union を拡張
- `src/engine/reflection/semantic-validator.ts` — 新しいケースで返す
- `src/engine/reflection/applier.ts` — 必要に応じて applier 側の rejection 経路も追加
- `src/bridge/memory-api.ts` — 同じコードを返す PUT バリデーションを更新
- `ui/src/components/settings/MemoryLearningForm.tsx` — UI 側でコード → 日本語メッセージ変換
- `src/engine/reflection/applier.fuzz.test.ts` — fuzz の "known code list" に追加
**なぜ必要か:**
rejection code は metrics・UI 表示・fuzz invariant の3箇所で参照される。fuzz テストは「decision の code は known list のいずれか」と assertion しているので、追加し忘れると fuzz が落ちる。UI 側で未対応コードはユーザーに「不明なエラー」と表示されてしまう。
---
## 11. MCP サーバー追加 / authKind 仕様変更時
**対象ファイル:**
- `src/mcp/registry.ts` — `auth_kind` の受け入れ・バリデーション
- `src/mcp/token-manager.ts` — `hasToken` / `getValidToken` の authKind 分岐
- `src/bridge/mcp-api.ts` — admin (global server) ルート、`/api/mcp/connections`
- `src/bridge/user-servers-api.ts` — user-owned server CRUD
- `ui/src/components/userfolder/McpServersPanel.tsx` — authKind ラジオ + 条件付きフォーム
- `ui/src/components/userfolder/McpConnectionsPanel.tsx` — authKind=api_key は Authorize ボタン非表示
- `docs/mcp.md` — 運用者向け手順 (OAuth と api_key 両方)
**なぜ必要か:**
authKind は server 行に保存され、token-manager・aggregator・UI のそれぞれで分岐する。Phase 8 で OAuth 専用設計から拡張したため、新しい authKind を入れる場合は schema migration + UI フォーム + docs の 3 点を必ず触る。OAuth start ルートは authKind=oauth でしか動かない (api_key は 400 を返す) ので、route 側のガードも忘れない。
**確認方法:**
- `grep -rn "authKind" src/mcp/ src/bridge/mcp-api.ts src/bridge/user-servers-api.ts ui/src/components/userfolder/Mcp*.tsx` で参照漏れを確認
- 統合テスト: `npx vitest run src/mcp/integration.test.ts` (OAuth + api_key 両 path をカバー)
---
## UI フォントサイズスケール (Tailwind class 規約)
UI に新しいパネルやコンポーネントを追加するときは、以下の **6 段階スケール** から選ぶ。それ以外の `text-[Npx]` を導入すると全体の字組が崩れる。
| サイズ | クラス | 用途 |
|---|---|---|
| 10px | `text-[10px]` | セクションラベル(uppercase)、極小バッジ |
| 11px | `text-2xs` ⭐ (Tailwind 拡張、`tailwind.config.js` で定義済み) | ヘルプテキスト、キャプション、メタ情報 |
| 12px | `text-xs` | 本文小、ボタンラベル、フォーム入力、テーブルセル |
| 13px | `text-[13px]` (Tailwind 標準に該当なし) | パネル本文、メイン段落 |
| 14px | `text-sm` | フォームラベル、ナビゲーション項目、見出し小 |
| 16px+ | `text-base` / `text-lg` / `text-xl` / `text-2xl` | h2 セクション見出し、ページタイトル、ヒーロー |
### 禁止リスト
以下は **絶対に書かないこと**。書いた場合は `2026-05 commit 993ef2f` の正規化と同様の一括置換でやり直しになる。
| 書いてしまった | 置き換え先 |
|---|---|
| `text-[11px]` | `text-2xs` |
| `text-[12px]` | `text-xs` |
| `text-[14px]` | `text-sm` |
| `text-[15px]` | `text-sm` |
| `text-[16px]` | `text-base` |
| `text-[18px]` | `text-lg` |
| `text-[9px]` | `text-[10px]` (極小に丸め) |
| `text-[12.5px]` | `text-[13px]` |
| 任意の非標準 px | 上記スケールに最も近いものに丸める |
### なぜ必要か
- 過去に新規パネル (Help, MCP, AGENTS.md 等) を追加するたびに implementer が独自に `text-[Npx]` を書き、累積で **13 種類のサイズ** が混在した
- 統一したフォントサイズは UI の一貫性 + 認知負荷の低下に直結する
- Tailwind utility (`text-xs/sm/base/lg/xl/2xl`) は font-size と line-height がセット定義されているので、行間も自然に揃う
- 不要な arbitrary value (`text-[Npx]`) は CSS の出力サイズも増やす
### 確認方法
```bash
cd ui
# 禁止サイズが残っていないか確認 — 何も出なければ OK
grep -rohE 'text-\[(9|11|12|12\.5|14|15|16|17|18)px\]' src --include='*.tsx'
# 全体のサイズ分布を確認
grep -rohE 'text-\[[0-9]+(\.[0-9]+)?px\]|text-(xs|sm|base|lg|xl|2xl|3xl|4xl|2xs)' src --include='*.tsx' | sort | uniq -c | sort -rn
```
期待される分布 (Tailwind utility が主、arbitrary は 10px / 13px のみ):
```
text-xs (~250 ↑)
text-2xs (~180 ↑)
text-sm (~100 ↑)
text-[13px] (~100)
text-[10px] (~100)
text-base, text-lg, text-xl, text-2xl, text-4xl ... (少量)
```
### 触る場所
- 新規 UI コンポーネント全般 (`ui/src/**/*.tsx`)
- 既存パネルへの追加要素
- 設定フォーム (`ui/src/components/settings/`)
- ダイアログ・モーダル
---
## 12. SSH 関連変更時 (Phase 0-8)
### 12-A. SSH ツール (`SshExec` / `SshUpload` / `SshDownload`) を変更したとき
**対象ファイル:**
- `src/engine/tools/ssh.ts` — TOOL_DEFS + executeTool + 12-step orchestration
- `src/engine/tools/ssh.test.ts` — unit tests (23 件)
- `src/engine/tools/ssh.e2e.test.ts` — in-process ssh2 server e2e (4 件、SKIP_SSH_E2E=1 で skip)
- `src/engine/tools/index.ts` — `tryLoadModule('./ssh.js')` 経由のロード + dispatch
- `src/engine/tools/core.ts` — `ToolContext.pieceName` (piece grants check に必要)
- `src/engine/piece-runner.ts` — `ToolContext` 構築時に `pieceName: piece.name` を thread
- `src/engine/tools/docs.ts` — `TOOL_DOC_ALIASES` で 3 ツール名を `'ssh-tools'` にマップ
- `docs/tools/ssh-tools.md` — LLM 向け詳細ドキュメント
- `src/bridge/server.ts` — `setSshSubsystem({...})` で DI
**新ツールを SSH 系で追加する場合、または既存ツールに引数を増やす場合:**
- ssh.ts の `TOOL_DEFS` description は **1 文 + 「詳細は ReadToolDoc」** に絞る (毎呼び出しのトークン節約)
- preflight (steps 1-7) を流用するため `runExec`/`runUpload`/`runDownload` の skeleton を踏襲
- `finally` で `clearBuffer(pem, passphrase)` を必ず実行 — ssh2 の internal retention は防げないが我々の copy は zeroize する
- 監査 action 名は `ssh.<verb>` 形式 (例: `ssh.exec`, `ssh.upload`)。command 全文は **記録せず SHA-256 16-char hex** を `detail.command_hash` に
- piece grants check は `subsystem.accessResolver.resolve()` を必ず通す (piece membership → access decision → state checks → policy の順)
**新しい SshSessionError code を追加する場合:**
- `src/ssh/session.ts` の `SshSessionErrorCode` union を拡張
- `src/engine/tools/ssh.ts` の `formatSessionError` switch に LLM-actionable message を追加
- `docs/tools/ssh-tools.md` の「共通エラーコード一覧」表に追記
- `docs/ssh.md` の「Troubleshooting → Symptom → cause table」に追記
**確認方法:**
```bash
# tool が grants check を通っているか
grep -n "accessResolver\.resolve\|preflight" src/engine/tools/ssh.ts
# 監査 action 名が既存と整合
grep -n "action:" src/engine/tools/ssh.ts src/bridge/ssh-api.ts | sort -u
# tests
npx vitest run src/engine/tools/ssh.test.ts
SKIP_SSH_E2E= npx vitest run src/engine/tools/ssh.e2e.test.ts # 明示的に run
```
### 12-B. SSH HTTP API (`src/bridge/ssh-api.ts`) を変更したとき
**対象ファイル:**
- `src/bridge/ssh-api.ts` — User router + Admin router (25 endpoints)
- `src/bridge/ssh-api.test.ts` — auth / maintenance / reason / CRUD / TOFU / grants / rotation
- `src/ssh/maintenance.ts` — in-memory maintenance flag
- `src/ssh/admin-rate-limit.ts` — token bucket for force-unlock (10/hr)
- `src/bridge/server.ts` — `ssh.enabled` ゲートで両 router をマウント
- `src/ssh/session.ts` — `sshTest()` (HTTP test endpoint で使用)
- `docs/ssh.md` §"HTTP API Reference" — endpoint table
**新しい admin write エンドポイントを追加するとき:**
- `body.reason` 必須 (≥ 8 chars)。`validateReason()` を必ず呼ぶ
- 操作直前に `maintenance503()` ガードを呼ぶ (read 系は不要)
- 成功・失敗どちらも `auditRepo.beginAndComplete()` で `ssh_audit_log` に記録
- 監査 action は `src/ssh/audit-repo.ts` の `SshAuditOutcome` と Phase 5 設計書の enum を踏襲
- admin-only フラグ (`allow_remote_unrestricted`, `allow_private_addresses` per-connection) は user 経路で常に reject
**新しい user write エンドポイントを追加するとき:**
- `getUserId()` で取得した userId をオーナーチェックに使う
- 他人の所有物に対しては `404` を返す (存在を漏らさない)
**確認方法:**
```bash
# admin endpoint で reason check / maintenance ガードが漏れていないか
grep -n "deps.requireAdmin\|maintenance503\|validateReason" src/bridge/ssh-api.ts
# audit log の action 名が既存と整合しているか
grep -n "action:" src/bridge/ssh-api.ts
```
### 12-C. SSH repo / schema を変更したとき
**対象ファイル:**
- `src/db/schema.sql` — `ssh_connections`, `ssh_connection_grants`, `ssh_audit_log`, `ssh_abuse_counters`, `system_deks`
- `src/db/migrate.ts` — `PRAGMA table_info` → 列存在チェック → `ALTER TABLE ADD COLUMN` パターン (バージョン管理テーブル不使用)
- `src/ssh/connection-repo.ts` / `grants-repo.ts` / `audit-repo.ts` / `abuse-repo.ts` / `access.ts`
- 各 repo に対応する `*.test.ts`
- `src/ssh/recovery.ts` — boot 時 pending → aborted sweep
- `src/ssh/crypto.ts` — DEK ラップ / マスター鍵検証
- `docs/ssh.md` §"Connection Model" / §"Access Grants" / §"Audit Log"
**スキーマ列を追加するとき:**
- `schema.sql` の `CREATE TABLE` に列追加 (初期スキーマ)
- `migrate.ts` に冪等 ALTER TABLE 追加 (既存 DB 用)
- 既存テストが影響を受けないか `npm test` で検証
- 列をエンドポイントから書く場合、`POST/PATCH` ハンドラのバリデーションに追加し、admin-only フラグなら user 経路で reject
**新しい監査 action を追加するとき:**
- `src/ssh/audit-repo.ts` の `SshAuditOutcome` は触らない (固定 5 値: `pending|success|failed|denied|aborted`)
- action 名は `ssh.<entity>.<verb>` 形式 (例: `ssh.connection.host_key.replace`)
- `docs/ssh.md` §"Audit Log → Actions" 表に追加
- `docs/tools/ssh-tools.md` の「監査ログ」セクションは tool 経由 action のみなので、tool 起因なら追記
**確認方法:**
```bash
# 監査 action 一覧 (重複/typo 検出)
grep -rn "action: 'ssh\." src/ | awk -F"'" '{print $2}' | sort -u
# repo CAS / transaction が崩れていないか
grep -n "BEGIN\|COMMIT\|prepare(" src/ssh/*.ts | head -30
```
### 12-D. SSH UI を変更したとき
**対象ファイル:**
- ユーザー向け:
- `ui/src/components/userfolder/SshConnectionsPanel.tsx` — 一覧 + actions
- `ui/src/components/userfolder/SshConnectionForm.tsx` — 作成・編集フォーム
- `ui/src/components/userfolder/SshHostKeyDialog.tsx` — TOFU verify ダイアログ
- 管理者向け:
- `ui/src/components/settings/SshForm.tsx` — Settings → SSH のルート
- `ui/src/components/settings/SshGlobalConnectionsForm.tsx` — グローバル接続 CRUD
- `ui/src/components/settings/SshGrantsForm.tsx` — grants CRUD
- `ui/src/components/settings/SshAuditLog.tsx` — 全テナント監査ログ
- `ui/src/components/settings/SshMasterKeyRotationForm.tsx` — 鍵 rotation
- 型定義: `ui/src/lib/ssh-types.ts`
- `ui/src/components/settings/ConfigForm.tsx` / `SettingsSidebar.tsx` — `SshForm` の登録 (新規 Form 追加時)
**確認方法:**
- 新 form を追加した場合 `ConfigForm.tsx` の section リストに登録、`SettingsSidebar.tsx` のナビ項目に追加
- ssh-types.ts と API レスポンス shape (`SshConnection`, `SshGrant`, `SshAuditRow`) が一致しているか
- 禁止フォントサイズ (`text-[11px]` 等) を導入していないか — 既存セクションの「UI フォントサイズスケール」参照
### 12-E. SSH 接続の認可 (workspace connection scoping) を変更したとき
SSH 接続の認可は **piece ではなくワークスペース**で決まる(設定→SSH の登録接続)。
piece の `allowed_ssh_connections` は撤去済み。接続スコープは worker が解決して
`workspaceSshConnections` として piece-runner に渡す。
**対象ファイル:**
- `src/worker.ts` — `parsedPolicy.enabledSensitive` に `ssh` があるとき `listBySpace(spaceId)` で接続 ID を解決(`workspaceSshConnections`)
- `src/engine/piece-runner.ts` — `workspaceSshConnections ?? []` を `ctx.allowedSshConnections` / `movement.allowedSshConnections` に流す
- `src/engine/tools/ssh.ts` — preflight で `ctx.allowedSshConnections` に対して接続 ID を検証
- `docs/ssh.md` §"接続の認可"
**規約:**
- 接続が 1 つも登録されていない(空配列)→ SSH ツールはクリーンに reject
- `'*'` ワイルドカードは worker からは渡さない(常に明示 ID リスト = クロススペース分離保証)
**確認方法:**
```bash
# worker が ssh 接続を解決している経路
grep -n 'workspaceSshConnections' src/worker.ts src/engine/piece-runner.ts
```
### 12-F. config.yaml の SSH セクション (`SshRuntimeConfig`) を変更したとき
**対象ファイル:**
- `src/ssh/config.ts` — `SshRuntimeConfig` interface + `SSH_DEFAULTS` + `mergeSshConfig`
- `src/config.ts` — `transformKeys` でスネーク→キャメル変換、`AppConfig` に統合
- `config.yaml.example` — コメント付きデフォルト値
- `ui/src/components/settings/SshForm.tsx` — UI で編集可能にする場合
- `docs/ssh.md` §"`config.yaml` Reference"
**確認方法:**
```bash
# config.ts と config.yaml.example の SSH キー数が一致するか
grep -E '^\s+[a-z_]+:' config.yaml.example | grep -A 30 '^# ssh:' | head -40
```
### 12-G. SSH Console (interactive PTY) を変更したとき
SSH Console は SshExec/Upload/Download とは別系統の対話的 PTY 経路。Phase 8 で新規追加。
- 新ツール SshConsoleEnsure / SshConsoleSend / SshConsoleSnapshot を追加した時:
- [ ] `src/engine/tools/ssh-console.ts` の `TOOL_DEFS` に追加
- [ ] `src/engine/tools/index.ts` の dispatch + aggregation に追加
- [ ] `src/engine/tools/docs.ts` の `TOOL_DOC_ALIASES` にエイリアス
- [ ] `docs/tools/ssh-console-tools.md` を更新
- 新 WS endpoint を追加した時:
- [ ] `src/bridge/server.ts` に upgrade handler 配線
- [ ] auth / visibility / access の 4 段階チェックを記述
- 新 `SessionCloseReason` を追加した時:
- [ ] `src/ssh/console-protocol.ts` (server) と `ui/src/lib/ssh-console-types.ts` (UI) の両方に追加
- [ ] `ConsoleHeader.tsx` の disconnected reason 表示に文言を追加
## 13. Scheduler から呼ばない手動オペレーション endpoint
以下の endpoint は **UI からの手動操作専用** で、scheduler / Routine / 自動化経路から起動できない設計になっている。Routine 側の payload schema にこれらの override を追加してはいけない (scheduled task のツール境界〈ワークスペースのツールポリシーで決まる〉が想定外に拡大するリスクのため)。
- `POST /api/local/tasks/:id/continue` — 別 piece で task を続ける (handoff)
- 実装: `src/bridge/local-tasks-api.ts` の `/continue` ハンドラ
- terminal 状態 (`succeeded` / `failed` / `waiting_human` / `cancelled`) のときのみ有効
- 新カテゴリを追加する場合: `scheduled-tasks-api.ts` / `scheduler.ts` 側に「実行する piece を override する」機能を持たせない。実行する piece は task 作成時に固定し、人間が UI から明示的に切り替える経路だけ残す
**確認方法:**
```bash
# scheduler 経路から /continue を叩いている箇所が無いか
grep -rn "/continue\b\|continueTaskWithPiece" src/scheduler.ts src/bridge/scheduled-tasks-api.ts
# 無いことが期待値
```
---
## 14. Knowledge Notes 機能 (削除済み)
共有 Markdown ノート + 購読 + `SearchNotes` / `ReadNote` / `WriteNote` ツールからなる
Knowledge Notes 機能は 2026-06 に削除された (共有ワークスペースと役割が重複したため)。
`src/notes/`・`src/bridge/notes-api.ts`・`src/engine/notes-inject.ts`・`NotesForm.tsx`・
`note_index` / `note_subscriptions` / `pending_reindex` テーブルはすべて撤去済み。
既存 DB に残る孤立テーブルは無害なので DROP していない。再導入の予定はない。
---
## Bash Unrestricted モード (`safety.bash_unrestricted`) の変更時
**対象ファイル:**
- `src/engine/tools/sandbox.ts` — bwrap サンドボックスのマウント構成・実行ロジック
- `src/engine/tools/core.ts` — `executeBash` の分岐 (`ctx.bashUnrestricted`)、`ToolContext` インターフェース
- `src/engine/piece-runner.ts` — `safetyConfig.bashUnrestricted` → `ToolContext` への伝播
- `src/worker-bootstrap.ts` — 起動時 bwrap 可用性チェック
- `src/config.ts` — `SafetyConfig.bashUnrestricted` 定義・バリデーション
**なぜ必要か:**
bwrap のマウント構成を変えた場合、セキュリティ境界が変わる。テスト (`sandbox.test.ts`, `core.test.ts` の `bashUnrestricted mode` セクション) を必ず更新すること。
---
## 15. A2A 認可サーバー (`src/bridge/a2a/`)
外部エージェント連携(A2A)の OAuth2 認可サーバー関連を変更するときに連動が必要な箇所。
### DB テーブル・スキーマを変更するとき
- `src/db/schema.sql` の `CREATE TABLE`(`oidc_models` / `a2a_clients`)を更新(初期スキーマ)
- `src/db/migrate.ts` に冪等 `ALTER TABLE ADD COLUMN` を追加(既存 DB 用)。**dual-path 必須**(どちらか片方だけ更新するとテストが大量に落ちる)
- `src/db/repository.a2a.ts` / `src/bridge/a2a/oidc-adapter.ts` のクエリを合わせて修正
### `config.yaml` の `a2a` セクションを変更するとき
- YAML キーはスネークケース (`client_ttl_seconds`)、コード内はキャメルケース (`clientTtlSeconds`)
- `src/config.ts` の `transformKeys` が自動変換するため、読み取り側では キャメルケースのみ使う
- `config.yaml.example` にコメント付きで追記する
- `A2aConfig` interface(`src/config.ts`)に対応フィールドを追加
### A2A ルーターを変更するとき
- `createConsentRouter` / `createA2aClientsAdminRouter` / `mountA2aOidc` はいずれも `src/bridge/server.ts` でマウントされる。新しい router を追加した場合は `server.ts` の配線を忘れずに
- 管理者専用エンドポイントは `requireAdmin` ミドルウェアを必ず通すこと
- ユーザー向け同意エンドポイントは `requireAuth` を通し、操作対象の `sub`(subject)をセッションの userId と照合する
### A2A 公開スキル(スペース許可リスト)を変更するとき
- 公開スキルは `spaces.a2a_skills`(JSON 配列カラム)に保存される。スキーマ変更時は `schema.sql` と `migrate.ts` の**両方**(dual-path 必須)を更新すること
- 委任は `a2a_delegations` テーブルで管理し、`grant_id` で OAuth grant(`oidc_models`)にリンクする。`grant_id` の整合性は外部キーではなくアプリ層で保証しているため、grant 削除時は対応する delegation も削除すること
- 委任フィルタは **`buildVisibilityWhere` の OR 句に追加しない**。タスク/ジョブの公開スコープ(private/org/public)とは独立したフィルタとして AND 交差で適用すること(OR に混ぜると cross-space IDOR の原因になる)
- Agent Card の公開エンドポイント(`GET /.well-known/agent.json`)とスペースオーナー向け API(公開スキル設定 PATCH)は、いずれも `a2a.enabled` ゲートを通過した場合のみ有効。ゲートを外したルートを追加しないこと
### A2A スキル実行(executor)を変更するとき
- `a2a_tasks` テーブルは `schema.sql` と `migrate.ts` の**両方**(dual-path 必須)を更新すること
- executor は `createLocalTask` → `createJob` の順に呼ぶ。`createLocalTask` 単体はジョブをエンキューしない(`createJob` で初めて Worker がポーリングで拾える状態になる)
- ジョブ完了の検知はポーリング(`a2a_tasks` の `status` カラムを定期参照)で行い、進捗は SSE で外部エージェントに返す
- 実行完了後、`output/` 以下のファイルは A2A Artifact として返却する
- 委任スコープは executor でも `computeEffectiveScope` を通じて再強制する。トークンの委任スコープがスペースの公開スキル設定を上回ることは許容しない(fail-closed)
- `buildVisibilityWhere` の OR 句に委任条件を追加しないこと(AND 交差で適用する)
### 非ブロッキング送信 + reconciler(`reconciler.ts` / `task-finalize.ts`)を変更するとき
- **非ブロッキング契約**: `message/send` の `params.configuration.blocking: false` は SDK ネイティブ機能。executor が初期 Task(`submitted`)を publish した時点で SDK が即座にレスポンスを返す(完了を待たない)。外部エージェントは後から `tasks/get`(`params.id`)で最新状態を取得する。executor 自体を非ブロッキング化する改造は不要(SDK 側で完結)。
- **reconciler は単一起動**: `A2aTaskReconciler` は `server.ts` で `a2a.enabled` ゲート内から 1 インスタンスだけ `start()` する(多重起動は state churn の原因)。テストでは `start()` の interval を使わず `reconcileOnce()` を明示的に呼んで決定論化すること。
- **DB が source of truth**: reconciler は `a2a_tasks`(非 terminal 行)を `listNonTerminalA2aTasks` で走査し、リンク先の MAESTRO ジョブ状態(`getJob`)と突き合わせて収束させる。live executor が切断・再起動で消えても、reconciler だけでタスクを terminal 化できる(再起動耐性)。in-memory の per-task detached promise には依存しない。
- **終端化ロジックは共有**: `enumerateOutputArtifacts` / `finalizeStatusFromJob` / `TERMINAL_JOB_STATUSES`(`task-finalize.ts`)を executor と reconciler の**両方**が使う。ジョブ状態 → A2A state の写像や artifact 列挙を変えるときは、この共有ヘルパだけを直せば両経路に反映される(ロジックを片方に複製しないこと)。
- **冪等な状態一致スキップ**: reconciler は `payload.status.state` が算出後の state と一致する場合は再書き込みしない(`waiting_human → input-required` の churn 防止)。`saveA2aTask` は task id の upsert なので executor と reconciler が競合しても二重書きは安全。
- **委任カラムの保持**: reconciler が `saveA2aTask` を呼ぶ際は `payload.metadata` の `a2aDelegationId` / `a2aGrantId` / `a2aActingUserId` を `a2a_tasks` の `delegation_id` / `grant_id` / `acting_user_id` 列へ写す(`task-store.ts` の save 規約と同一)。列を増やすときは executor の初期 Task metadata・`SqliteA2aTaskStore.save`・`reconciler.persistTask` の 3 箇所を揃えること。
### A2A 成果物ダウンロード(`artifact-download-api.ts` / `artifact-parts.ts`)を変更するとき
- **artifactId は決定的**: `deriveArtifactId(a2aTaskId, fileName)` から作る。ランダム UUID に戻すと、executor が流した ID と reconciler が組み直した ID が食い違い、`tasks/get` のたびに成果物の識別子が変わる(受け入れ条件「ID の安定性」が崩れる)。
- **URI は識別子だけ**: `{baseUrl}/a2a/artifacts/{taskId}/{artifactId}`。ワークスペースの実パス・worktree 名・`output/` 相対パスのいずれも URI へ入れないこと。
- **baseUrl は 1 か所から導出**: `resolveA2aBaseUrl`(`a2a-subsystem.ts`)を executor(`request-handler.ts` 経由)と reconciler の両方へ渡す。片方だけ変えると経路によって URI が変わる。
- **認可は書き写さない**: 可視性は `getLocalTask(id, { viewer })`(=`buildVisibilityWhere`)にそのまま乗せ、委任のスペース範囲は `principal.delegation.grantedSpaceIds` との AND 交差で重ねる。可視性のルール(持ち主・組織・公開・スペースメンバー)をこのルータへ再実装しないこと。
- **タスクと委任の突き合わせは grant_id ではなく当事者で**: `resolveTaskDelegationParty` が返す `acting_user_id` × 委任行の `client_id` で一致を見る。`grant_id` の一致に戻すと、アクセストークン(寿命 10 分・更新トークン無し)を取り直すための再承認が新しい grant を作り、それ以前に配ったリンクが一斉に 404 になる(PR #1111 レビュー P2-1)。引いた委任行の生死は見ないこと——提示されたトークン側の生死・期限は `authenticateA2aRequest` が引き直している。
- **`deriveArtifactId` の区切りは生バイトで書かない**: NUL はエスケープ表記(`\u0000`)で書く。生の 1 バイトをソースへ置くと git がファイル全体を二値と判定し、以後この中核モジュールの差分がどこでも読めなくなる。
- **どのファイルを返すかは記録済みメタ情報から引く**: クライアントが送れるのは taskId と artifactId だけ。パスを受け取るクエリ引数を足さないこと。パス解決は必ず `ensurePathWithin(workspace/output, name)` を通す(字句チェックと realpath チェックの両方がここに入っている)。
- **チャットコネクタ経路には baseUrl を渡さない**: `chat-connector-service.ts` は SDK の TaskStore を通さず `a2a_tasks` 行が残らないため、URI を載せると開けないリンクになる。
**確認方法:**
```bash
# A2A 関連テスト一括実行
npx vitest run src/bridge/a2a/ src/db/migrate.test.ts src/db/repository.a2a.test.ts src/db/repository.a2a-deleg.test.ts
# router のマウント漏れを確認
grep -n "mountA2aOidc\|createA2aClients\|createConsent" src/bridge/server.ts
# buildVisibilityWhere に委任条件が混入していないか確認
grep -n "a2a_delegations\|a2a_skills" src/db/repository.ts
```
---
## 16. OSS リリース(`scripts/oss-sync.sh --push`)を実行するとき
- **スクリーンショットの目視確認(必須)**: `docs/screenshots/*.png` はバイナリのため、release gate(`oss/forbidden.txt` の正規表現スキャン)では**描画内容を検査できない**。スクリーンショットを更新した場合は、push 前に各画像を開いて以下が写り込んでいないか目視で確認する
- 内部ホスト名・URL(Gitea / 社内サービスのアドレスバー等)
- 実在のタスクタイトル・会話内容・ファイル名
- ユーザー名・メールアドレス・組織名
- 資格情報やトークンの断片(設定画面のマスク漏れ)
- 撮り直しの手順は README スクリーンショット刷新時のレシピ(隔離 worktree + シードデータ)に従い、実データ環境のキャプチャをそのまま使わない
---
## 17. 承認パーク型 META ツール(RequestTool / RequestPackage)を追加・変更する場合
エージェントが「これが欲しい」と申告 → 人が承認 → ジョブ再開、という停車型ツールの配線点。RequestPackage(PR3)は RequestTool を全面ミラーしている。片方だけ直すと不整合になるので、両方を並べて確認すること。
**対象ファイル(RequestPackage を例に):**
- `src/engine/tools/package-request.ts` — ツール本体(`TOOL_DEFS` + `executeTool`)。バリデーションは `engine/python-packages.ts` の `parseAndValidateSpec` / `assertNotShadowing` を再利用
- `src/engine/tools/core.ts` — `ToolContext` に park フラグ(`pendingPackageApproval`)+ recorder(`recordPackageRequest`)+ 補助状態(`installedPackages` / `declinedPackages`)
- `src/engine/tools/index.ts` — module loader / `Object.assign(allDefs, ...)` / dispatch 分岐(**META の直書き配列は撤去済み。#1126**)
- 常時利用の登録先は 1 か所になった。モジュールの `TOOL_MODULE.metaTools` に書く(0-A 参照)。申告を持たない既存モジュールの分だけ `tool-categories.ts` の `META_TOOLS` Set に残っている
- `src/engine/agent-loop/pending-states.ts` — `buildPackageApprovalWaitResult`(park → `next:'WAITING_HUMAN_PACKAGE_REQUEST'`)
- `src/engine/agent-loop.ts` — import + `buildToolApprovalWaitResult` の隣で呼ぶ
- `src/engine/piece-runner.ts` — `WAITING_HUMAN_PACKAGE_REQUEST` sentinel 分岐(waiting_human park)+ **2 つの PieceRunOptions interface** に recorder/補助状態を追加 + ctx 構築で piece/movement を注入
- `src/worker.ts` — recorder を task/job/space にバインド(feature enabled かつ space 解決可のときのみ)+ waiting_human 分岐で **`wait_reason='package_request'` を永続化**(忘れると resume がマッチせず永久停止)
- **DB(三重ミラー)**: `src/db/schema.sql` / `src/db/migrate.ts` / `src/db/repositories/schema.ts`(`package_requests` テーブル)+ `src/db/repositories/tool-requests.ts`(record/get/list/decide/revert/listDeclined + 型 + rowMapper)+ `src/db/repositories/jobs.ts`(`resumePackageRequestJob` + `createJobIfNoPending` の承認ポーズ判定)。実装は `repositories/*` に置き、`src/db/repository.ts` ファサードに委譲メソッドを足す(`repository.ts` は肥大分割済みの薄いファサード)
- `src/bridge/local-tasks-package-requests-api.ts` — GET 一覧(view 権限)+ POST decide(**task write 権限= owner/admin/space editor のみ**、エージェント自己承認不可)。承認時のインストールは `space-python-packages-service.ts` の共有パスを通す
- `src/bridge/local-tasks-api.ts` — ルート登録 + `LocalTasksApiOptions` に config アクセサ / `server.ts` で配線
- `ui/src/components/chat/PackageRequestApproval.tsx` + `ChatPane.tsx` + `ui/src/api/tasks.ts` + i18n `chat.json`(ja/en)
- `docs/tools/requestpackage.md`(ReadToolDoc)+ ヘルプ + changelog
**なぜ必要か:**
停車系は「ジョブ停車=ワーカー解放」で成り立つ(ツール内同期ブロックはデッドロック)。sentinel(`WAITING_HUMAN_*`)は tool → pending-states → agent-loop → piece-runner → **worker(wait_reason 永続化)** の全経路が揃って初めて機能する。承認 API は必ず write 権限ゲートを通し、インストールは共有サービス(bwrap 必須 fail-closed / wheels のみ / 固定 index / shadowing 拒否)経由に限定する。
---
## 自動検知の可能性
- **ツールモジュール登録漏れ**: `index.ts` と `tools-api.ts` のモジュール一覧を比較するスクリプトで CI チェック可能
- **code-review-graph**: `importers_of` で各ツールモジュールの参照元を列挙できるが、「tools-api.ts にも登録すべき」というルールの自動適用は困難。変更時の `detect_changes` + `get_impact_radius` で影響範囲の見落としを防ぐ用途が現実的