テンプレートフォーマット
Busabase テンプレートのディレクトリに何が入っているか — ルートの SKILL.md、busabase.json の template オブジェクト、.busabaseignore、バリデータのルール、そして 2 つのインストーラが同じアプリを認識するための所有スタンプ。
テンプレートは busabase-package@1 フォーマットに、content/ の外側だけで完結する小さな増分を足したものです。
busa-email/
├── SKILL.md ← エージェントが読むマニュアル
├── references/ ← Skill ノードへ一緒に運ばれる
├── agents/
├── scripts/
├── assets/screenshots/ ← テンプレートセンターのカード画像(インストールされない)
├── busabase.json ← マニフェスト + `template` オブジェクト
└── content/ ← 普通のパッケージとバイト単位で同一
├── settings/base.json
├── settings/records.ndjson
└── busa-email-app/ ← AirApp(.busabaseignore 付き)content/ 配下は一切変わりません。パッケージフォーマットがそのままであることが、これを 2 つ目のフォーマットではなく多重化されたフォーマットにしています。同じディレクトリが正当な Agent Skill(npx skills add でインストールできる)であり、同時に正当な Busabase パッケージ(busabase-cli install でインストールできる)であって、どちらの見方も相手を知る必要がありません。
ルートの SKILL.md
エントリファイル名が SKILL.md なのは意図的です。インストール時にそのまま対象フォルダ内の Skill ノード になり、マニュアルがリソースと一緒に移動します — 公開者のディスクに置き去りにはなりません。
そのノードには 3 つの付随ディレクトリが一緒に入ります。
| ディレクトリ | 理由 |
|---|---|
references/ | フィールド定義、スキーマ、例 — SKILL.md がリンクする資料 |
agents/ | skill が同梱するサブエージェント定義 |
scripts/ | skill 自身のスクリプト(例:setup.mjs) |
scripts/ は保存されますが、Busabase が実行することは決してありません。保存するのは、エージェントが space から skill を取り出したときに動く写しが手に入るようにするためで、落とすと往復が非可逆になります。安全性は「Busabase が実行しないこと」から来るのであって、保存を拒むことから来るのではありません。
フロントマター
---
name: busa-email
description: 受信箱のトリアージデスク — 返信を起草し、承認はあなたが行う。
metadata:
busabase:
template: true
resources:
- settings
- reviews
---| キー | 意味 |
|---|---|
name | 必須。busabase.json の name と一致すること — 同じアプリだからです |
description | skill が一覧される場所すべてで表示されます |
metadata.busabase.template | オプトインそのもの。 true は「テンプレートとして公開する」の意味 |
metadata.busabase.resources | マニュアルが言及する Base の slug。すべて content/ に存在する必要があります |
metadata.busabase.folderSlug、risk | 宣言的な項目 — 読み手と将来のツール向けで、現在のインストールは参照しません |
template: true はディレクトリ構造からの推論ではなく、明示的なオプトインです。テンプレートを公開するとは、インストールする人がその AirApp のコードを実行し、その SKILL.md を自分のエージェントに読ませることを受け入れるということ。ディレクトリ形状の副作用ではなく、意識的なフラグに値します。
resources は信用ではなく検証の対象です。存在しないテーブルを教えられたエージェントは、別のテーブルに書き込みます。
busabase.json の template オブジェクト
{
"format": "busabase-package@1",
"name": "busa-email",
"description": "受信箱のトリアージデスク。",
"version": "1.2.0",
"template": {
"category": "email",
"tags": ["inbox", "triage"],
"screenshots": ["assets/screenshots/overview.webp"],
"agentPrompts": [
"今日の受信箱をトリアージして、返信が要るものは下書きを作って。"
],
"airapp": "busa-email-app",
"schemaVersion": 1,
"secrets": [
{ "key": "BUSA_EMAIL_IMAP_PASSWORD_PRIMARY", "description": "メールボックスのパスワード", "required": true }
],
"requires": { "airapp": true }
}
}| フィールド | 既定値 | 役割 |
|---|---|---|
category | 必須 | カードに表示されるテンプレートセンターのカテゴリ |
tags | [] | ギャラリーで検索対象になる |
screenshots | [] | パッケージ相対パス。サーバー側で URL 化されるため、カードがインストール先と別の ref を指すことはあり得ない |
agentPrompts | [] | 詳細ページに出る既製プロンプト — 「テーブルの集まり」と「使えるもの」の差 |
airapp | — | AirApp が 1 つのときの短縮形。airapps とは排他 |
airapps | — | { slug, role, label }[]。role は primary・admin・public・tool。primary はちょうど 1 つ |
schemaVersion | 1 | 宣言するリソース形状が変わったら作者が上げる。所有スタンプの一部なので両方のインストーラが一致している必要がある |
vaultNamespace | — | アプリのシークレットが置かれる Vault の名前空間 |
secrets | [] | { key, description, required }[] — 宣言するだけで、作成はしない |
requires.airapp | — | true にすると「content/ に AirApp がない」が警告ではなくエラーになる |
パッケージフォーマットにはシークレットの値を入れる場所がなく、増やすべきでもありません — フォーマット全体が拠って立つ「フォーマットが表現できないものは漏れない」というルールそのものです。secrets はどのキーを Vault に自分で入れるべきかを伝えるだけで、インストールはそれを作りませんし、現在は入力を促しもしません。
どの AirApp が「そのアプリ」か
機械的な意味を持つのは primary だけで、他のロールは人間向けのラベルであり、エージェントへの手がかり(「管理画面を起動して」)です。解決順は次のとおりです。
template.airappがあればそれ —content/の AirApp と一致していること。- なければ
template.airapps。primaryがちょうど 1 つで、参照先の AirApp がすべて存在すること。 - それもなければ、
content/に AirApp がちょうど 1 つならそれ。 - それ以外はエラー。あいまいさをアルファベット順の先頭で解決することはありません — 黙って違うアプリを開くテンプレートは、公開を拒むテンプレートより悪いからです。
.busabaseignore
AirApp ノードはそのプロジェクトそのものです — Busabase はノードの中身に対して npm run dev を実行します — したがって content/<airapp>/ はデプロイ成果物であると同時に、開発者が作業するディレクトリでもある必要があります。content/<airapp>/.busabaseignore は 1 つのディレクトリに両方を兼ねさせるための仕組みです。リポジトリには test/、ロックファイル、カバレッジを残し、ノードには動作に必要なものだけを渡します。
gitignore 構文の、意図的に小さくした部分集合です。「惜しいけれど一致しない」で作者が驚かないよう、ここに明記します。
| パターン | 意味 |
|---|---|
# コメント、空行 | 無視 |
test/ | そのディレクトリと配下すべて |
coverage | 任意の深さの、この名前のファイルまたはディレクトリ |
/build | AirApp のルートにのみ固定 |
*.log | * は 1 つのパスセグメント内で一致 |
docs/**/*.tmp | ** はセグメントをまたいで一致 |
!keep.log | 否定 — 前のルールで除外されたパスを戻す |
gitignore と同じく、最後に一致したルールが勝ちます。非対応: 文字クラス([a-z])とエスケープ(\#)。これらを使ったパターンは黙って再解釈されず、リテラルとして一致します。
package.json と、そこで宣言されたエントリは決して無視できません。警告ではなくハードエラーです。無視すると AirApp は問題なくインストールされ、そのあと起動しない — 最も原因を突き止めにくい壊れ方になるからです。
スクリーンショット
assets/screenshots/ はテンプレートセンター用のカード画像です。これらはノードではなく、インストールもされません — ギャラリーに見せるものがあるようにするためだけに存在します。template.screenshots にパッケージ相対パスで宣言すると、サーバーがカードのインストール元と同じリポジトリ・ref に対して URL を解決します。
ファイルが無い/改名されている場合は壊れた画像アイコンではなくプレースホルダーに落ちます。無くてもカードは役に立つからです。
必須リレーションを持つサンプルレコード
records.ndjson のリレーション値はパッケージ内の record key であり、そのリレーションフィールドの
targetBaseSlug が指す Base の中で解決されます。インストーラは必須リレーションの参照先を先に作成し、
依存する行を作成する前にそれらの key を canonical な record id へ置き換えます。任意リレーションは
すべてのサンプル行が id を得たあとに正規化されます。
これにより Company → Contact → Activity のような非循環の依存関係が、Base のスキーマを弱めることなく インストールできます。さらに次の 3 つの誤りが、ワークスペースのリソースを 1 つも作る前に、 プレビューの時点で明確に失敗するようになります。
- 必須リレーションが空である;
- その key が宣言された参照先 Base に存在しない;
- 必須リレーションが循環しており、最初に作れる行が存在しない。
record key のスコープは所属する Base です。2 つの Base がどちらも default という key を持っていても
かまいません。参照を一意にするのはリレーションフィールドの targetBaseSlug です。
バリデータが見るもの
パッケージがテンプレートかどうかを決める関数は 1 つで、4 つの利用者がそれを共有します。カタログのビルド、busabase-cli install、ダッシュボードのインストールプレビュー、そして自分の出力を点検するエージェントです。カードには出るのに普通のパッケージとしてインストールされる(あるいはその逆)のは、見た目の問題ではなく信頼の欠陥です。
完全に静的で、AirApp を起動せず、スクリプトを実行せず、サーバーにも接続しません。
エラー — 1 つでも該当すれば「テンプレートではない」
| チェック | なぜ致命的か |
|---|---|
ルートに SKILL.md がない | それは単に普通のパッケージ |
| フロントマターが正当な skill 宣言でない | 下流の何もそれを読めない |
metadata.busabase.template が true でない | オプトインが無ければ公開しない |
busabase.json に template オブジェクトが無い | 最低限 category が必要 |
SKILL.md の name ≠ マニフェストの name | 同じアプリなので一致していなければならない |
resources の項目に対応する Base が content/ に無い | エージェントが違うテーブルに書く |
| 主となる AirApp が解決できない | どのアプリを開くかを推測してはならない |
AirApp の package.json が不正な JSON、または dev スクリプトが無い | インストールは通り、そのあと起動しない |
requires.airapp が true なのに content/ に AirApp が無い | マニフェストと中身が矛盾している |
| いずれかの Base のサンプルレコードが 50 件を超える | テンプレートはデモの種であり、データセットではない |
警告 — 公開はされる。カードが正直になるだけ
| 警告 | 結果 |
|---|---|
| AirApp が無い | データのみのテンプレート:見えるのはテーブルであってアプリではない |
| サンプルレコードが無い | 初回に開いたとき空 |
| スクリーンショットが無い | カードはプレースホルダーを使う |
agentPrompts が無い | 詳細ページに提案プロンプトが出ない |
テンプレートになれないことは、正当なパッケージを拒む理由には決してなりません。 これらのチェックに落ちたパッケージも、テンプレートが存在しなかった頃と全く同じく、普通の busabase-package@1 としてインストールされます。
所有スタンプ
「入口は 2 つ、アプリは 1 つ」を成立させている仕組みです。同じリソースを作る独立したコードパスが 2 つあり(busabase-package のインストーラと、busabase-sdk 経由で動く skill 自身の setup.mjs)、互いの成果を認識する手段は、ノードの metadata に書かれたスタンプの形だけです。
| ノード | スタンプ |
|---|---|
| すべてのリソース(Base、ドライブ、AirApp …) | appId、resourceKey、schemaVersion |
| アプリのルート Folder | 同じ 3 つ組(resourceKey は "app-root")に加えて version、source(repo / ref / subdir)、installedAt |
| パッケージルートから引き上げられた Skill ノード | appId と isTemplateSkill: true |
重要な点が 2 つあります。
resourceKeyはパッケージ側の slug であって、インストール後の slug ではありません。 作者がsettingsと呼んだ Base はbusa-email-settingsとしてインストールされますが、スタンプはsettingsのままです。どのフォルダに入れられても、アプリは安定したハンドルで自分のテーブルを見つけられます。- SDK はスタンプが無い/異なるノードを他人のものとみなし、触らずに
SETUP_CONFLICTを投げます。したがって 2 つの書き手がずれると、テンプレートセンターからインストールしたあとシェルで同じ skill を実行した利用者は、自分のデータの上でハード衝突に遭います。だからこそこのスタンプは契約パッケージに 1 度だけ定義され、どちらの側もローカルに再宣言できません。SDK 側の定義はコンパイル時に契約側と突き合わされます。
Skill ノードの isTemplateSkill は双方向で必要です。export --template はまさにこのノードをパッケージルートへ引き上げ(content/ に書けばフォーマットが排除したい重複が生まれます)、エージェント連携はまさにこれらのノードを*「この space で読めるアプリのマニュアル」*として一覧します。
関連
- テンプレート — インストールと利用
- テンプレートを公開する —
export --templateとカタログ - インストールとエクスポート(packages) — 基盤となるフォーマット
- ノードタイプ — Skill と AirApp ノード