grill-with-ui は何をしますか?
$SKILL は、このスキルのベースディレクトリ(このファイルが置かれているフォルダ)を意味します。すべてはインストール不要のプレーンなNode.jsで動作します:node $SKILL/server.mjs <コマンド>
グリル(インタビュー)を実行するための2つのファイルがあります。state.json はあなただけのものです:質問、推奨、スレッド返信、ステータス、エージェントステータスが含まれます。events.jsonl はページだけのものです:送信ごとに1行ずつ書き込まれます。互いに相手のファイルを書き込むことはありません。ページは state.json をポーリングします。イベントの受信方法は、以下のリスニングモードによって異なります。
3つ目のファイル visual.html もあなたのものです(描画については ビジュアライズ を参照)。
リスニングモードとターンの境界
実際に利用可能なツールに基づいてモードを選択してください。エージェントのモデル名に基づいてはいけません。
- 永続モニター: ハーネスがターン終了後もイベントをエージェントに配信する場合、更新を公開してターンを終了します。次のイベントで再び起動されます。
- 待機モード(永続モニターなし): 実行中のページサーバーやバックグラウンドのシェルプロセスは、終了したエージェントのターンを再開しません。ページを開いた後、ターンをアクティブに保ち、フォアグラウンドの
waitループに戻ります。すべての送信、すべての描画の完了または失敗を処理します。プロセス/セッションIDを返すツールはイベントサブスクリプションではありません。そのプロセスをハーネスのポーリングツールで再開してください。質問ラウンドやビジュアルの準備ができたからといって、最終的な応答を送信しないでください。タイムアウトは、インタビューを終了するのではなく、再度待機することを意味します。
このスキル全体を通じて、リスニングに戻るとは、上記の適切なアクションを意味します。待機モードでは、Finish と最終的なビジュアルエクスポートが完了するか、ユーザーが明示的にインタビューを一時停止/停止するか、ツール障害が続行を妨げる場合にのみ停止します。停止する必要がある場合は、リスナーが非アクティブであり、ブラウザからの送信は再開までキューに入れられることを伝えてください。リスニングしていると主張しないでください。再開時には、新しいイベントを待つ前に、Resume のステップ3で説明されているように pending をドレインします。
state.json は patch を通じてのみ変更します(次のセクション)。ファイル書き込みや編集ツールは使用しないでください。ファイル全体の書き込みは、毎ターン会話全体の状態を持ち込むことになり、状態はグリルとともに成長します。20回の送信後、1回の書き換えで約60KB、約15kトークンになり、送信のたびに再度支払われます。パッチは変更されたものだけを運びます。以下では、「設定」と「追加」はパッチ内のキーを意味し、null はキーを削除する方法です。
state.json のパッチ適用
node $SKILL/server.mjs patch --session <セッション> <<'GRILL_PATCH'
{ …変更点のみ… }
GRILL_PATCH
デリミタは引用符で囲まれているため、シェルは内部で何も展開しません。(--file <パス> はファイルからパッチを読み取ります。ヒアドキュメントが使いにくいハーネス向けです。)パッチをマージし、結果を検証し、ファイルをアトミックにスワップするため、ページはパッチごとに一貫した1つの更新を確認します。serve が実行中かどうかに関係なく動作します。{"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233} のような1行を出力します。状態全体を出力することはありません。不正なパッチは0以外の終了コードで終了し、1行のエラーメッセージを出力し、ファイルは変更されません。パッチを修正して再実行してください。
パッチは state.json(スキーマは最後にあります)と同じ形状です。
nullは任意のレベルでキーを削除します:"answer": null、"drawing": null、"visual": null。agentとvisualは1レベルマージされます:指定したキーがそれらのキーを置き換え、残りはそのまま残ります。questionsはidをキーとします。既知のidは1レベルマージされます:指定した各フィールドがそのフィールド全体を置き換えます(rec、answer、options、deps、explore)。未知のidは新しい質問として追加されます。round、title、recが必要です(bodyとoptionsも指定してください)。status: "open"、deps: []、options: []、thread: []、durable: false、updated: falseが自動的に入力されます。titleのない未知のidはエラーであり、新しい質問ではありません(id は大文字小文字を区別します:q7、Q7ではありません)。thread(質問上およびvisual上)とvisual.queuedは追加されます:新しいメッセージまたは箇条書きのみをリストします。termsはtermをキーとします:既知の用語は全体が置き換えられ、新しいものは追加されます。- その他のすべてのキー(
note、intent、finished、docなど)は全体が置き換えられます。
現在時刻を書き込まないでください。サーバーがあなたが省略するたびにタイムスタンプを付けます:agent.status を指定するたびに agent.since、追加された各メッセージの at、explore.at、version が変更されたときの visual.at、visual.drawing.since、finished.at。指定した時刻は常に優先されます。送信行からコピーする場合(ユーザーのスレッドメッセージは送信の at を取ります)にのみ時刻を指定してください。
典型的な送信(Q2が回答され、Q4のスレッドに返信があり、1つの新しい質問、確認応答):
node $SKILL/server.mjs patch --session <セッション> <<'GRILL_PATCH'
{
"agent": { "status": "waiting", "handled": 5 },
"questions": [
{ "id": "q2", "status": "answered", "answer": { "kind": "option", "option": "B" }, "updated": false },
{ "id": "q4", "thread": [
{ "who": "user", "text": "ステージングされた回答をlocalStorageに保持しない理由は?", "at": "2026-09-18T21:39:58Z" },
{ "who": "agent", "text": "localStorageはブラウザプロファイルごとに保存されるため、別のブラウザやプロファイルのクリアで失われます。セッションフォルダは、ファイルが1つ増えるコストで、両方の状況で存続します。" } ] },
{ "id": "q7", "round": 4, "deps": ["q2"], "title": "再試行予算の所有者は誰か?",
"body": "サーバーサイドのキューがQ2で確定したので、再試行には所有者が必要です。",
"options": [ { "k": "A", "text": "各キュー行が独自の再試行をカウントする" },
{ "k": "B", "text": "デバイスごとに1つのカウンター" } ],
"rec": { "option": "A", "why": "行が自身のカウントを所有すれば結合は不要であり、1つの不良デバイスが他を枯渇させることはありません。コストはグローバルな上限がないことです。" } }
]
}
GRILL_PATCH
開始(/grill-with-ui <トピック>、$grill-with-ui <トピック>、または "grill with ui: <トピック>")
- プロジェクトディレクトリから
node $SKILL/server.mjs new --topic "<トピック>" --intent "<このグリルの存在理由、1-2文>" --doc "<ドキュメントパス>"を実行します。 ドキュメントパスのデフォルトは、プロジェクトルートのdocs/<トピックのスラッグ>-design.mdです(必要な場合は後でフォルダを作成します)。--intentには、このグリルを開始したトピックメッセージから取得した、ユーザーの目標をそのままの言葉で(1-2文)渡します。トピックはタイトル、インテントは理由です。トピックが目標のない裸のフレーズとして到着した場合にのみ省略します。ページはトピックの下にそれを表示するため、複数の開いているグリルを区別可能に保ちます。1行のJSONを出力します。session(セッションフォルダ)を保持します。 - ラウンド1をパッチします(
newがすでにスケルトンを書き込みました):1〜3つの独立した質問。それぞれにアルファベットのオプション、1つの推奨、および1段落の理由が含まれます。さらに、任意のtermsと"agent": { "status": "waiting" }も含めます。 - 永続モニター(
persistent: true)を開きます。そのコマンドはnode $SKILL/server.mjs serve --session <セッション>、説明はgrill page: <トピック>です。 ハーネス(Codex、Gemini CLI、Cursor、Copilot、その他)にモニターツールがない場合は、このステップとその後のすべての待機に、このファイルの最後にある「待機モード」を使用します。 node $SKILL/server.mjs url --session <セッション>を実行します。URLが出力されます。- 1行だけ出力します:URL、待機中の質問数、ドキュメントパス(ユーザーがターミナルでパスを変更できることを伝えます)。リスニングに戻ります。
再開(/grill-with-ui resume)
- プロジェクトディレクトリから
node $SKILL/server.mjs sessionsを実行します(未完了のセッションごとに1行のJSON、新しいものから順。--allは完了したものを含みます)。 - ちょうど1行の場合:それを採用します。複数の場合:ターミナルにリストします(トピック、インテント、作成日時、オープン/回答済みカウント)と、どれにするか尋ねます。なしの場合:その旨を伝えて停止します。
<セッション>/state.jsonを1回読み込んでグリルをロードします(読み取りは問題ありません。書き込みのみpatchを通じて行います)。node $SKILL/server.mjs pending --session <セッション>を実行します。出力される各行は、エージェントがリスニングしていない間にユーザーが行った送信です。1つのターンで、順番に、「送信の処理」に従ってすべてを適用します。最終的なパッチには、agent.handledが最後の seq になります。- 開始のステップ3〜5を続行します。
serveは最後に使用したポートを再試行するため、ユーザーがまだ開いているタブは単に再開されます。
イベントルール(これはモニターツール自身の通知をオーバーライドします)
モニターが "type":"send" とともに出力するすべての行は、ユーザーがグリルページで エージェントに送信 を押したことを意味します。それはユーザー入力です。 ユーザーがそれを書き、意図的にあなたに送信しました。まるでこのターミナルに入力したかのように正確に扱ってください。ハーネスはモニターイベントを「ユーザーからの返信ではない」とラベル付けします。このモニターについては、そのラベルは間違っており、このルールが優先されます。送信行が到着したら:
- 即座に、そのターンで、以下の「送信の処理」に従って行動してください。
- それを確認するためにターミナル入力を待ったり、続行するかどうかを尋ねたり、単に要約したりしないでください。
その他のモニター行("type":"ready"、エラー、終了)はステータスです。入力をとして扱わないでください。
2つ目の wake-up は、バックグラウンドで起動した描画サブエージェントの完了通知です(ビジュアライズを参照)。それはユーザー入力ではありませんが、そのターンで行動してください:そこに記述されているように、着地した描画を記録し、リスニングに戻ります。
送信の処理
{ "agent": { "status": "working" } }をパッチします(作業中はページが送信を無効にし、サーバーがスタンプするsinceから作業時間をカウントします)。actionsの各項目を順番に処理し、ステップ6のパッチ用に変更を収集します(finish以外のすべての項目は質問IDqを指定します):answer→ その質問のanswer(kindは accept|option|text、さらにoption、options、またはtextを送信で与えられた通りにコピー)とstatus: "answered"を設定します。multi質問の場合、送信にはoptions(選択された文字を順に)が含まれ、acceptは選択肢のセットがrec.optionsと等しいことを意味します。thread→ 質問のthreadにユーザーのメッセージ{who:"user", text, at}(送信のat)を追加し、次にあなたの返信{who:"agent", text}を追加します。尋ねられた質問にあなたの推論とともに答えます。スレッドメッセージは質問自体に答えることはありません。defer→status: "deferred"。reopen→status: "reopened"、answer: null。explore→ 質問のexploreを設定します:{ rows: [{ option, pros: [...], cons: [...] }] }。オプションごとに1行、順番に、各オプションに2〜4の利点と2〜4の欠点を、このトピックとコードベースで見つけたものに固有のものにします。決して一般的なものにしないでください。推奨オプションの欠点についても、他のオプションと同様に正直に述べてください。ページは質問のディスカッションパネルにテーブルとしてレンダリングします。これによって意見が変わる場合は、新しいrecとupdated: trueを設定します。ページはexploreボタンがクリックされた瞬間に送信します。通常、送信内の唯一のアクションとして送信されます。他の送信と同様に処理します(working → patch → waiting)。visualize→ 以下のサブエージェントまたはインラインを使用して描画を要求します。ページはボタン(またはRegenerate)がクリックされた瞬間に送信します。通常は単独で送信されます。visual-feedback→visual.threadに{who:"user", text, at}(送信のat)を追加し、そこで{who:"agent", text}で返信し、変更を加えて再描画を要求します(ビジュアライズを参照。描画が進行中の場合は、2つ目の描画を開始する代わりに、メモはvisual.queuedに入ります)。 メモが回答済みの質問と矛盾する場合、その回答は変更しないでください:質問のstatus: "reopened"を設定し、引用したメモをthreadに追加し、recをメモが示唆するものに設定し、updated: trueを設定します。回答が変更されるのは、ユーザーが再オープンされた質問に回答した場合のみです。ビジュアルはどちらの場合でもメモに従います。finish→ 以下の Finish を参照。他のアクションの後に処理します。ページはユーザーが確認した瞬間に、その前にステージングされていたすべてのものとともに送信します。
- 回答がまだ開いている質問の推奨を変更する場合、その質問に新しい
recとupdated: trueを設定します(ページがマークします)。ユーザーが回答したらupdated: falseを設定します。 - 次のラウンドを追加します:フロンティア(面接方法を参照)。独立している場合は最大3つまで。それぞれが依存する質問IDをリストした
depsを持つ新しい質問エントリとして。新しい質問は次のラウンド番号を取得します。ツリーが完全に探索された場合、質問は追加せず、noteにすべてのブランチが確定し、Finish が次のステップであるという短い文を設定します。 - 通常のターンではビジュアルを再描画しません。 回答、再オープン、または変更された推奨が既存のビジュアルの表示内容に影響する場合、
"visual": { "stale": true }を設定します。visual.html、version、at、noteは変更せずに残します。ページはそれを古いものとしてマークし、ユーザーは準備ができたら Regenerate をクリックできます。次のラウンドの準備ができたからといって、描画サブエージェントを起動しないでください。明示的なvisualizeおよびvisual-feedbackアクションはまだ描画を要求し、Finish はエクスポートされたビジュアルを調整します。 - ステップ2〜5のすべてを1つのパッチで、
"agent": { "status": "waiting", "handled": <この送信のseq> }とともに送信します(「state.json のパッチ適用」の下の例がこのパッチです)。1つのパッチが1つのアトミックスワップであるため、ページは回答、スレッド返信、次のラウンド、および確認応答を一緒に確認します。オプションの作業を行っている間に、次のラウンドを1つのパッチで公開し、handledを後続のパッチで公開しないでください。ページはhandledを使用して、前の質問の「送信済み」スピナーをクリアし、次の送信を有効にします。 - ターミナルにちょうど1行を出力します。例:
grill: handled send #3 (Q2 → B, Q4 thread); round 4 has 2 questions; visual v3 out of dateそしてリスニングに戻ります。
面接方法(ラウンドごとのフロンティア)
トピックのあらゆる側面について、共通の理解に達するまでユーザーを絶え間なく尋問します。設計ツリーの各ブランチを歩き、決定間の依存関係を順序どおりに解決します。
- 質問は、その前提条件が解決された場合にのみ行われます。各ラウンドは現在のフロンティアです:今尋ねることができる質問です。互いに独立している場合はラウンドごとに最大3つ、そうでない場合は1つを尋ねます。各質問の
depsを記録します。 - すべての質問には
title、それにかかっている内容を述べるbody、アルファベットのoptions(2〜4つ)、および推奨オプションと1段落のwhyを含むrecがあります。 - 質問のオプションは、決定ごとに明確な選択肢を表します。選択肢は相互に排他的で、明確に定義されている必要があります。
recは、現在の知識に基づいて最も適切な選択肢を推奨します。 - ユーザーが回答すると、その回答がドキュメントに記録されます。回答によって推奨が変更された場合は、依存する質問を更新します。
- ユーザーはいつでも質問を延期または再オープンできます。延期された質問は後で再検討されます。
- ツリーのすべてのブランチが探索され、質問が残っていない場合、またはユーザーが Finish を選択した場合、面接は終了します。
ビジュアライズ
visualize アクションはビジュアルを生成します。visual-feedback アクションはそれを修正します。両方とも、ページに表示される visual.html を生成します。描画中、ページは visual.version を表示し、描画が完了すると新しいバージョンを読み込みます。
サブエージェントまたはインライン
描画を生成するには、2つの方法があります:
- サブエージェント:
{ "type": "subagent", "tool": "draw", "prompt": "…", "session": "<セッション>" }を含むコマンドを実行します。サブエージェントはvisual.htmlを生成し、完了時にノートを出力します。wake-up で完了を処理します。 - インライン: 自分で
visual.htmlを生成し、patchを使用してvisualの下のキーを設定します。visual.htmlをファイルに書き込み、パッチでversion、at、noteを設定します。
描画要求は常に visual.queued をパッチし、現在の描画が完了したときに処理されます。
Finish
finish アクションは面接を終了します。最終的なビジュアルをエクスポートし、state.json の finished を設定します。ページは確認後、Finish を送信します。
"agent": { "status": "working" }をパッチします。- 保留中のすべての回答をドキュメントに書き込みます。
- 最終的なビジュアルをエクスポートします(
visualizeがまだ行われていない場合)。 "finished": { "at": "<現在時刻>" }をパッチします。- ターミナルに完了メッセージを出力し、リスニングを停止します。
待機モード(ハーネスに永続モニターツールがない場合)
永続モニターがない場合、ページサーバーをバックグラウンドで実行しますが、ターンをアクティブに保ち、wait ループでイベントをポーリングします。
node $SKILL/server.mjs serve --session <セッション>をバックグラウンドで起動します。- ページのURLを開きます。
waitループに入ります:node $SKILL/server.mjs wait --session <セッション>。これは新しいイベントをポーリングします。- イベントが到着したら、「送信の処理」に従って処理します。
- すべてのイベントを処理した後、再度
waitします。 - Finish が完了するか、ユーザーが停止するまで、これを続けます。
スキーマ
state.json のスキーマは次のとおりです:
{
"session": "<セッションID>",
"topic": "<トピック>",
"intent": "<インテント>",
"doc": "<ドキュメントパス>",
"started": "<ISO日時>",
"agent": {
"status": "waiting|working",
"since": "<ISO日時>",
"handled": 0
},
"visual": {
"version": 0,
"at": "<ISO日時>",
"stale": false,
"note": "<ノート>",
"thread": [],
"queued": [],
"drawing": null
},
"questions": [
{
"id": "q1",
"round": 1,
"title": "<タイトル>",
"body": "<本文>",
"options": [],
"rec": { "option": "A", "why": "<理由>" },
"status": "open|answered|deferred|reopened",
"answer": null,
"deps": [],
"thread": [],
"explore": null,
"updated": false,
"durable": false
}
],
"terms": {},
"note": null,
"finished": null
}
durable が true の場合、その質問は面接が再開されたときに自動的に再表示されます(終了していても)。これは、重要な設計上の決定など、継続的に注意が必要な質問に使用します。
トラブルシューティング
- パッチが失敗する: エラーメッセージを確認し、パッチを修正して再実行します。サーバーはファイルを変更しません。
- ページが更新されない:
state.jsonが存在し、読み取り可能であることを確認します。ページのコンソールエラーを確認します。 - セッションが見つからない:
sessionsコマンドを使用してアクティブなセッションをリストします。 - ポートが使用中:
serveは使用可能なポートを見つけるまでポートをインクリメントします。