DeepSeek Harness で初めてプラグインコードを書くとき、実際にあなたを詰まらせるのは TypeScript の文法ではなく、多くの場合、三つの具体的な疑問です:apply 関数は一体何を受け取るのか、ctx コンテキストオブジェクトは何ができるのか、書いたプラグインはなぜフレームワークに読み込まれるのか。この三つの疑問は、それぞれ本チュートリアルの三つの中核キーワード——apply、ctx、cordis.yml——に対応しています。多くの人はプラグイン開発を複雑なことだと想像しますが、実際には Harness において、正当なプラグインとは apply 関数と name 定数をエクスポートする TypeScript モジュールにすぎません。フレームワークがそれを読み込むとき、apply を呼び出し、Context オブジェクトをあなたの手に渡します。あなたはこのオブジェクトを通じて機能を登録し、残りはフレームワークに返します。本節は全文の第 1/2 部分として、まず「プラグインとは何か」を徹底的に説明します。apply の関数シグネチャの分解から始め、name の二重の身分と ctx の登記簿としての意味を明らかにし、その後、ソースからの Harness のインストール、scratch-plugin 実験ディレクトリの構築、漏れのない最小プラグインの作成を案内し、最後に読み込み経路の二点セット——patch オーバーレイ層 cordis.yml と --patch 起動パラメータ——を完全につなげます。読み終えた後には、コピー&ペーストの段階にとどまらず、Web UI に実際に読み込まれて動作するローカルプラグインを自力で書けるようになっているはずです。
apply 関数シグネチャの解析:@deepseek-ai/cordis から Context 型をインポートする
Harness の世界観では、プラグインは基底クラスを継承し、インターフェースを実装し、グローバルシングルトンに登録する必要があるような巨大な存在ではありません。それはただの普通の ES モジュールファイルです。フレームワークがあるモジュールをプラグインかどうか判断するときは、二つのことだけを見ます:apply をエクスポートしているか、そして apply が約束どおりコンテキストパラメータを受け取るかどうかです。この設計の利点は、プラグインが本質的に tree-shakable で、テストしやすく、オンデマンドで読み込みやすいことです。同時に、「プラグインの初期化」という事柄を、関数が実行される一瞬に圧縮しています。
まず apply のシグネチャを見てみましょう。公式に示されている書き方は、プラグインモジュールが @deepseek-ai/cordis から Context 型をインポートし、次に ctx: Context を受け取る apply 関数をエクスポートするというものです。ここで型の出所に注意してください——deepseek-harness のメインパッケージからインポートするのではなく、cordis という基盤コンテナライブラリからインポートします。この点は非常に重要です。なぜなら Harness のプラグインシステムは、依存性注入とライフサイクルコンテナである Cordis の上に構築されており、Context は Cordis の中核抽象であり、Harness はその能力を「Agent ランタイム」のセマンティクスへと拡張しているにすぎないからです。
なぜ「@deepseek-ai/cordis から Context 型をインポートする」ことを強調するのでしょうか。TypeScript の型インポートはコンパイル後に消え、実行時に依存の負担を一切増やしませんが、開発者にもたらす利益は極めて大きいからです:ctx で利用可能なメソッド、プロパティ、ジェネリック制約はすべてエディタで自動補完され、メソッド名を間違えたり引数の型を間違えたりすれば、IDE が即座に赤くマークします。型インポートを正しく行うことは、ctx API 全体への鍵を手に入れることに等しいのです。その後、イベントリスナーの登録、ツールの登録、LLM アダプターの登録を行うとき、ドキュメントを何度もめくるのではなく、型ヒントに頼ることができます。
また、apply の戻り値にも注意する必要があります。最小プラグインでは、apply は通常何も返さず、副作用を実行するだけの同期関数です。フレームワークがそれを呼び出し、登録が完了すると、ライフサイクルはそのまま先へ進みます。この「一度きりの初期化」というセマンティクスが、apply に長時間かかるブロッキング処理を置くのに適さないことを決めています。リモート設定を取得したり接続を確立したりする必要がある場合は、apply 内で同期的に待つのではなく、非同期登録や後続のフックで処理することを検討すべきです。素材では、フレームワークがプラグインを読み込むときに apply を呼び出すことが明確に述べられていますが、apply が async であることは要求されていません。したがって、初学段階では同期を保ち、非同期処理は登録後のコールバックに任せるのが最も安全な方法です。
さらに、よくある初心者の誤解を一つ注意しておきます。多くの人は当然のように apply に第2、第3の引数(たとえば「config」や「options」)を追加し、フレームワークが設定を注入してくれることを期待します。現在の素材の説明によれば、apply の引数は ctx: Context だけです。プラグインが必要とする依存関係は apply の実行前にすでに整っており、つまりコンテキストそのものが依存関係と設定の統一的な入口です。関数シグネチャを拡張する必要はありませんし、すべきでもありません。設定を読み取りたい場合は、独自の引数を発明するのではなく、ctx が提供する仕組みを使うべきです。
name エクスポートフィールドの二重の役割:ログ識別子と設定参照キー
最小プラグインでは、apply のほかにもう一つ必ずエクスポートする必要があるのが name 定数です。素材はそれを非常に控えめに定義しています。name はプラグイン名であり、ログと設定の中でこのプラグインを識別するために使われます。この短い一文は、実は name の二重の役割を指摘しており、これを理解すると、後々の「読み込まれたのに有効にならない」という調査地獄を避けられます。
第一の役割はログ識別子です。Harness は Agent ランタイムを中心に構築されたフレームワークであり、ランタイムは大量のイベント、呼び出し、登録、エラーメッセージを生成します。フレームワークがログを1行出力しようと決めたとき、「このログは誰が言ったのか」を知る必要があります。あなたのプラグインが name = 'hello-plugin' をエクスポートしていれば、このプラグインに関連するすべての出力に [hello-plugin] のようなプレフィックスを付けられ、ターミナルで何千行もの中から一目でそのログを見つけられます。逆に name をエクスポートしていなければ、ログは追跡不能な匿名出力に退化し、複数のプラグインが混在するとデバッグはほぼ不可能になります。したがって、プラグインに意味が明確でグローバルに衝突しない name を付けることは、任意ではなくエンジニアリング上の規範です。
第二の役割は設定参照キーです。プラグインシステムは通常、設定ファイル内でプラグインをパラメータ化したり、有効/無効を切り替えたり、設定を上書きしたりできます。フレームワークが設定内で「どのプラグインにパラメータを渡したいのか」を特定するために頼るのが name です。つまり、name はプラグインを書くときに外部へ公開する「身分証番号」であり、設定側はこの名前を使ってあなたを見つけます。name は一度公開したら気軽に変更しないことが推奨されます。そうでなければ、それを参照する設定がすべて一致しなくなり、プラグインは静かに有効にならないか、わけのわからないエラーを出します。
ここには踏みやすい落とし穴が一つあります。多くの人はファイル名 my-plugin.ts とプラグイン名 hello-plugin を混同し、両者は一致していなければならないと考えてしまいます。実際にはこれらは二つの別物です——ファイル名はファイルシステム層のパスであり、読み込み時には絶対パスが使われます。name はフレームワーク層の論理識別子であり、ログと設定に用いられます。ファイルが my-plugin.ts で、プラグインが hello-plugin であっても完全に合法であり、素材内の例はまさにそのように書かれています。しかし逆に、ファイル名と name を意味的に近づけておくこと(たとえばファイル名 my-plugin.ts、name を hello-plugin とするような同族命名)は、チーム協業時の認知負荷を大幅に下げることができます。
name と読み込みパスの責務をはっきり分けるために、以下の表と照らし合わせるとよいでしょう:
| 比較項目 | name エクスポートフィールド | プラグインファイルパス |
|---|---|---|
| 所属する層 | フレームワーク論理層 | ファイルシステム層 |
| 典型的な値 | hello-plugin のような短い識別子 | .ts で終わる絶対パス |
| 主な用途 | ログのプレフィックス、設定参照キー | フレームワークにどこからコードを読み込むかを伝える |
| 一意である必要があるか | ログと設定の曖昧さを避けるため、グローバルに一意であることを推奨 | 各プラグインファイルパスは自然に一意 |
| 改名の影響 | ログの可読性と設定参照に影響するため慎重に | 読み込み設定内のその一行のパスにのみ影響 |
| 出現位置 | プラグインソースコード内の定数エクスポート | cordis.yml の name フィールド |
この表を見たうえで、素材にある「name はプラグイン名であり、ログと設定の中でこのプラグインを識別するために用いられる」という一文に戻ると、それがランタイムの可観測性と設定のアドレス可能性という二つの次元を同時にカバーしていることがわかります。責任あるプラグイン作者は、命名時に「ログで見栄えがよいこと」と「設定で参照しやすいこと」の両方を考慮します。
ctx コンテキストオブジェクト: 能力登録の入口とリソース登録簿
もし apply がプラグインの「門」であり、name がプラグインの「身分証」であるなら、ctx はプラグインとフレームワークの間のすべてのインターフェースです。素材は ctx の意味について非常に凝縮された定義を与えています。ctx(Context)はフレームワークが各プラグインに渡すコンテキストオブジェクトであり、能力を登録する入口であると同時に、プラグインが登録したすべてのリソースを記録するものです。この二つの半文は、分けてゆっくり説明する価値があります。
前半は「能力を登録する入口」です。Harness では、プラグインがフレームワークに影響を与えたい場合、唯一の正規の方法は ctx を通じて登録することです。素材は三つの典型的な登録対象を明確に挙げています:イベントリスナー、ツール(tools)、LLM アダプターです。これら三つは Agent ランタイムの三つの中核的な拡張ポイントに対応します——リスニングとはランタイムイベントを観察し応答できることを意味し、ツールとは Agent に呼び出し可能な能力を追加できることを意味し、LLM アダプターとはモデル呼び出しチェーンに接続したり改変したりできることを意味します。それらはすべて ctx にぶら下がっており、さまざまなグローバル変数や静的クラスに散在しているわけではありません。この「すべてはコンテキストを経由する」という設計により、プラグインの副作用はフレームワークによって統一的に管理・回収できます。
後半は「リソース登記簿」です。ctx はただ register メソッドをいくつか渡して終わり、というものではありません。それだけでなく、プラグインが登録したすべてのリソースを記録します。これには二つの直接的な利点があります。第一に、フレームワークが各プラグインがどのような能力を宣言したかを把握できるため、依存関係の並べ替え、競合検出、診断が容易になります。第二に、プラグインをアンロードしたりホットリロードしたりする必要があるとき、フレームワークはそのプラグインが登録したものを根拠をもってクリーンアップできます。プラグイン自身が大量の登録解除ロジックを手書きする必要はありません(もっとも、コンテキストは通常、対応する破棄コールバック機構も提供します)。ctx を「ツールボックス」ではなく「登記簿」として理解すると、コードを書くときに、フレームワークを迂回して自分でグローバルフックをぶら下げるのではなく、正規の登録チャネルを通すようにより自覚的になります。後者はリソースを登記から外してしまい、アンロード時に幽霊になってしまいます。
エンジニアリング実践の観点から見ると、ctx をめぐっていくつか覚えておく価値のある原則があります:
- 能力は ctx からのみ得る:リスナーが必要でも、ツールが必要でも、アダプターが必要でも、すべて ctx を通じて登録します。自分でグローバルシングルトンを作ってフレームワークと管理権を奪い合わないでください。
- 登録はすなわち宣言:すべての登録はフレームワークへの宣言です。登録が明確であるほど、その後のデバッグとアンロードが楽になります。
- 必要に応じて登録する:apply の中で使わない能力を無闇に大量登録しないでください。登記簿がきれいであるほど、実行時は軽くなります。
- 型を活用する:Context 型は @deepseek-ai/cordis に由来するため、登録 API の型ヒントが誤用を事前に発見する助けになります。
他のプラグイン体系から移行してきた多くの開発者は、「プラグイン」を「ライフサイクルを持つクラス」と同一視することに慣れており、onInit や onDestroy のようなフックをあちこち探し求めます。しかし Harness のモデルでは、プラグインのライフサイクルは高度に集中しています。apply が呼ばれたときに登録が完了し、リソースは ctx に記録され、その後の起動・停止と回収はフレームワークが登記簿に基づいて統一的に調整します。これを理解すれば、プラグインを書くときの心構えは「自分のライフサイクルを管理する」から「フレームワークに自分の意図を宣言する」へと変わり、コードは明らかに短く、より安定します。
ソースからのインストール四連撃:git clone、pnpm install、pnpm run build、pnpm dsh web
実際に動かすには、まず Harness をソースからインストールします。素材で示されているのは四段階のコマンドフローで、順序は入れ替えられず、各段階には存在する理由があります。以下にそのまま貼り付けて実行できる完全なコマンドを示します:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
この連鎖を段階ごとに分解します:
- git clone:公式リポジトリ deepseek-ai/deepseek-harness から完全なソースを取得します。「ソースからのインストール」を強調するのは、プラグイン開発ではしばしばリポジトリ内のビルド成果物、型宣言、設定の取り決めと整合させる必要があり、プリコンパイル済みパッケージを直接使うとそうした文脈が欠けてしまうからです。
- cd deepseek-harness:リポジトリのルートディレクトリに入ります。以降のすべての相対パス(後で作成する scratch-plugin を含む)はこのルートディレクトリを基準とするので、自分が正しい位置に立っているか必ず確認してください。
pwdで自己確認できます。 - pnpm install:依存関係をインストールします。ここでは npm や yarn ではなく pnpm を使います。必ず pnpm を使用してください。リポジトリの依存トポロジーや workspace 構造は pnpm を前提に組織されており、他のパッケージマネージャを使うと依存解決の不一致が起きやすいからです。
- pnpm run build:ビルドを実行します。ソースリポジトリは通常複数のパッケージを含み、実行時に読み込める成果物をまずビルドする必要があります。この段階を飛ばして直接起動すると、読み込み段階でモジュールが見つからないエラーになることがよくあります。
- pnpm dsh web:Web UI を起動します。dsh は Harness のコマンドラインエントリで、web サブコマンドでインターフェースを立ち上げれば、ブラウザでプラグイン読み込み後の効果を観察できます。
ここでは、エンジニアリング上の落とし穴と解決策をまとめて列挙します:
- 落とし穴:clone が非常に遅い、または失敗する。 解決策:ネットワークとプロキシ設定を確認し、github.com の deepseek-ai/deepseek-harness リポジトリにアクセスできることを確認します。
- 落とし穴:install で依存関係の競合が報告される。 解決策:pnpm を使用していることを確認し、既存の lock ファイルと node_modules を削除してから再インストールします。
- 落とし穴:build で型エラーが報告される。 解決策:Node のバージョンがリポジトリの要件を満たしていることを確認し、必要に応じて再インストールしてから再度 build します。
- 落とし穴:dsh web が起動しない、またはポートが占有されている。 解決策:起動ログのポート情報を確認し、ポートを解放するか、ログの指示に従って調整します。
- 落とし穴:プラグインのコードを変更したのに UI が変わらない。 解決策:プラグインの読み込み設定が正しいことを確認し(後述の cordis.yml と --patch を参照)、ファイルパスが絶対パスでなければならない点に注意します。
このコマンドの流れを覚えるだけでなく、その順序の意味を理解することがより重要です:clone でコードを取得し、install で依存関係を補い、build で成果物を生成し、web でランタイムを起動する。前者は後者の前提です。build を飛ばして直接 web を実行するのは、初心者に最もよくある「コマンドを一つ省いたばかりに、エラー調査に半日かかる」という事故現場です。
依存関係の準備タイミング:なぜ apply 実行前に必要な依存関係がすべて揃っているのか
資料の中に、一見目立たないが非常に重みのある一文があります——「必要な依存関係は apply 実行前にすでに整っている」。この一文は、プラグイン作者を長く悩ませてきた問題を説明しています:apply の中で他のプラグインが提供する機能を直接使っても安全なのか?答えは、フレームワークの取り決めに従えば安全です。
背後にある仕組みは次のように理解できます:Cordis のようなコンテナはプラグインを読み込む際、まず依存関係の解決と前提プラグインの初期化を完了し、現在のプラグインが宣言した必要な外部機能がすでに利用可能であることを確認してから、現在のプラグインの apply を呼び出します。言い換えれば、apply が呼び出されるその瞬間自体が、フレームワークからの「あなたの前提条件は満たされた」というシグナルなのです。プラグイン内で自分でポーリング待機を書いたり、読み込みをリトライしたり、setTimeout で登録を遅延させて他のプラグインが先に初期化されることに賭けたりする必要はありません。
このタイミング保証は、いくつかの非常に実用的な利点をもたらします:
- プラグインのコードがよりシンプルになる:apply 内に「X が準備できるまで待ってから Y を登録する」といった補償ロジックを書く必要がありません。
- 競合を回避できる:読み込み順序が不確定なことによる偶発的な失敗が起こりません。この種のバグは、ローカルでは再現できないのに本番では頻繁に発生しがちです。
- 宣言的な依存関係:フレームワークの要求に従って依存関係を宣言するだけで、読み込み順序はコンテナが統一的に編成します。チームで協力する際に「あのプラグインを先に起動するのを忘れないで」と口伝えする必要はありません。
ただし、この一文の境界も正しく理解する必要があります。ここで言う「必要な依存関係」が apply 前に整っているとは、プラグインの依存関係宣言に基づいて解決された依存関係を指します。「宇宙の万物がすべて準備完了」という意味ではありません——例えば、外部ネットワークの可用性や、あるランタイムイベントの初回発火などは、依然として apply の後にコールバックを登録して対応する必要があります。「依存関係解決の準備完了」と「ランタイムの準備完了」を区別することは、上級プラグイン作者の必修科目です:前者は読み込み段階の静的な保証であり、後者は実行段階の動的な事実です。
scratch-plugin のディレクトリ設計:mkdir -p で src ソースディレクトリを作成
インストール完了後、リポジトリのソースコードには手を付けず、プラグインを入れるための実験用プロジェクトを別途作成します。これを行う価値は、「プラグインの学習」と「フレームワークの改変」を完全に分離することにあります。自由に実験でき、リポジトリを汚染せず、問題が起きたときも自分のプラグインのコードなのかフレームワーク自体の挙動なのかを特定しやすくなります。素材ではまず src ソースディレクトリを作成することが求められており、コマンドは以下の通りです。リポジトリのルートディレクトリで実行してください:
mkdir -p scratch-plugin/src
mkdir -p の -p オプションは「親ディレクトリが存在しない場合はまとめて作成し、既に存在する場合はエラーを出さない」ことを保証するので、1 回の呼び出しで scratch-plugin とその下の src の両方が作成され、スクリプトやドキュメントにそのまま貼り付けるのに適しています。実行後はついでにディレクトリ構造を確認できます:
ls -R scratch-plugin
この時点では src の 1 階層だけが見えるはずです。プラグインを書き終えて cordis.yml を追加した後、scratch-plugin の完全な構造は以下のようになります(これは実験プロジェクト全体の最終形態で、本節の後半で両方を埋めていきます):
scratch-plugin/
├── src/
│ └── my-plugin.ts # プラグインのソースコード(hello-plugin)
└── cordis.yml # patch オーバーレイ:どのプラグインを挿入するかをフレームワークに伝える
ディレクトリ設計について、いくつかのエンジニアリング上の提案があります:
- ディレクトリ名は scratch の意味を保つ:scratch は「下書き、実験」を意味し、後から見る人にここのコードが正式なモジュールではないことを思い出させ、本番コードとして誤って依存されるのを防ぎます。
- ソースコードは src に統一する:単一ファイルのプラグインであっても、まず src を作ってからファイルを置くことをお勧めします。プラグインが大きくなったときに設定構造を変えずにサブディレクトリを追加できます。
- cordis.yml は src と同じ階層に置く:オーバーレイは「src 内のプラグインをどのように読み込むか」を記述するもので、プロジェクトのルート階層に置くのが最も直感的です。
- 相対パスのコマンドは常にリポジトリのルートディレクトリで実行する:後で cordis.yml 内の「絶対パス」を
pwdで組み立てる必要があるため、正しいディレクトリに立っていることでパスのミスを減らせます。
最小限で動作するプラグイン my-plugin.ts:漏れのない完全な設定
ディレクトリができたので、次はプラグイン本体を書きます。src ディレクトリに入ります:
cd scratch-plugin/src
そして scratch-plugin/src/my-plugin.ts に以下の内容を書き込みます。素材ではこれが「完全に使用可能なプラグイン設定で、何一つ欠けていない」ことが強調されています。私もそのまま貼り付けられるバージョンに整理し、重要なコメントを保持しました:
// ファイルパス:scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
// name はプラグイン名で、ログや設定でこのプラグインを識別するために使われる
export const name = 'hello-plugin'
// apply はプラグインのエントリポイント:フレームワークがプラグインを読み込むときに呼び出す
export function apply(ctx: Context) {
// 必要な依存関係は apply の実行前にすでに準備されている(第 9 回参照)
console.log('[hello-plugin] plugin loaded!')
}このファイルはわずか数行しかないほど短いですが、前のいくつかのセクションで説明したすべての概念を実践に落とし込んでおり、一行ずつ読む価値があります:
- import type:型のみをインポートし、コンパイル後にランタイム依存を生じません。これは TypeScript の標準的なやり方であり、素材で指定されたインポート方法でもあります。
- export const name:プラグインの身元をフレームワークに公開します。ログと設定はこれによって識別されます。
- export function apply:プラグインのエントリポイントで、ctx を受け取り、ロード時に呼び出されます。
- console.log:最小限で検証可能な副作用です。その価値は目に見えるロードの証拠を提供することにあります——ログさえ出れば、フレームワークが確かにこのプラグインをロードして実行したことを意味します。
なぜ「漏れゼロ」と言えるのか?なぜなら、ロード可能な最小のプラグインに必要なエクスポートはちょうど二つだけだからです:name と apply。型インポートはエディタ体験を保証し、コメントは保守性を保証し、console.log は可観測性を保証します。それ以外に必須のものは何もありません。多くの初心者は最初に試すとき、いろいろなものを追加したがります——偽のツールを登録したり、空のリスナーを登録したり——その結果、かえって「プラグインが実際にロードされたのかどうか」という基本的な問題を覆い隠してしまいます。最小プラグインの正しい姿勢は、まずそれを「見えるようにする」こと、そして徐々に能力を追加していくことです。
ここで、後できっと踏むことになる落とし穴を先に言っておきます:このプラグインの apply は、フレームワークにロードされたときにのみ実行されます。ファイルを書き終えても、ロードのエントリポイントを設定していなければ、Web UI を実行してもコンソールに [hello-plugin] plugin loaded! の行は出ません。したがって、次のステップではロードの経路を解決しなければならず、それが cordis.yml と --patch につながります。
プラグインのロード経路:patch オーバーレイ cordis.yml と --patch 起動パラメータ
Harness でローカルプラグインをロードするには「二点セット」が必要です:一つは patch オーバーレイ cordis.yml、もう一つは --patch 起動パラメータです。まず概念から:オーバーレイ(overlay)とはパッチ的な設定の層であり、元の設定を覆すのではなく、「元の設定を基に何を挿入するか」を宣言するものです。私たちの目標はシンプルです——scratch-plugin/src/my-plugin.ts をランタイムのプラグインリストに挿入すること。
まずリポジトリのルートディレクトリで pwd を実行して絶対パスを取得します。これは重要な前提作業です:
pwd
そしてコマンドが出力したパスを書き留め、scratch-plugin/cordis.yml を作成します。内容は以下のとおりです(/absolute/path/to/ を pwd で取得した実際のパスに置き換えてください):
# ファイルパス:scratch-plugin/cordis.yml
# これは Web オーバーレイ(overlay)であり、ローカルプラグインを挿入する役割だけを担う
- insert:
- id: hello
# name はプラグインファイルのパスであり、絶対パスでなければならない!
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'この YAML の各フィールドには明確な責務があります。
- insert:オーバーレイのアクション種別で、「挿入」を意味します。既存の設定を書き換えず、新しいエントリを追加するだけなので、安全で重ねて適用できます。
- id: hello:挿入されるエントリの識別子で、このオーバーレイ層の内部でこの項目を特定し管理するために使います。
- name: 絶対パス:ここが最も混同しやすい点です。cordis.yml の文脈では、name はプラグインファイルのパスを指し、プラグインのソースコードでエクスポートされる hello-plugin という文字列ではありません。しかも、絶対パスでなければならないと、素材では感嘆符付きで特に強調されています。
なぜ絶対パスでなければならないのでしょうか。フレームワークがこの設定を解析するとき、その作業ディレクトリはあなたが想定しているディレクトリとは限らず、相対パスは「どこから起動したか」に依存してずれてしまいます。絶対パスを使うことで不確実性を排除できますが、その代償として、マシンやディレクトリを変えるとこの行も合わせて変更する必要があります。したがって、これは scratch のようなローカル実験の場面に適しており、正式に配布するときはより安定したパス戦略に置き換えるべきです。
この二点セットの責務と間違えやすい点を明確にするために、次の表をご覧ください。
| 比較項目 | cordis.yml オーバーレイ | --patch 起動パラメータ |
|---|---|---|
| 役割 | 「何を挿入するか」を記述する設定の担い手 | フレームワークに「どのオーバーレイを有効にするか」を伝える |
| 必須フィールド | insert、id、name(絶対パス) | cordis.yml を指すパス |
| よくある間違い | name を相対パスで書いてしまう | --patch を付け忘れてオーバーレイが有効にならない |
| 変更の影響 | ファイルを変えると挿入内容が変わる | 起動コマンドを変えると有効範囲が変わる |
| 適用場面 | ローカルでのプラグイン実験、一時的な注入 | 起動のたびにオーバーレイを明示的に宣言する |
この二点セットをまとめて見ると、cordis.yml は「書き記す」パッチであり、--patch は「使う」ためのスイッチです。cordis.yml だけを書いて --patch を付けなければ、オーバーレイは実行時設定に参加しません。--patch だけを付けても使える cordis.yml がなければ、当てるパッチがありません。どちらかが欠けるとプラグインは読み込まれず、コンソールにあの [hello-plugin] plugin loaded! の行も表示されません。
ここまでで、第1部の核心的な流れの半分をたどってきました。apply のシグネチャと意味、name の二重の身分、ctx の登録簿モデル、ソースからのインストールコマンドの流れ、依存関係が整うタイミング、scratch-plugin ディレクトリ、最小プラグインの実装、そして cordis.yml + --patch の読み込み機構です。あなたの手元には、「なぜこう書くのか」を説明できる完全な理解と、そのまま貼り付けて実行できるプラグインと設定がすでにあります。次のステップ、つまり本記事の 2/2 部分では、--patch の具体的な起動コマンドを補完し、プラグインが本当に読み込まれることを検証し、その上でさらに ctx に最初の実能力——イベント監視とツール——を登録していきます。これにより、あなたの hello-plugin は「挨拶するだけの空殻」から「本当に Agent ランタイムに参加する」プラグインへと進化します。
前回のセクションでは、すでに DeepSeek Harness のソースコードを取得し、依存関係をインストールし、ビルドを通し、最初の最小プラグイン hello-plugin を書きました。これはただ一つのことだけを行います——apply という名前の関数をエクスポートし、フレームワークから渡される ctx(コンテキストオブジェクト) を受け取り、apply の実行時にログを一行出力します。書き終えるとすぐに疑問が湧きました。この .ts ファイルはディスク上に置かれているだけなのに、フレームワークはどうやってその存在を知るのでしょうか。その答えが本記事の核心です——cordis.yml という patch オーバーレイを使ってローカルプラグインをランタイムに「差し込み」、さらに --patch 起動パラメータでそれを有効にします。以下ではディレクトリ構造から始めて、読み込み検証と失敗時のトラブルシューティングまで一通り見ていきます。
scratch-plugin の最終ディレクトリツリー:src と cordis.yml が併存する構造
前回は scratch-plugin/src という階層だけを作成しました。厳密に言えば、それはまだ「フレームワークに認識されるプラグインプロジェクト」ではなく、ソースコードを置いただけの普通のフォルダでした。それをフレームワークが読み込める単位にするには、cordis.yml を追加する必要があり、この yml は src ディレクトリと同じ親ディレクトリ、つまり両方とも scratch-plugin/ の下に置かなければなりません。
最終的なプロジェクト構造は次のとおりです:
scratch-plugin/
├── src/
│ └── my-plugin.ts # プラグインのソースコード(前回書いた hello-plugin)
└── cordis.yml # patch オーバーレイ:どのプラグインを挿入するかをフレームワークに伝える
この構造はシンプルすぎて少し「貧相」に見えますが、その二つの部分は責務が完全に分離されており、この点を理解することはディレクトリそのものを覚えるよりもはるかに重要です:
- src/my-plugin.ts は能力そのものです。これは
name(プラグイン名。ログや設定でこのプラグインを識別するために使う)とapply(プラグインのエントリポイント。フレームワークがプラグインを読み込むときに呼び出す)をエクスポートします。これは純粋な TypeScript モジュールであり、自分が誰に読み込まれ、どのようなパラメータで読み込まれるかには関心がありません。 - cordis.yml は配線図です。ビジネスロジックは一切含まず、「どのファイルを現在の設定に挿入するか」を宣言するだけです。これはフレームワークの既存設定の上に重ねられる overlay(オーバーレイ) 層です。
なぜ両方を同じ親ディレクトリ scratch-plugin/ の下に置くのでしょうか。実際のエンジニアリングでは複数の実験プラグインを同時に保守するため、各プラグインプロジェクトは自己完結したディレクトリであるべきです。ソースコードも中に、配線図も中にあり、丸ごと削除でき、丸ごとコピーでき、丸ごと別のマシンへ移動できます。もし cordis.yml をリポジトリのルートに置いたり、src の中に置いたりすると、次の二つの典型的な混乱が生じます:
- リポジトリのルートに置く——実験プラグインを書くたびにメイン設定を繰り返し上書きすることになり、メインリポジトリの設定を乱しやすく、ロールバックのコストが高くなります;
- src の中に置く——ディレクトリの意味が曖昧になり、このプロジェクトを読んだ他人が「どの階層がディレクトリの約束で、どの階層がソースコードなのか」を一目で判断できなくなります。
素材に示されたディレクトリツリーは、まさに src/ と cordis.yml を兄弟ノードとして並べています。これは適当に描かれたものではなく、このワークフローの約束事です:一つのプラグインサンドボックス = 一つのソースディレクトリ + 一つの patch オーバーレイ。これは多層にネストした monorepo 構造ではなく、意図的に最も浅い一層に圧縮されている点に注意してください。新しく参加した人でもディレクトリを開けば全体像をすぐに理解できます。
もう一つ、個別に触れておく価値のある細かい点があります。ファイル名 my-plugin.ts と、プラグイン内部の name = 'hello-plugin' は一致している必要はありません。ファイル名はディスク上の位置を示す座標にすぎず、name こそがフレームワーク内部での身分標識です。素材の例では両者が意図的に一致していません(ファイルは my-plugin.ts、プラグイン名は hello-plugin)。これはまさに、この二つの名前空間が分離されていることを示しています——ただし実際のプロジェクトでは、両者を対応関係に保つことを強くおすすめします。というのも、問題を調査するときには「ファイルパス」と「ログ内のプラグイン名」の間を行き来する必要があり、命名が混乱していると、それだけで余計な認知的負担がたくさん生まれるからです。
insert ディレクティブの書き方:cordis.yml オーバーレイはローカルプラグインの挿入だけを担当する
ディレクトリを把握したら、次は中身を書きます。scratch-plugin/cordis.yml の完全な内容は以下のとおりです:
# ファイルパス:scratch-plugin/cordis.yml
# これは Web オーバーレイ(overlay)であり、ローカルプラグインの挿入だけを担当する
- insert:
- id: hello
# name はプラグインファイルのパスであり、絶対パスでなければならない!
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
わずか数行ですが、どの部分にも意味があります。順を追って分解して見ていきましょう:
- 最上位は配列です(YAML では
-で始まる)。これは Cordis の patch 機構が、複数のディレクティブを一度に宣言し、順番に重ねて実行できることを意味します。私たちの例では一つだけ、つまりプラグインの挿入です。 - ディレクティブオブジェクトのキーは insert だけです。これは素材におけるその位置づけを表しています。このオーバーレイは「ローカルプラグインの挿入だけを担当する」のです。既存プラグインのパラメータを変更せず、他所の設定を上書きせず、条件分岐も行わず、ただ一つのことだけを行います——ローカルファイルをランタイムに挿入することです。
- insert の値自体もまた配列です。つまり、複数のプラグインを一度にバッチ挿入でき、各エントリは
idとnameを持つオブジェクトです。
特に強調したいのは「単一責任」という点です。これは初心者が最も陥りやすい見えない落とし穴だからです。多くの人は設定を書くとき、同じ yml に「ついでに」何か別のものも加えたくなります——たとえばポートを変えたり、ログレベルを調整したり。一度きりの実験では効率的に見えますが、二つの結果を招きます。一つは、このオーバーレイがもはやきれいに削除できなくなること(触るべきでないものが混ざり込む)。もう一つは、読み込みに失敗したとき、問題が「プラグイン挿入」の段階にあるのか「その他の変更」の段階にあるのかを素早く判断できなくなることです。patch ファイルを一つのトランザクションとして扱いましょう:一度に一つのことだけを行い、終わったら検証し、検証に通ってから責任を広げるかどうかを考えるのです。
もう一つ見落としやすいのが YAML のインデントです。上記のファイルでは、insert の下にある二つのハイフンで始まるエントリはインデントしなければならず、id と name はさらに - に対して一段深くインデントしなければなりません。YAML はインデントに敏感で、スペースの代わりに Tab を使ったり、インデントの階層が不揃いだったりすると、直接パース失敗を引き起こします——しかもエラーメッセージは多くの場合「パースエラー」としか言わず、何行目のインデントが問題なのかを教えてくれません。スペース二つで統一し、エディタで「Tab をスペースに変換」以外の自動整形プラグインを有効にしてこのファイルを並べ替えないことをおすすめします。
次の表は「オーバーレイ層で何ができるか、私たちは何をすることを選ぶか」を明確にし、境界感覚を築くのに役立ちます:
| 観点 | Web オーバーレイ層としての cordis.yml | プラグインソース itself(my-plugin.ts) |
|---|---|---|
| 中心的な責務 | 宣言的な配線:どのファイルを現在の設定に挿入するか | 命令的なロジック:apply をエクスポートし、機能を登録する |
| 言語と形態 | YAML 設定、純粋なデータ | TypeScript モジュール、実行可能なコード |
| 主要フィールド | insert、id、name | name、apply(ctx) |
| 変更後の反映方法 | --patch 起動パラメータと組み合わせて再読み込みが必要 | プラグインを再読み込みした後、フレームワークが apply を呼び出す |
| 業務ロジックを含むか | いいえ、挿入のみを行う | はい、ctx を通じてイベント、ツール、LLM アダプタなどを登録する |
| 削除時のリスク | 低、全体を削除すればロールバックできる | 中、それが登録した機能に他のプラグインが依存していないことを確認する必要がある |
表の右側の列は、本記事では今のところ展開しない部分です——apply の中で ctx を呼び出してイベントリスナー、ツール、LLM アダプタを登録すること。それはプラグインが本当に価値を生み始める段階です。しかし順序を忘れないでください:まず読み込めること、それから機能を語る。大量の機能を登録しても読み込めないプラグインは、ゼロに等しいのです。
id と name フィールドの辨析:プラグインパスは絶対パスでなければならない
insert エントリには二つのフィールド、id と name があります。どちらも素朴な名前ですが、意味は大きく異なり、混同すると直接障害を引き起こします。
id はこの挿入命令の識別子です。例では hello です。これは設定体系の内部でこのエントリを参照するために使われます——たとえばログでどの insert が有効になったかを区別したり、将来他の命令がそれを指し示す必要がある場合などです。それは「名前」であり、論理的な座標であり、何を書くかはあなたが決めてよく、現在のファイル内で一意であれば十分です。素材の例では hello を使っており、プラグインファイル内の name = 'hello-plugin' と形が似ているだけで、同じものではありません。
name はプラグインファイルのパスであり、しかも絶対パスでなければなりません。これは本記事で最も厳格で、最も間違えやすい制約です。素材では感嘆符付きのコメントでそれを強調しています:# name はプラグインファイルのパスであり、絶対パスでなければなりません!。フレームワークはこれに基づいてディスク上のモジュールを特定し、読み込みます。相対パスは「何に対して相対なのか」という問題のために不確定になります——あなたの現在の作業ディレクトリはリポジトリのルートですか?それとも Web UI が起動された場所ですか?あるいは何かのビルド成果物ディレクトリですか?不確定である限り、読み込みはランダムに成功したり失敗したりします。絶対パスはこの不確定性を消し去ります。
この2つのフィールドの違いを対照表にすると次のようになります:
| フィールド | 意味 | 値の例 | ファイル解決に影響するか | よくある間違い |
|---|---|---|---|---|
| id | insert エントリの論理識別子で、設定内部からの参照やログでの区別に使われる | hello | いいえ | 他のエントリと名前が重複し、参照が曖昧になる |
| name | プラグインファイルのパスで、フレームワークはこれに基づいてモジュールを読み込む | /absolute/path/.../src/my-plugin.ts | はい | 相対パスで書いてしまい、読み込みと解決に失敗する |
さらに派生する問題があります。このパスが指しているのは .ts ソースファイルであり、コンパイル成果物ではありません。素材の例では name が明確に scratch-plugin/src/my-plugin.ts と書かれているので、このワークフローはフレームワーク(または開発環境)が TypeScript を扱えることを前提としていることが分かります。つまり、以前の pnpm install と pnpm run build の手順は、Web UI を動かすためだけでなく、この「TS ソースファイルを直接読み込む」開発体験のための土台でもあったのです。ビルド手順を飛ばしてしまうと、たとえパスを正しく書いても、読み込み段階でモジュール解決の問題に遭遇する可能性があります。
パスにおけるもう一つの見えない落とし穴は空白と特殊文字です。素材では '/absolute/path/to/...' のように引用符付きで書かれており、この引用符は必須です。リポジトリのパスに空白やエスケープが必要な文字が含まれている場合、引用符を使わないと YAML パーサーがパスを分割してしまいます。パスに空白が含まれているかどうかに関わらず、常にシングルクォートで囲むことをおすすめします。同時に、パスに中国語の文字を混ぜないよう注意してください。一部のツールチェーンは非 ASCII パスを処理する際に問題を起こすことがあり、これはまさに中国語話者の開発者が最も見落としやすい点です(たとえば、リポジトリを「マイドキュメント」ディレクトリの下に置くなど)。
pwd で絶対パスを取得する:相対パスによるプラグイン読み込み失敗を避ける
name が絶対パスでなければならない以上、この絶対パスはどこから取得するのが最も確実でしょうか。答えは最も素朴な方法です:リポジトリのルートディレクトリで pwd を実行し、その出力をそのまま name に連結する。素材で示されている操作順序は、まず「リポジトリのルートディレクトリで pwd を実行し、絶対パスを取得する」、次に「scratch-plugin/cordis.yml を作成する」そして記入する、というものです。
なぜ「リポジトリのルートディレクトリで」と強調するのでしょうか。なぜなら、連結する必要があるのは <リポジトリルート>/scratch-plugin/src/my-plugin.ts であり、pwd が出力するのは現在のディレクトリの絶対パスだからです。リポジトリのルートディレクトリに立っているときだけ、このプレフィックスが正しくなります。他のサブディレクトリで pwd を実行すると、連結されたパスは1階層多くなったり少なくなったりします。
完全な手動の流れは次のように書けるので、コピーして実行するのに便利です:
# 1. リポジトリのルートディレクトリに入る(自分の clone 場所に合わせて調整)
cd /absolute/path/to/deepseek-harness
# 2. リポジトリルートの絶対パスを取得する
pwd
# 出力例:/absolute/path/to/deepseek-harness
# 3. プラグインディレクトリとソースディレクトリを作成する(前の記事で済んでいればスキップ)
mkdir -p scratch-plugin/src
# 4. プラグインのソースファイルが本当に想定の場所にあるか確認する
ls -l scratch-plugin/src/my-plugin.ts
# 5. オーバーレイ設定を書き込み、pwd の出力と相対部分を絶対パスに連結する
cat > scratch-plugin/cordis.yml <<'YAML'
# ファイルパス:scratch-plugin/cordis.yml
# これは Web オーバーレイ(overlay)で、ローカルプラグインを挿入するだけの役割を持つ
- insert:
- id: hello
# name はプラグインファイルのパスで、絶対パスでなければならない!
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
YAML
# 6. 連結したパスが実際に存在するか検証する(重要なステップで、多くの初歩的なミスを事前に防げる)
test -f /absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts && echo "PATH OK" || echo "PATH BROKEN"ステップ 6 の test -f ... && echo は、私が強く残すことを勧める自己チェックです。やっていることはごく単純で、name に書いた絶対パスがディスク上に実際にファイルとして存在するかを確認するだけです。「プラグインの読み込みに失敗した」という多くのケースは、実はフレームワークがエラーを出す段階にすら到達しておらず、パス自体が間違っています。clone した場所が思っていたのと違う、ファイル名の綴りを間違えた(my-plugin を myplugin と書いた)、ディレクトリを一段余分に書いた、などです。このチェックを 2 秒で走らせるだけで、30 分のログ探しを節約できます。
ここで「相対パス」に関する思考の罠についても注意を促しておきます。相対パスが魅力的なのは、より「ポータブル」に見えるからです——別のマシンに移しても変更不要です。しかしプラグイン読み込みという場面では、ポータビリティはプロジェクトのディレクトリ構造によって保証されるのであって、相対パスによって保証されるのではありません。scratch-plugin/ 全体を別のマシンにコピーした場合、変更が必要なのは name の中のリポジトリルートのプレフィックスだけであり、一箇所を直せば済みます。一方、相対パスを使うと、かえって「何に対して相対なのか」という永遠の問いに直面することになります。絶対パスを選ぶことは、エンジニアリング上より堅実なトレードオフです。
開発環境を頻繁に切り替える必要がある場合は、このプレフィックスを小さなスクリプトとして切り出し、手作業での修正を避けられます:
#!/usr/bin/env bash
# ファイルパス:scratch-plugin/refresh-patch.sh
# 用途:現在のリポジトリルートパスを cordis.yml の name フィールドに書き込み、相対/絶対パスの手修正によるミスを避ける
set -euo pipefail
REPO_ROOT="$(pwd)"
PLUGIN_TS="${REPO_ROOT}/scratch-plugin/src/my-plugin.ts"
if [ ! -f "${PLUGIN_TS}" ]; then
echo "[refresh-patch] プラグインのソースファイルが存在しません:${PLUGIN_TS}" >&2
exit 1
fi
cat > scratch-plugin/cordis.yml <<YAML
# ファイルパス:scratch-plugin/cordis.yml
# これは Web オーバーレイ(overlay)で、ローカルプラグインを挿入するだけの役割
- insert:
- id: hello
# name はプラグインファイルのパスであり、絶対パスでなければならない!
name: '${PLUGIN_TS}'
YAML
echo "[refresh-patch] 書き込み完了:${PLUGIN_TS}"
スクリプト内の set -euo pipefail とファイル存在チェックに注目してください。この二つにより、パスの誤りは「設定を書き込む前」に露見し、フレームワーク起動時まで待たされることがなくなります。この「エラーを左にシフトする」習慣は、設定を頻繁に変更するプラグイン開発の場面で極めて高い効果を発揮します。
hello-plugin のロード検証:console.log からプラグインのライフサイクルを眺める
設定は書けたとして、それが本当に効いているかはどうやって分かるのか?ログを見ればいい。前回のプラグインのソースコードを振り返ってみよう:
// ファイルパス:scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
// name はプラグイン名で、ログや設定の中でこのプラグインを識別するために使われる
export const name = 'hello-plugin'
// apply はプラグインの入口:フレームワークがプラグインをロードするときにこれを呼び出す
export function apply(ctx: Context) {
// 必要な依存は apply の実行前にすでに整っている(第 9 回を参照)
console.log('[hello-plugin] plugin loaded!')
}
このコードには、何度も噛みしめる価値のある 2 つのエクスポートがある:
- export const name:プラグイン名で、ログや設定の中でこのプラグインを識別するために使われる。ログで目にするプレフィックスが
[hello-plugin]になることを決め、他の設定項目がこのプラグインを参照したいときにどの名前を使うかも決める。 - export function apply(ctx):プラグインの入口。フレームワークはプラグインをロードするときにこれを呼び出し、ctx(コンテキストオブジェクト) を渡してくる。
したがって検証方法はごく単純だ:ログに [hello-plugin] plugin loaded! が見えれば、フレームワークが確かにこのファイルをロードし、確かに apply を呼び出し、確かにあなたのプラグインに制御を渡したということだ。この 1 行の console.log は、チェーン全体における最初の観測可能なシグナルであり、これから先のあらゆる複雑な機能の土台でもある——もしこの 1 行が出なければ、その後のイベント登録、ツール登録、LLM アダプタ登録などは話にならない。
なぜこの 1 行のログが同時に 3 つのことを検証していると言えるのか?それを「ライフサイクル点検」として分解してみよう:
- ファイルが特定されロードされた:name のパスを書き間違えたり、ディレクトリ構造が正しくなかったり、オーバーレイが効いていなかったりすれば、このモジュールはそもそも評価されず、console.log も当然実行されない。
- apply がフレームワークから呼び出された:たとえファイルがロードされても、フレームワークが apply を呼び出していなければ(たとえばエクスポートの形が正しくない場合など)、ログは現れない。ログが現れる = フレームワークがこのモジュールを認識し、プラグインとして扱ったということだ。
- ctx が渡されている:apply のシグネチャは ctx を受け取り、console.log まで正常に実行できているということは、コンテキストオブジェクトがすでに整っているということだ。これは apply の本体内で ctx を使って機能を登録する前提であり、ソースコードのコメントにある「必要な依存は apply の実行前にすでに整っている」という一文にも呼応している。
ctx(Context) について、素材が与えている定義は個別に書き留めておく価値がある:それはフレームワークが各プラグインに渡すコンテキストオブジェクトであり、機能を登録する入口であると同時に、プラグインが登録したあらゆるリソースを記録するものでもある。この後半がとりわけ重要だ——それは ctx が単なる「道具箱」ではなく、帳簿でもあることを意味する:あなたがそれを通じて何を登録したか、それはちゃんと記録している。これこそ「プラグインのロード」という行為と「リソースの登録」が同じ事柄の裏表である理由でもある:ロードが成功すると、ctx はあなたに代わって記帳を始める。この最小例 hello-plugin では、まだ ctx のどの登録メソッドも呼んでいないので帳簿は空だが、帳簿そのものはすでに手元にある。
読み込みプロセスをシーケンス図にすると、おおよそ次のようになります:
- Web UI を起動し、先ほど書いた cordis.yml を指す
--patchを付けます; - フレームワークはこのオーバーレイを読み取り、insert 指令と name が指す絶対パスに遭遇します;
- フレームワークはそのパス配下のモジュールを読み込み、name と apply を識別します;
- フレームワークはこのプラグイン用に ctx を準備し、apply(ctx) を呼び出します;
- apply 本体の console.log が実行され、
[hello-plugin] plugin loaded!が表示されます; - その後、ctx はこのプラグインが登録したすべてのリソースを継続的に記録します。
強調すべきは、ステップ 6 の「継続的」という点です。ctx は一度きりの関数引数ではなく、プラグインのライフサイクル全体に寄り添います。したがって正しいメンタルモデルは「apply の中で ctx を一度使った」ではなく、「プラグインの一生を ctx に託した」です。これを理解すれば、後でもっと複雑なプラグインを書くときに自然と考えられるようになります:ctx を通じて登録したものは、将来どうやって解除されるのか、他のプラグインからどうやって発見されるのか、ログでどうやって追跡されるのか、と。
2026 年 9 月最新実践:プラグイン開発サンドボックスとしての scratch-plugin の約束事
上記の手順を一連として見ると、それらは断片的なテクニックではなく、一つの完成されたワークフローであることがわかります。現行バージョン時点で、素材にあるこの「scratch-plugin 隔離実験プラグイン + patch オーバーレイ読み込み」の組み合わせは、コミュニティでかなり成熟した開発の約束事となっており、デフォルトのやり方として定着させる価値があります。その中心思想は、新しいプラグインは必ずまず独立したディレクトリで育て、オーバーレイを通じて接続し、検証が通ってから正式な構造に統合するかどうかを検討するというものです。
なぜこの約束事は使いやすいのでしょうか。それは「実験」と「本番」を物理的なレベルで分離するからです:
- リスクの隔離:scratch-plugin の中身をどれだけいじっても、フレームワーク本体の設定は変更されません。壊してしまっても、ディレクトリごと削除すればクリーンな状態に戻れます。
- 読み込み方法の統一:プラグインがどんなに小さくても大きくても、接続方法は常に「cordis.yml を書く + --patch を付けて起動する」であり、認知負荷は一定です。
- 再現可能:ディレクトリ + オーバーレイが自己完結した単位を構成し、他人にコピーすればそのまま動くため、チュートリアル、最小再現(minimal reproduction)、バグ報告の添付に最適です。
- 命名がすなわちドキュメント:scratch という言葉自体が「試験場、使い捨て可能」という意味を伝えています。このディレクトリ名を見れば、誰でも中身が長期的な資産ではないと予想します。
対応する標準的な操作リズムは、五段階のサイクルとして定着させられます:
- リポジトリのルートに
cdし、pwdで絶対パスを取得します; scratch-plugin/src/配下でプラグインファイルを新規作成または修正し、name と apply をエクスポートします;scratch-plugin/cordis.ymlを更新し、name が先ほど変更した .ts ファイル(絶対パス)を指すようにします;--patchを付けて起動し、ログに対応する loaded 出力が現れるか観察します;- 問題ないことを確認してから、さらに機能を追加していきます(ctx を通じてイベント、ツール、LLM アダプターを登録する)。
実践で過小評価されがちな点が一つあります。一度に挿入するプラグインは一つだけにすることです。素材の例では、insert に入っているのは id: hello の一行だけです。学習初期には多くの人が「どうせ insert は配列をサポートしているのだから、実験用の五つのプラグインを一度に全部入れてしまおう」と考えるでしょう。これは初期段階では楽に見えますが、上記のステップ4で述べた可観測性を壊します。五つのログが同時に現れると、どの行がどのファイルに対応するのか、どの変更が問題を引き起こしたのかを特定できません。正しいやり方は、insert 配列を検証の進捗に合わせて徐々に増やしていくことです。今日は一つ追加し、ログがきれいであることを確認し、明日また一つ追加するのです。
もう一つ強調すべき約束事は、ログのプレフィックスには常に角括弧でプラグイン名を囲むというものです。たとえば [hello-plugin] plugin loaded! のようにします。これは必須要件ではありませんが、複数のプラグインを同時に開発し始めると、これがほぼ唯一の命綱になります。何百行も入り交じった出力の中で、[hello-plugin] というプレフィックスがあれば、自分のプラグインに属する行を瞬時に絞り込めます。この約束は、登録成功、登録失敗、イベント受信などの重要な時点を含め、すべてのログ出力箇所で厳格に守ることをお勧めします。
最後に、--patch パラメータそのものについても、一つの経験則があります。毎回手で打つのではなく、起動スクリプトや npm script に書き込むことです。手で打つ場合の問題は、忘れやすいことです。特に cordis.yml を変更した後にフレームワークを再起動するとき、--patch を付け忘れると、フレームワークは正常に起動し正常に動作しますが、あなたのプラグインはまったく読み込まれず、「なぜログに [hello-plugin] がないのか」と長い間悩むことになります。起動コマンドを固定しておくことが、この種の「サイレント障害」を防ぐ最も効果的な方法です。
プラグイン読み込み失敗のトラブルシューティングチェックリスト:パス・パラメータ・ファイル位置の三確認
完全にその通りにやっても、最初の読み込みが一発で成功するとは限りません。良い知らせは、この段階での失敗原因は非常に集中しており、「三確認チェックリスト」で素早く特定できることです。いわゆる三確認とは、パスを確認し、パラメータを確認し、ファイル位置を確認することです。以下、トラブルシューティングの優先度が高い順に展開します。
第一確認:name が絶対パスかどうか。これは最も頻度の高い故障源であり、素材が感嘆符で強調していた項目でもあります。確認方法は簡単です。cordis.yml を開き、name の値が / で始まっているか(Unix 系システムの場合)を見ます。もし . や ..、あるいはディレクトリ名で始まっていれば、それは相対パスであり、必ず修正しなければなりません。修正する際は引用符で囲むことを忘れず、修正後すぐに前節の test -f で自己チェックしてください。
第二確認:起動時に --patch を付けたかどうか。パスが完全に正しいのにログに何も出力されない場合、次に疑うべきは、オーバーレイがそもそも読み込まれていないことです。素材はここで二つのものを使うと明確に述べています。patch オーバーレイの cordis.yml と、--patch 起動パラメータです。両方とも欠かせません。ファイルだけ書いてパラメータを渡さなければ、フレームワークはそれを読むべきだと分かりません。パラメータだけ渡してファイルがなければ、フレームワークは何も読めません。確認方法:起動コマンドを見返し、--patch とそれが指すパスの両方が存在することを確認します。
第三確認:cordis.yml が scratch-plugin の下に置かれているかどうか。素材が示したディレクトリ構造では、cordis.yml と src は兄弟ノードであり、ともに scratch-plugin の下にあります。もしファイルを間違った階層に置いた場合(たとえばリポジトリのルートに置いたり、誤って src の中に入れたりした場合)、name のパスが正しくパラメータも付けていても、渡した patch パスと実際のファイル位置が一致せず、同様に失敗します。確認方法:ls -l scratch-plugin/ を実行し、src と cordis.yml の両方が見えることを確認します。
これら3つの確認項目を表にまとめると、比較しやすくなります:
| 確認項目 | 確認対象 | 期待される状態 | 典型的な症状 |
|---|---|---|---|
| パス | cordis.yml の name の値 | / で始まる絶対パスで、かつファイルが実際に存在する | プラグインから一切ログが出力されない |
| 引数 | 起動コマンドに --patch と正しいパスが含まれているか | --patch が scratch-plugin/cordis.yml を指している | オーバーレイが有効にならず、プラグインが挿入されない |
| ファイル位置 | cordis.yml と src の相対階層 | 両者が scratch-plugin の直下に並んでいる | patch パスがファイルを解決できない |
この3つの確認以外にも、「二次的な容疑点」がいくつかあり、発生頻度順に並べると次のとおりです:
- YAML のインデント崩れ:Tab を使ったりインデント階層が一致していないため、パースに失敗する。症状は通常、起動段階で設定パースエラーが報告されることであり、プラグインが静かに読み込まれないことではありません。
- エクスポートの形が正しくない:apply のエクスポートを忘れたり、apply をアロー関数として変数に代入したが export していない。フレームワークがエントリーポイントを見つけられないため、これもログには現れません。
- ファイルパスにスペースや特殊文字が含まれているのに引用符を付けていない:YAML がパスを分割し、name が不完全な文字列になります。
- ビルド手順の省略:git clone と pnpm install だけを実行し、
pnpm run buildを実行していない場合、環境自体が不完全である可能性があり、TS ソースファイルを読み込む際にパスとは無関係なモジュール解決の問題が発生します。 - id の重複:同じ cordis.yml 内の2つの insert が同じ id を使っており、参照が曖昧になる。マルチプラグインのシナリオでは特に注意が必要です。
トラブルシューティングの推奨順序は、まずパスを自己確認(秒単位)→ 次に起動コマンドを確認(秒単位)→ 次にファイル位置を確認(秒単位)→ 最後にようやく完全なログを調べるです。この順序の価値は、最初の3ステップが確定的で長いログを読む必要がなく、ほとんどの障害をカバーできる点にあります。ログを読むのを最後に回すのは、最初の3ステップをすべて除外した後に初めて行うことです。多くの人のトラブルシューティング習慣は正反対で、いきなり何百行ものログをスクロールし、無関係な情報に埋もれてしまいます。
もう一つ、私自身の苦い経験からの教訓があります:設定を変更した後は、必ずフレームワークが再起動されたことを確認してください。cordis.yml は起動時に読み込まれるため、コードだけをホットリロードしてフレームワークを再起動していない場合、オーバーレイの変更は有効になりません。これはマルチウィンドウ開発で特に陥りやすく、A ウィンドウで yml を変更し、B ウィンドウで古いプロセスのログをぼんやり眺めることになります。
まとめとベストプラクティス
ここまでで、ディレクトリ構造、insert 構文、フィールドの意味、絶対パスの取得方法から、ロード検証とトラブルシューティングまで、チェーン全体が閉じました。全文を実行可能なチェックリストに圧縮します:
- ディレクトリ構造は「1 サンドボックス = 1 src + 1 cordis.yml」に固定する:scratch-plugin/ の下で
src/とcordis.ymlを並列に置き、設定を src に放り込まず、リポジトリのルートディレクトリも汚染しない。 - cordis.yml には insert だけを書き、単一責任を保つ:この Web オーバーレイはローカルプラグインの挿入だけを担当し、他の変更を一切含めないことで、全体の削除とロールバックが容易になる。
- insert 配列には一度に 1 つのプラグインだけを追加する:検証の進捗に合わせて段階的に増やし、ログの各 loaded 出力が明確なファイルと変更に対応することを保証する。
- id と name を厳密に区別する:
id(例えば hello)はエントリの論理識別子であり、設定参照とログの区別に使う;nameはプラグインファイルのパスであり、絶対パスを使わなければならない。 - pwd で絶対パスを取得し、すぐに自己チェックする:リポジトリのルートで
pwdを実行し、<リポジトリルート>/scratch-plugin/src/my-plugin.tsを組み立て、さらにtest -fでファイルが実在することを確認し、設定を書き込む前にエラーを食い止める。 - パスは一律シングルクォートで囲む:スペースや特殊文字が YAML パスを分断するのを避け、同時に中国語を含むディレクトリパスもできるだけ避ける。
- YAML のインデントは一律 2 スペース:Tab を使わず、フォーマッターツールにこのファイルを並べ替えさせない。
- 起動時は必ず --patch を付け、それを起動スクリプトに固定する:付け忘れは典型的な「サイレント失敗」であり、コマンドをハードコードすれば完全に回避できる。
- [hello-plugin] plugin loaded! をロード成功のシグナルにする:このログ行は、ファイルが特定されてロードされ、apply がフレームワークから呼び出され、ctx が渡されたことを同時に証明する;これを以降のすべての機能開発の土台となるチェックポイントとする。
- ログのプレフィックスはプラグイン名を角括弧で囲むよう統一する:複数プラグインを並行開発する際、これが迅速なフィルタリングと特定の唯一信頼できる手段である。
- ctx の二重の役割を肝に銘じる:それは能力を登録する入口であると同時に、プラグインが登録したすべてのリソースを記録する;今後それを介してイベントリスナー、ツール、LLM アダプターを登録する際は、「ライフサイクル全体に伴走する」ものとして設計する。
- ロード失敗は三査の順序で調査する:一に name が絶対パスかどうか、二に起動時に --patch が付いているか、三に cordis.yml が scratch-plugin の下に置かれているかを確認する;三つすべてを除外した後、順に YAML インデント、エクスポート形状、パスのクォート、ビルド手順、id の重複を疑う。
- scratch-plugin を捨て可能な実験場として扱う:実験プラグインはまずここで育て、オーバーレイ経由で接続し、ログで検証した後に正式な構造へ統合することを検討し、実験と本番を物理的なレベルで分離しておく。
最後にもう一度、順序を強調しておきます:まずプラグインを読み込ませること、それからそれがフレームワークのために何ができるかを語ることです。loaded と表示できる空のプラグインは、機能は充実していても読み込めない複雑なプラグインよりも優れています。今日の「絶対パス + patch オーバーレイ層 + ログ検証」という一連の流れを体に叩き込み、筋肉記憶にしてください。そうすれば、後で apply の中で ctx を使ってイベントやツール、LLM アダプターを登録するときに、本来のビジネスロジックに集中でき、「なぜ自分のプラグインが効かないのか」といった問題で時間を無駄にせずに済みます。