terse-improve について
指定した Terse ワークフローの実装、生成済みの連携定義、本番実行履歴を調べ、改善と検証を行うスキルです。Terse job の保守担当者向けで、決定的なツール利用と安全なテスト、進捗の説明を重視します。
主な機能
- 01job コードと生成済み連携定義の確認
- 02本番実行履歴の調査
- 03ツール利用とプロンプトの監査
- 04ワークフロー変更の実装と検証
- 05コマンド実行とテスト結果の説明
活用シーン
- 01失敗またはキャンセルされた実行の診断
- 02無関係なトリガーイベントの除外
- 03エージェント処理の決定的な呼び出しへの置換
- 04リプレイによるワークフロー修正の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ terseai/ terse --skill terse-improveインストール後、Claude Code で/terse-improveを入力して読み込まれたか確認します。
詳細
簡潔なワークフローを改善する
$ARGUMENTS という名前の Terse ワークフローを改善します。
ワークフローは、コード内ではジョブ(src/terse.jobs.ts の createJob)として定義されています。以下の CLI コマンドでは、そのジョブ名を指定します。
ワークフローの改善方法を学ぶ
Terse プラットフォームのコンテキストを把握するには、以下の情報源を参照してください。
- プロジェクト内にある場合は、プロジェクトのコード(
src/terse.jobs.ts、src/jobs/) - 生成ファイルを主な信頼できる情報源として、Terse の仕組みを確認してください。エントリーポイントの
src/terse.generated.tsのヘッダーには、利用可能なすべてのインテグレーションと、src/terse.generated/フォルダーの行番号付きインデックスが記載されています。このフォルダーには、インテグレーションと種類ごとに 1 つのファイルがあります(トリガーファクトリーとペイロード型を含む<integration>.triggers.ts、型付きツールメソッド、リソース、スキルを含む<integration>.tools.ts、attio.schemas.tsなどのワークスペーススキーマファイル、さらにcommon.tsの共通処理)。インデックスを使って、そのジョブが使用するファイルだけを開いてください。生成ファイルは、何が存在するかとその正確な形式を示します。ライブドキュメントと内容が食い違う場合は、生成ファイルを優先してください。ヘルパーが見つからない場合はterse generateを再実行し、生成ファイルで裏付けられていないドキュメントの記述をもとにコードを書かないでください。 - Terse CLI(
terse --help) - 最後の手段として、ライブの Terse ドキュメント(docs.useterse.ai)を参照してください。まずインデックスページ(https://docs.useterse.ai/llms.txt)を取得して利用可能なページを確認し、その後、ワークフローの改善に必要な特定のページだけを取得してください。
terse-sdk パッケージの dist ファイルは検索も読み取りもしないでください。
利用可能な場合はコンピューター操作を優先する
環境でコンピューター操作ツールを利用でき、グラフィカルインターフェースを通じてタスクを確実に完了できる場合は、常にそれらのツールを使用してください。これには、Claude Code、ChatGPT、その他のインストール済みまたはサインイン済みのアプリにおけるブラウザーやデスクトップの画面も含まれます。
- コンピューター操作を行う前に、読み取り専用の確認であっても、明示的な許可を得てください。直後に行う操作に合わせて依頼文を作成し、実際のアプリまたはサイト、具体的な UI 操作、達成したい結果を伝えてください。質問して、返答を待ちます。「コンピューター操作を利用できるようです。[結果]を達成するために、[アプリ/サイト]で[操作]を行ってもよろしいですか?」という形式にしてください。タスクに合わない汎用的なダッシュボードの例を使い回さないでください。
- API キーを設定する場合は、どのプロバイダーの API キーページを開くのか、画面を移動するのか、スコープを限定したキーを作成するのか、設定を確認するのかを伝えてください。シークレットが表示またはコピーされるときは、ユーザーがその管理権を保持することも説明してください。
terse integrate connectによって開かれた認証ウィンドウでは、接続するインテグレーションを伝え、その特定の同意フローを確認して続行するよう依頼し、サインインや MFA はユーザー自身が行えるよう操作を引き渡してください。 - 現在のタスクでコンピューター操作を使うよう明示的に依頼されている場合は、その指定された範囲について許可を得たものとします。改めて尋ねる必要はありません。それ以外の場合、利用可能であることだけでは許可になりません。
- 許可を得たら、範囲内の通常の画面移動、確認、スクリーンショット、検証、クリック、入力、アップロードは、各手順ごとに確認を求めずに進めてください。別のアプリ、アカウント、画面に移る場合や、目的が大きく変わる場合は、改めて確認してください。
- ユーザーが許可しない場合や返答しない場合は、コンピューター操作を使わないでください。可能であれば標準ツールで続行し、それができない場合は必要最小限の手動操作を説明してください。
- まだ明示的に許可されていない、外部に影響する重大な操作(送信または公開、重要なフォームの送信、削除、購入、権限やセキュリティ設定の変更、本番データの変更)を行う前には、必ず直前に確認してください。そうした具体的な操作を実行する明確な依頼は、通常の範囲内の UI 操作を行う許可とみなします。クリックのたびに再確認する必要はありません。
- OAuth 接続を開始するのは、ユーザーがそのインテグレーションへの接続を明示的に依頼した場合に限ります。許可を得た場合は同意フローの画面を進めますが、パスワード、MFA コード、支払い情報などの機密情報を入力する場面では、操作をユーザーに引き渡してください。コンピューター操作を通じて、それらの情報を読み取ったり、開示したり、入力したりしてはいけません。
- 依頼された画面の範囲内にとどまり、無関係な個人情報を開かないでください。操作後は結果の画面を確認し、実際に起きたことを報告してください。
- ソースコードの編集、厳密な構造化データの読み取り、大量または反復的な操作、Terse CLI コマンドには、標準ツールを使用してください。UI でタスクを確実かつ安全に実行できない場合は、専用コネクターまたは API を使用してください。
- コンピューター操作を利用できない場合、アプリがインストールされていないかサインインしていない場合、画面を操作できない場合、または UI によってシークレットが露出したりリスクが高まったりする場合は、適切に別の方法へ切り替えてください。ユーザーが行う必要のある最小限の手動操作を説明し、その後は自動的に続行してください。
- 以下の確認ルールとテスト安全ルールはすべて守ってください。コンピューター操作によって変わるのは操作する画面であり、許可の範囲ではありません。
手順
src/terse.generated.ts と src/terse.generated/ フォルダーは、接続済みのインテグレーション、利用可能なトリガー、スキル、リソース、決定論的なラッパーの信頼できる情報源です。ルートファイルの先頭にあるコメントには、Terse で現在利用可能なすべてのインテグレーションが記載されています。一覧にないインテグレーションを前提に作業を計画してはいけません。また、各インテグレーションのファイルと、すべてのツール、トリガー、リソースの正確な行番号を示す索引も記載されています。ジョブの実装とあわせてルートファイルを読んでください。terse integrate list は実行しないでください。生成済みファイルには、このワークスペースで terse integrate が接続したもの、または接続可能なものがすでに反映されています。利用可能なインテグレーションで何ができるかを確認するには、terse integrate tool <type> --json でツールとその説明を一覧表示し、terse integrate tool <type> <tool-name> --json で個別のツールの入力/出力スキーマを確認できます。ツールが実際に何を返すかを確認するには、terse integrate tool run <tool> --params '<json>' を実行して一度だけ呼び出します。「CLI からのツール呼び出しのテスト」をテスト規約で参照してください。
ジョブが使用するインテグレーションについて、生成済みファイルがない、または古くなっている場合や、ヘッダーの索引とリーフファイルの実際の内容が一致しない場合は、不足しているヘルパーを推測するのではなく、terse generate を再実行してください。生成済みファイルを直接編集してはいけません。
作業の進捗を伝えてください。 作業そのものについて、くだけた一人称でユーザーに話しかけてください。「実際に何が起きたのか確認するため、本番の履歴を調べています」「新しいフィルターでリプレイが通るようになりました」のように伝えます。フェーズが移るたびに、順序の中での位置を添えて、くだけた言葉でフェーズを明示してください。診断、分析、実装、検証の順に、「診断が終わりました。全4段階のうち1段階目です。次はジョブを詳しく調べ、改善できる点を探します」のように伝えます。内部的な手順番号は口にしないでください。文章だけでユーザーが常に作業の進捗を把握できるようにしてください。
CLI の実行をすべて明示してください。 terse CLI が何かを実行するのは、ユーザーが確認できなければならないイベントです。ハーネスによってはツール呼び出しが折りたたまれることがあるため、実行の説明が必要です。各 terse コマンドの直前に、次のようなマーカー行を出してください。
⏵
terse replay 01hq3v…— 修正を適用して、失敗した本番イベントを再実行します
コマンドごとにマーカーを1つ出します。バッククォート内にはコマンドをそのまま記載し、その後に実行理由を短く添えてください。連続する軽量な読み取りコマンド(terse test list、terse test show)は、1つのマーカーにまとめてもかまいません。状態を変更するコマンドや時間のかかるコマンド(generate、deploy、replay、test run)が完了したら、通常の語り口で結果を報告してください。ユーザーから実行が見えなくなるパイプライン、サブシェル、スクリプトの中に terse の呼び出しを隠してはいけません。
テスト実行前に、すべてのテストについて説明してください。 terse replay や terse test run の場合、マーカー行だけでは不十分です。実行の直前に、何を検証するのか、なぜ今実行するのか、どのような結果が望ましいのかを、くだけた1〜3文で説明してください。「このリプレイでは、火曜日に失敗した本番イベントをそのまま再実行します。新しいフィルターが機能すれば、クラッシュする代わりにボットのコメントをスキップするはずです」のように伝えます。完了したら、同じ語り口で、事前に示した期待に照らして判定を伝えてください。軽量な読み取り(terse test list、terse test show)では、マーカーだけでかまいません。
terse test run が TERSE TEST REPORT を出力した場合は、ただちに自分のメッセージでユーザーに伝えてください。最初の文は必ず「テストは想定どおりに実行されました」または「テストは想定どおりに実行されませんでした」のいずれかにしてください。続けて、報告された副作用を一字一句そのまま列挙し、事前に示した期待との不一致があれば説明してください。
1. ワークフローを見つける
src/terse.jobs.ts と src/terse.generated.ts(ルートの生成ファイル)を開きます。エントリーファイルが副作用のあるインポートのマニフェストである場合は、src/jobs/ までたどります。依頼されたワークフロー名に一致するジョブを見つけ、トリガー、スキル、フィルター、ハンドラーを含む実装全体を読みます。次に、ルートファイルのインデックスをたどり、そのジョブがインポートまたは使用する連携の terse.generated/ のリーフファイルだけを確認し、すべて読みます。以下の分析では、それらの正確なシグネチャを把握しておく必要があります。
CLI は、レガシーフォールバックとして引き続き src/index.ts を読み込めます。また、カスタムレイアウトでは --entry-file を指定してエントリーファイルを上書きできます。
2. 本番環境での実行履歴を取得する
改善案を推測する前に、本番環境でジョブが実際にどのように動作したかを確認します。terse history CLI コマンドを使うと、デプロイ済みのエージェントから過去の実行を取得し、何が問題だったかを確認できます。
推奨される実行方法(すべてのフラグについては https://docs.useterse.ai/reference/cli を参照してください):
# Recent runs as JSON, with the trigger event payload for each one (cheap, ideal for this skill)
terse history "<job-name>" --json --triggers --limit 20
# Narrow to failures only
terse history "<job-name>" --json --triggers --status failed,cancelled --limit 20
# Full chat history (model events + trigger event) for a single run — heavier, use when you need to see exactly what the agent decided
terse history --run-id <run-id> --json
JSON モードのコマンドが actionRequired: true を含む構造化された { "error": ... } エンベロープを返す場合、または終了コード 2 で終了する場合は、コードのバグとして扱わず、処理を止めて必要な次の手順または URL を提示してください。
確認する点:
- 失敗またはキャンセルされた実行 — トリガーイベントに、ジョブの不具合を引き起こした入力が示されています。
- 繰り返されるパターン — 同じ種類のイベントで同様の問題が起きている場合、フィルターの不足、曖昧なプロンプト、またはスキルの不足が考えられます。
- 無駄な実行 — フィルターで除外すべきボットのイベント、下書き、または何もしないイベントです。
- エージェントの過剰な対応 —
generateTextが決定論的な処理を行っています(この場合はtoolboxが適切です)。チャット履歴を確認し、誤ったツールの選択や、パラメーターの幻覚がないか調べます。
ユーザーがまだジョブをデプロイしていない場合(エージェントが見つからない場合)は、この手順を省略し、ソースコードと terse test list のサンプルイベントを参考にしてください。
3. 改善点を分析する
完了する前に、以下のすべての項目を確認してください。それぞれについて、変更を加えるか、すでに問題がないことを確認します。最初に見つけた簡単な改善だけで終わらせないでください。まず ツールの使用 から始めます。処理をエージェントから toolbox に移すことが、通常、最も効果の大きい修正です。
このファイルの末尾にある「Terse Job Code Conventions」セクションが、「Conventions」と「Durability」の監査基準です。よくある修正の変更前後の例については、同じく以下にある「Common Improvement Patterns」セクションを参照してください。
ツールの使用
- 既知の呼び出しと判断の区別:パラメーターが既知のアクションには、
generateTextではなくtoolboxを使います。利用可能なメソッドはterse.generated/<integration>.tools.tsで確認します。ルートファイルのインデックスから正確な行をたどれます。 - 不要なエージェント:ハンドラーが決定論的なツールしか実行しない場合は、
generateTextを完全に取り除き、toolboxを直接呼び出します。 - プロンプトによる連携処理:プロンプトに「Slack に投稿する」「Linear の課題を作成する」「X のラベルを追加する」のような表現がある場合、その手順は通常、コードで処理すべきです。プロンプトは判断が必要な処理(要約、トリアージ、下書き)のみに使います。
- スキルの適用範囲:
skillsはgenerateTextの実行中にモデルが選べるツールを制御します。toolbox.<integration>には影響しません。エージェントを設定せずに決定論的な呼び出しを行う場合は、こちらを使います。 - 複数ステップの処理:まず既知の設定を行い(
toolbox.slack.sendMessage)、次に判断が必要な部分に絞ってgenerateTextを呼び出し、その結果をtoolboxで投稿します。 - ツールの結果:後続の手順で必要になる場合は、決定論的な呼び出しの戻り値を保持します(スレッドへの返信に使う
message.message_tsなど)。
プロンプトの品質
- 具体性:プロンプトはエージェントに何をすべきかを正確に伝えていますか?「このイベントに対応して」のような曖昧なプロンプトはトークンを浪費し、結果に一貫性がなくなります。「PR の変更点を箇条書き 3 点で要約し、Slack に投稿する」のように具体的にしてください。
- イベントのコンテキスト:
event.formatForAgentRunner()を使ってイベントのペイロード全体を含めていますか? - エッジケース:状況が曖昧な場合の対応をプロンプトで説明していますか?例:「PR に説明がない場合は、差分のみから要約する。」
- 形式の指示:出力形式を指定していますか?「Block Kit JSON 形式にする」と指定するか、形式を未指定のままにするか。
- 長さ:プロンプトが長すぎませんか?複数段階の指示は複数のエージェント実行に分割するか、予測可能な部分には決定論的なツール呼び出しを使用してください。
イベントのフィルタリング
- ボットのイベント:ボットが生成したイベントはスキップすべきですか?(
event.sender.login.includes("[bot]")) - ドラフト/WIP:ドラフト PR や WIP 項目は無視すべきですか?
- 特定のソース:特定のユーザー、リポジトリ、チャンネルからのイベントをフィルタリングすべきですか?
- コスト:フィルタリングされていないイベントはすべてエージェント実行を発生させます。フィルターを使えば、実際のコストを削減できます。
エラー処理
- データの欠落:イベントデータが欠落している場合に、コードは適切に処理しますか?(例:本文がない PR、コミットがないプッシュ)処理を続けようとせず、カスタムエラーを使って早期に失敗させてください。
- プロンプトの堅牢性:ツール呼び出しに失敗した場合の対応を、エージェントのプロンプトで説明していますか?
規約
- 規約セクションのすべてのルールに照らしてジョブを監査してください。規約セクションがチェックリストです。
- 見つかった違反はすべて報告してください。この改善ですでに変更しているコードパスの違反のみ修正し、単独で行うスタイルの改修は、明示的にオプトインできる選択肢として提示してください。ジョブごとに 1 ファイルとするレイアウト規則への違反については、すでに変更しているジョブのみを切り出し、残りはインラインのままにして、違反として指摘してください。
耐久性
- 耐久性のあるジョブ:「耐久性のあるジョブのスタイル」ルール(ステップの配置、シリアライズ可能な境界、分岐ヘルパー、ジャーナル化された分岐条件)に照らして、ハンドラーを監査してください。
- 耐久性のないジョブ:規約セクションの「耐久性のあるジョブにすべき場合」に記載された耐久性を必要とする兆候が入り込んでいないか確認してください。強制的な兆候(
waitForInput、sleep)がある場合、そのジョブは改善の余地があるのではなく、壊れています。durable: trueへの切り替え(および必要なステップの再構成)は必須の修正です。質問形式ではなく、その結果として明示してください。判断の目安となる兆候(副作用を伴うステージが 3 つ以上あること)がある場合は、切り替えは推奨する任意の改善です。
スキルの設定
- 不足しているスキル:
generateTextの実行中にモデルが必要とする統合機能がすべて含まれていますか?プロンプトで Slack への投稿を指示しているのに、skillsに Slack がなければ、そのエージェント処理は失敗します。 - 不要なスキル:エージェントが実際には使用しないスキルがありますか?混乱を減らすため、それらを削除してください。
- スコープ:リポジトリ、チャンネル、チームのスコープは適切ですか?広すぎるとエージェントが触れるべきでないものにもアクセスできてしまい、狭すぎると役割を果たせません。
4. 動作の変更についてユーザーに確認する
提案する改善によってジョブの外部から観察可能な動作が変わる場合(以前は処理していたイベントをスキップするフィルター、書き換えたプロンプト、変更した出力先、判断の目安となる兆候に基づく耐久性への切り替えなど)は、実装する前にユーザーに判断を求めてください。それぞれに推奨する回答を提示し、関連する質問はまとめて、確認の中断は最大 4 問までにしてください。機械的な修正(型付け、エラークラス、ファイル構成)と、強制される耐久性への切り替えについては確認不要です。
コード、実行履歴、ドキュメントから事実を確認できる場合は、質問するのではなく調べてください。対話相手のいないヘッドレス環境で実行している場合は、推奨する回答を採用し、その理由を最終的な要約に記載してください。
5. 改善を実装する
ジョブのファイルを編集します。単一ジョブのプロジェクトでは src/terse.jobs.ts、マニフェスト形式のプロジェクトでは src/jobs/<name>.ts、またはリポジトリで設定された --entry-file を編集してください。「Conventions」セクションの規約に従って変更します。
改善に新しい連携が必要な場合は、接続する前に説明します。terse integrate describe <type> --json を実行すると、installationType(form または OAuth)と、フォーム形式でインストールする場合に connect が必要とする正確なフィールドが返されます。フィールド名を推測してはいけません。フォーム形式でインストールする場合は --field key=value で値を渡し、シークレットは --fields-stdin で渡します。OAuth 形式でインストールする場合はブラウザーが開き、ハンドオフ情報とともに終了コード 2 で終了します。そのため、先にユーザーに伝えてから terse integrate wait <type> を実行してください。
新しい連携に接続した後、またはヘルパーの更新が必要な場合は、terse generate を再実行し、ヘッダーインデックスと該当するリーフファイルを読み直します。生成されたファイルを手作業で編集してはいけません。
6. 変更をローカルで検証する
正常に動作することを確認せずに、変更を引き渡してはいけません。
リプレイとテスト実行では、実際の認証情報を使い、本番イベントに対して実際のハンドラーを実行します。外部の対象に書き込むジョブを初めてローカルで実行する前に、このファイルの末尾にある「Testing Safety Conventions」セクションに従ってください。書き込み先をテスト用の対象にするか、テストモードのキーを使い、本番環境への書き込みはユーザーに確認してください。
相互に補完する 2 つのループがあります。
失敗した本番実行をそのままリプレイします。 手順 2 で terse history を使って問題のある実行を見つけた場合は、新しいコードでローカルにリプレイします。
terse replay <run-id>
terse replay は、Terse バックエンドから元のシリアライズ済みトリガーイベントを取得し、詳細なエージェント出力とともに、ジョブの onTrigger をローカルで実行します。これにより、本番環境で確認したバグが実際に修正されたかを最速で確かめられます。
または、新しいサンプルイベントで非対話的に実行します。 再現すべき特定の実行がない場合、または正常系にデグレードがないことを確認する場合に使います。
terse test list "<job-name>" --json
terse test show <id> "<job-name>" --json
terse test run "<job-name>" --id <id>
terse test list は、バックエンドから実際のサンプルイベントを取得します(cron および webhook トリガーの場合は合成イベントを生成します)。取得したイベントには、コンテンツアドレス指定の ID が割り当てられます。
terse test show を使うと、実行前に特定のキャッシュ済みサンプルを確認できます。
terse test run は、TTY を必要とせずにハンドラーを実行します。
複数のジョブがある場合は、非対話的なジョブの読み込みでは選択を促せないため、ジョブ名を明示的に指定してください。
引数なしの terse test は、手動の対話セッションでのみ使用してください。
特定のペイロードをリプレイする場合や、サンプルイベントがない場合は、イベントファイルを手動で作成し、terse test run --event-file <path> で実行します。形式が一致しない場合、コマンドは想定されるエンベロープと検出された検証エラーを正確に出力します。フィールドを推測するのではなく、そのエラー出力をもとにファイルを修正してください。
これらすべてのコマンドのオプション一覧については、https://docs.useterse.ai/reference/cli を参照してください。
7. プロジェクトを型チェックする
ローカル実行が正常に見えたら、デプロイ前に変更が静的に有効であることを確認するため、型チェッカーを実行します。
pnpm exec tsc --noEmit
プロジェクトの構成に合っていれば、npx tsc --noEmit または pnpm run build を使ってください。報告する前に、エラーをすべて修正してください。
8. 変更内容を説明する
実装と検証の後、変更した内容とその理由を要約します。必要に応じて、各変更のきっかけとなった terse history の本番実行を挙げ、修正の確認に使用した terse replay/terse test list/show/run の実行内容も記載してください。
9. デプロイ前に確認する
terse deploy を自動的に実行しないでください。変更内容を説明した後、今すぐデプロイするかどうかをユーザーに確認してください。
デプロイ済みの永続的なジョブのステップ構成を変更した場合(ステップの追加、削除、並べ替え)、同じ確認時に警告してください。実行中の処理は変更後のジャーナルを基に再開されるため、失敗したり、予期しない動作をしたりする可能性があります。デプロイは処理が落ち着いているタイミングで行ってください。
確認プロンプトの例:
改善内容はローカルで検証済みです。
terse deployで本番環境にデプロイしますか?(プロジェクト内のすべてのジョブが同期されます。削除されたジョブはリモートで削除されます。)
- ユーザーが「はい」と答えた場合は、
terse deployを実行し、結果を報告してください。 - ユーザーが「いいえ」と答えた場合、または追加の変更を希望した場合は、デプロイせずに中止し、準備ができたときに
terse deployを実行できることを伝えてください。
よくある改善パターン
分析の工程でよく適用される修正の変更前後の例です。メソッド名と定数名はプロジェクトの生成ファイルに由来します。決して勝手に作らないでください。
ボットのフィルタリングを追加する
// BEFORE: runs on every event
onTrigger: async (event) => { ... }
// AFTER: skip bot events
filter: async (event: GithubPRTrigger) => {
return !event.sender.login.includes("[bot]") && !event.pullRequest.merged
},
onTrigger: async (event: GithubPRTrigger) => { ... }
プロンプトをより具体的にする
// BEFORE: vague
await generateText({ prompt: `Review this PR: ${event.formatForAgentRunner()}`, skills: [...] })
// AFTER: specific instructions, format, edge cases
await generateText({
prompt:
`Review PR "${event.pullRequest.title}" (${event.pullRequest.url}). ` +
`Look at the diff and leave a concise review comment. ` +
`Focus on: correctness, edge cases, and naming. ` +
`Skip style nits. If the PR looks good, approve it with a short note. ` +
`Context: ${event.formatForAgentRunner()}`,
skills: [...],
})
決定論的なアクションと AI アクションを分ける
// BEFORE: agent decides everything including the message send
await generateText({ prompt: `Send a welcome message and summarize: ${event.formatForAgentRunner()}`, skills: [...] })
// AFTER: deterministic send via toolbox (no skill needed), then AI analysis in thread
import { generateText } from "terse-sdk"
import { toolbox } from "./terse.generated"
const message = await toolbox.slack.sendMessage({
channelId: SlackChannel.Engineering.channelId,
message: `New PR from ${event.sender.login}: ${event.pullRequest.title}`,
thread_ts: "",
blocks: "",
})
const summary = await generateText({
prompt: `Summarize the changes in this PR. Context: ${event.formatForAgentRunner()}`,
skills: [Skills.github({ repos: [Repos.MyOrg.MyRepo] })],
})
await toolbox.slack.sendMessage({
channelId: SlackChannel.Engineering.channelId,
message: summary,
thread_ts: message.message_ts,
blocks: "",
})
型安全性を追加する
// BEFORE: untyped event
onTrigger: async (event) => {
await generateText({ prompt: `Handle: ${event.formatForAgentRunner()}`, skills: [...] })
}
// AFTER: annotate with the precise trigger type that matches your trigger factory.
// `Triggers.github.onPROpened(...)` returns a typed trigger, so `event` infers
// as `GithubPROpenedTrigger` — annotating just makes it explicit.
import { generateText } from "terse-sdk"
import type { GithubPROpenedTrigger } from "./terse.generated"
onTrigger: async (event: GithubPROpenedTrigger) => {
const { title, url } = event.pullRequest
await generateText({
prompt: `Review PR "${title}" at ${url}. Context: ${event.formatForAgentRunner()}`,
skills: [...],
})
}
テスト時の安全に関するルール
これらのルールは、ジョブのあらゆるローカル実行(terse test run、terse replay、およびアドホックなツール呼び出し(terse integrate tool run))に適用されます。ローカル実行では、実際の認証情報を使って実際のハンドラーが動作します。安全な宛先を指定しない限り、テスト実行はサンドボックス化されません。
実際のイベントを入力し、テスト用の宛先に出力する
サンプルイベント(terse test list、terse replay)は本番環境の実データです。それを入力として使うことが目的です。現実的なペイロードを使うことで、実際のバグを検出できます。注意すべきなのは副作用です。テスト実行による書き込みが、実在の人や実際のサービスに対して行われてはなりません。メール、メッセージ、チケットの更新、CRM への書き込みなど、テスト実行中の出力先はユーザーが指定したテスト用の宛先とし、イベントに含まれる顧客、チャンネル、レコードには決して送信・書き込みを行わないでください。
認められる例外は、実際の宛先に切り替えた後に行う検証実行を 1 回だけです。実行前にユーザーにその旨を伝えてください。
テスト用 API キー
作業対象が、課金される、または顧客に見える影響を与える外部 API(Stripe、Resend など)の場合は、テスト用の宛先に関する質問に含めて、プラットフォームにテスト用またはサンドボックス用のキーがあるかどうかをユーザーに一度だけ尋ねてください。「作業中に使うテストモード用のキーはありますか?ある場合は、terse secrets add <NAME> で追加してください。」ローカル実行ではすべてテスト用キーを使用してください。ライブシークレットへの切り替えは、実際の宛先に最終切り替えする際に行います。
シークレットの値は書き込み専用です。保存済みのシークレットがテスト用キーかライブキーかを確認するために、読み取ろうとしてはいけません。確認は調べるのではなく、質問して行ってください。
テスト用キーがない場合:読み取りは自由、書き込みは事前確認
テスト用キーがない場合、ローカル実行では本番環境と同じ認証情報が使われます。
- 読み取りに許可は不要です。 テスト実行中に本番データの一覧取得、取得、クエリを行うのは、常に問題ありません。
- 書き込みは宛先ごとに確認します。 本番環境の宛先(実際のリポジトリ、稼働中のオーディエンス、顧客レコードなど)に書き込む最初のローカル実行の前に、その宛先についてユーザーに明示的に確認してください。同じ宛先への後続の実行には、最初の確認が適用されます。新しい宛先を使う場合は、改めて確認が必要です。
CLI からのツール呼び出しのテスト
外部 API が返す内容(レコードの形式、ステータスフィールドが取り得る値、存在するチャネルなど)を確認するには、CLI からツールを実行します。
terse integrate tool run slack.listChannels
terse integrate tool run attio.records --params '{"request":{"action":"query","objectSlug":"deals","filter":null,"limit":5,"offset":null}}'
- ツール名にはワイヤー名(
attio_records)またはドット形式(attio.records)を指定します。名前が間違っている場合は、有効な名前の一覧とともにエラーが表示されます。 --paramsには、ツールのワイヤー形式の JSON を指定します。これはterse integrate tool <type> <tool-name> --jsonが出力する正確な入力スキーマです。生成されたtoolbox.*シグネチャではなく、このスキーマを確認してください。ワイヤー形式とは異なる場合があります。パラメーターが大きい場合は、--paramsではなく標準入力から渡します。- 接続は、プロジェクトに固定された接続(
terse integrate use <type> <connection-id>)またはワークスペース内の唯一の接続から自動的に解決されます。複数の接続があり、固定されていない場合は、terse integrate connections <type> --jsonで ID を一覧表示してから、--integration <id>を指定して再実行するか、接続を固定します。 - 結果は未加工の JSON として標準出力に表示されます。失敗した場合は、ゼロ以外の終了コードで終了し、エラーは標準エラー出力に表示されます。
探索実行は読み取り専用にしてください。query、list、get を使用します。書き込みはジョブ内で行い、上記のセクションに従ってください。
ツールボックスのツールがない API で、呼び出しにハイドレート済みのシークレットが必要な場合は、既存のジョブ内で読み取りを実行します。
- 読み取りを実行し、結果をログに記録して
returnで終わる、連続したコードブロックをジョブのハンドラーの先頭に挿入します。これより下のコードは実行されません。 terse test runで実行します。実際のジョブと同じ方法でシークレットがハイドレートされます。- 判明した内容を記録し、コードブロックを削除します。その後、
git diffでハンドラーが変更前とバイト単位で同一の状態に戻ったことを確認します。
このためだけの使い捨てジョブは作成しないでください。また、コードブロックが残った状態でデプロイしないでください。terse deploy はプロジェクト内のすべてのジョブを同期します。
探索で判明した値は、名前付きの明示的に型付けされた定数としてジョブコードに記述します。コード規約の「検出した値は型付き定数にする」を参照してください。
Terse ジョブコード規約
この規約は、プロジェクトのジョブファイル(src/terse.jobs.ts と src/jobs/)で作成または変更する、すべてのジョブコードに適用されます。優先順位は次のとおりです。事実(API シグネチャ、CLI フラグ、利用可否)については Terse の最新ドキュメントが優先され、スタイルについてはこのファイルが優先されます。
セッション中にユーザーが指定した上書き内容は、このファイルより優先されます。
型の規律
すべてに型を付け、型アサーションはほとんど使いません。 as や any を使うのは、ほぼ避けてください。型アサーションが必要になる場合は、通常、アプローチが間違っています。より上位のレベルでリファクタリングする、サードパーティ製依存関係のラップ方法を修正する、またはユーザーに対応方法を確認してください。型ガード、ジェネリクス、satisfies を優先します。
信頼境界では Zod を使用します。 自分で制御できない境界をデータが越える箇所では、正確にその箇所で Zod スキーマ(静的型には z.infer を使用)が必要です。
- 公式 SDK で型付けされていない外部 API のレスポンス
- Webhook トリガーのペイロード
jobStepのinputSchema/outputSchemagenerateTextのoutputSchema
自分で構築する内部データ構造は、通常の型のままにします。公式 SDK の型は信頼できるものとして扱い、実行時に再検証しないでください。
スキーマから静的型を導出し、信頼できる情報源を一つにします。
const Classification = z.object({
severity: z.enum(["critical", "routine"]),
reason: z.string(),
})
type Classification = z.infer<typeof Classification>
明示的な戻り値の型。 関数宣言と名前付きのアロー関数は、ヘルパーを含め、すべて明示的な戻り値の型を指定します。インラインコールバック(map/filter のラムダや、createJob にインラインで渡す filter 関数と onTrigger 関数)は推論に任せます。
検出した値は型付き定数にします。 外部の状態を調べて判明した値(オーディエンス ID、確認済みドメイン、チャネル ID など)は、名前付きの明示的に型付けされた定数として記述します。呼び出し内に文字列リテラルのまま直接記述してはいけません。SDK に型がある場合はそれを使い、ない場合は範囲を絞ったエイリアスを使います。
const WAITLIST_AUDIENCE_ID: AudienceId = "78261eea-8f8b-4381-83c6-79fa7120f1cf"
type AudienceId = string
制御フロー
判別可能なユニオンを網羅的に処理します。 判別子を switch で振り分け、default の末尾では throw x satisfies never とします。インラインの三項演算子で振り分けてはいけません。
switch (classification.severity) {
case "critical":
return escalateCritical(event, classification)
case "routine":
return fileRoutine(event, classification)
default:
throw classification.severity satisfies never
}
try/catchを入れ子にしません。 catch 本体で独自のエラー処理が必要な場合は、catch 本体をヘルパー関数に切り出し、catch から呼び出します。
時刻はイベントから取得します。 日付や期間は event.triggeredAt から導出し、キューの遅延、再試行、再生によってずれが生じる Date.now() や new Date() は使いません。期間の計算は通常の同期コードで行い、ステップやヘルパーは必要ありません。
関数型の反復処理を使います。 変換には map/filter/reduce、同期的な副作用には forEach を優先し、for ループはできるだけ使いません。ループ本体で await する場合は、並列処理には Promise.all(items.map(...))、順次処理には for...of を使います。forEach は待機せずに実行し、拒否を握りつぶすため、非同期コールバックを渡してはいけません。
.then より async/await を使います。 .then() のチェーンではなく、async/await と Promise のコンビネーター(Promise.all、Promise.allSettled、Promise.race)を使います。
エラーにはカスタムクラスを使い、Error を extend して this.name を設定します。
class MissingSecretError extends Error {
constructor(secretName: string) {
super(`Missing required secret: ${secretName}. Add it with \`terse secrets add ${secretName}\`.`)
this.name = "MissingSecretError"
}
}
存在しない値を捏造してはいけません。 契約上、値が存在するはずの場合(SDK 呼び出しのエラーはすでに処理済み、スキーマでフィールドが必須と定義されているなど)、?. や ?? null を使って値がない状態をデータとして紛れ込ませてはいけません。違反した不変条件の名前を示すカスタムエラーをスローします。正当なドメイン状態として値がない場合(検索結果が見つからない、本当に任意のフィールドなど)は問題ありませんが、明示的にモデル化します。型を | null とし、すべての呼び出し元で分岐するようにします。
const { data, error } = await resend.emails.send({ from, to, subject, html })
if (error) throw new ResendApiError("emails.send", error.message)
if (!data) throw new ResendApiError("emails.send", "no data in response")
return { emailId: data.id }
ファイル構成
段階的に詳細化するルール。 高水準のロジックを上部(createJob とその onTrigger)に置き、ヘルパー関数をその下に置きます。新聞記事のように、全体像から詳細へと読めるファイルにします。
型、インターフェース、zod スキーマはファイルの末尾に置きます。 実装より下に配置します。ハンドラーやヘルパーから参照されるスキーマが読み込まれるのはジョブの実行時なので、末尾に置いても問題ありません。
コメントは最小限にします。 選択が自明でない場合、奇妙な場合、または意図的な妥協である場合にのみ追加します。
プロジェクト構成
ジョブごとに必ず1つのファイルを用意します。 各ジョブは src/jobs/ 内の専用ファイルに置き、ジョブ名に基づくケバブケースのファイル名にします(src/jobs/triage-bug-reports.ts)。また、プロジェクトにジョブが1つしかない場合でも、src/terse.jobs.ts は副作用を伴うインポートだけを記述したマニフェストにします。
import "./jobs/triage-bug-reports"
import "./jobs/weekly-digest"
createJob() は副作用によって登録されるため、エントリーファイルからインポートされないジョブファイルは、実行もデプロイもされないままになります。src/jobs/ 内のすべてのファイルに、マニフェスト内の対応するインポート行が必要です。
ジョブファイルでは、生成されたヘルパーを ../terse.generated からインポートします。複数のジョブで共有する、純粋でステップを含まないヘルパーは共有モジュールに置いてもかまいません。ステップを含むヘルパーは、そのジョブのファイルに置きます(下記の「分岐はヘルパーに切り出す」を参照)。2つのジョブで必要な場合は、ジョブファイル間で重複させます。
ライブラリ
一般的な問題(再試行、日付計算、解析、検証など)には、自作するよりライブラリを優先します。 広く使われ、メンテナンスが行き届いているものを選びます。採用前にダウンロード数と最近のリリースを確認します。
プラットフォームとの連携
この順番で確認し、要件を満たせる最初の段階で止めてください。
-
Terse に組み込み済みで、すでに接続されている連携 —
src/terse.generated.tsとそのterse.generated/フォルダー内のすべて(toolbox.*、Skills.*、Triggers.*)。 -
組み込みの連携タイプだが、まだ接続されていない —
terse integrate connectで接続してから、terse generateを再実行します。 -
組み込みの連携がない — 正式なものであることを確認したうえで、プラットフォームの公式 TypeScript SDK を使用します。公式であることの確認方法は、ベンダーの npm 組織から公開されていること、またはベンダーの公式開発者ドキュメント/GitHub 組織からリンクされていることです(例:
@slack/web-api、octokit、@linear/sdk、stripe)。組み込みの型を活用してください。 -
公式 SDK がない — 有力なコミュニティ製ラッパーを調査し、次のどちらかをユーザーに提示します。
- コミュニティ製ラッパー。具体的な根拠として、GitHub のスター数、メンテナンス年数、最終リリース日、npm の週間ダウンロード数、メンテナーの評判を示します。
- プラットフォームの REST API ドキュメントに基づき、レスポンス境界で zod スキーマを使う、独自に実装した型付き fetch クライアント。
コミュニティ製ラッパーを、ユーザーに知らせずに採用してはいけません。
認証情報:第 2 段階より先で必要になるものは、すべてプロジェクトシークレットで管理します。terse secrets add <NAME> で保存し、ジョブの先頭で process.env.<NAME> から読み込み、値がない場合はカスタムエラーを使って即座に失敗させます。
Terse 自身の 2 つの認証情報を設定する必要はありません。TERSE_PROJECT_KEY は実行中のジョブコードの認証に使われます。プラットフォームがすべての実行にこれを注入し、terse run/terse test はローカルでこれを導出するため、.env やシークレットに記述してはいけません。TERSE_API_KEY はデプロイ、生成、連携管理に CLI が使うユーザートークンです。データプレーンの環境に含めてはいけません。また、プロジェクトキーを TERSE_API_KEY に設定してはいけません。terse secrets は TERSE_ で始まる名前を拒否するため、これらのいずれかを使おうとしている場合は、設計に問題があります。
スカラー値の認証情報(API キー、トークン)は、そのまま保存します。ファイル形式の認証情報(Google サービスアカウントの JSON、PEM キー、その他の複数行の値)は、_B64 を末尾に付けた名前で base64 エンコードして保存し、生のまま貼り付けてはいけません。生の JSON は対話型プロンプトを壊し、シェルのクォートを非常に扱いにくくしますが、base64 なら安全な単一のトークンになります。ファイルのパスをユーザーに尋ね、ファイルから直接エンコードしてください。これにより、平文が会話に現れることはありません。
base64 -i service-account.json | terse secrets add GOOGLE_SERVICE_ACCOUNT_B64 --value-stdin
(GNU coreutils では、行の折り返しを無効にするために -w 0 を追加します。)
ジョブの先頭でデコードし、ジョブが使うフィールドを境界で検証します。
const encoded = process.env.GOOGLE_SERVICE_ACCOUNT_B64
if (!encoded) throw new MissingSecretError("GOOGLE_SERVICE_ACCOUNT_B64")
const parsed = serviceAccountSchema.safeParse(JSON.parse(Buffer.from(encoded, "base64").toString("utf8")))
if (!parsed.success) throw new InvalidSecretError("GOOGLE_SERVICE_ACCOUNT_B64", parsed.error)
_B64 シークレットが見つからない場合のエラーメッセージには、修正をコピーして貼り付けられるよう、エンコード用の完全なワンライナーを記載してください。
Durable ジョブのスタイル
durable: true を設定したジョブに、以下のルールが適用されます。仕組み(リプレイモデル、step()、jobStep、sleep、waitForInput)については https://docs.useterse.ai/core-concepts/durability を参照してください。記載されている事実が優先されます。
Durable にする場合。 シグナルは 2 種類あり、それぞれ異なる扱いをします。
- 強制シグナル — 人間による入力または承認(
waitForInput)、あるいは時間指定の待機(sleep)です。これらのプリミティブは durable モードでのみ使用できるため、存在する場合はdurable: trueが必須です。ユーザーに判断を委ねることはなく、その帰結を伝えるだけです。 - 判断シグナル — 実行途中の失敗によって、目に見える未完了の作業が残る副作用のあるステージが 3 つ以上ある場合です。これは実際に選択が必要なケースです。
durable: trueを推奨し、ユーザーに判断してもらいます。
どちらのシグナルもなければ、デフォルトは non-durable です。
インラインの step() をデフォルトにします。 各外部呼び出しを直接ラップします(await step(client.method(args)))。そうすることで、ハンドラーを順次実行されるブロックとして読めます。簡潔な SDK 呼び出し(toolbox.*、generateText、state.get/state.set)は、すでに durable ステップなので、そのままにします。
ステップ境界を越えるのはデータだけです。 引数と解決された値はジャーナルに記録されるため、シリアライズ可能でなければなりません。クロージャー、コールバック、ストリーム、ライブオブジェクトは渡せません。呼び出し先はモジュールスコープに置く必要があります。複数の呼び出しをまとめる場合や、コールバックを受け取る API を使う場合は、モジュールスコープの関数を作成し、その呼び出しをラップします(await step(sendWithRetry(args)))。
jobStep({ inputSchema, outputSchema, run }) は信頼境界のために限って使用します — 耐久性境界を越える値に実行時検証が必要な場合、または 1 つの単位としてジャーナルに記録する必要があり、モジュールスコープ関数の呼び出しでは表現できないブロックに使用します。
分岐はヘルパーに切り出します。 条件分岐にステップ(step()、jobStep、toolbox.*、generateText、sleep、waitForInput)が含まれる場合に限り、その分岐を名前付きヘルパー関数に切り出します。値の計算だけを行う条件分岐はインラインのままにします。ヘルパーは同じファイル内でジョブの下に配置します。step() は createJob() を呼び出すファイル内でのみ変換されるため、ヘルパーを別のファイルに移すと、気づかないうちに動作しなくなります。こうすることで、ハンドラーを上から順に読み進め、必要なときだけ各分岐の中を確認できます。
ジャーナルに記録されたデータに基づいて分岐します。 分岐を選択する条件は、トリガーイベントまたはステップの結果から導出する必要があります。そうすれば、リプレイのたびに同じ経路が選ばれます。
ステップ外のコードはリプレイのたびに再実行されます。 純粋かつ軽量な処理にとどめ、副作用はすべてステップ内に置きます。ステップ外の console.log はリプレイごとに出力されます。一度だけ出力したい行には、terse-sdk の await log(...)(ジャーナルに記録されるステップ)を使用します。
実例
どちらの例でも、メソッド名と定数名はプロジェクトの生成ファイルに由来します。決して独自に作らないでください。
Durable
以下のジョブは、目標とする durable な構成を示しています。ハンドラーを先頭に置いて順次実行されるブロックとして記述し、副作用のある分岐ヘルパーを網羅的な switch で振り分け、スキーマと型は末尾に置きます。
import { createJob, generateText, slack, sleep, waitForInput } from "terse-sdk"
import { z } from "zod"
import { Triggers, LinearTeam, SlackChannel, toolbox, type LinearIssueCreatedTrigger } from "./terse.generated"
createJob({
name: "Triage inbound bug reports",
triggers: [Triggers.linear.onIssueCreated({ team: LinearTeam.Support })],
durable: true,
filter: async event => !event.issue.title.startsWith("[test]"),
onTrigger: async event => {
const classification = await generateText({
prompt:
`Classify this bug report as critical (data loss, security, outage) or routine. ` +
`Explain your reasoning in one sentence. Context: ${event.formatForAgentRunner()}`,
skills: [],
outputSchema: Classification,
})
switch (classification.severity) {
case "critical":
return escalateCritical(event, classification)
case "routine":
return fileRoutine(event, classification)
default:
throw classification.severity satisfies never
}
},
})
async function escalateCritical(event: LinearIssueCreatedTrigger, classification: Classification): Promise<void> {
await toolbox.slack.sendMessage({
channelId: SlackChannel.OnCall.channelId,
message: `Critical bug: ${event.issue.title} — ${classification.reason}`,
thread_ts: "",
blocks: "",
})
const approval = await waitForInput({
via: slack({ channel: SlackChannel.OnCall.channelId }),
prompt: "Page the on-call engineer?",
details: { issue: event.issue.title, reason: classification.reason },
options: [
{ id: "page", label: "Page now" },
{ id: "hold", label: "Hold until morning" },
],
})
if (approval.choice === "hold") {
await sleep("8h")
}
await toolbox.linear.updateIssue({ issueId: event.issue.id, priority: 1 })
}
async function fileRoutine(event: LinearIssueCreatedTrigger, classification: Classification): Promise<void> {
await toolbox.linear.createComment({
issueId: event.issue.id,
body: `Auto-triaged as routine: ${classification.reason}`,
})
}
const Classification = z.object({
severity: z.enum(["critical", "routine"]),
reason: z.string(),
})
type Classification = z.infer<typeof Classification>
Non-durable
non-durable ジョブの完全な例です。決定論的な投稿、エージェントによる要約、スレッドへの決定論的な返信を行います。
import { createJob, generateText } from "terse-sdk"
import { Triggers, Skills, Repos, SlackChannel, toolbox, type GithubPROpenedTrigger } from "./terse.generated"
createJob({
name: "Summarize PR and notify Slack",
triggers: [Triggers.github.onPROpened({ repo: Repos.MyOrg.MyRepo })],
filter: async (event: GithubPROpenedTrigger) => {
return !event.sender.login.includes("[bot]")
},
onTrigger: async (event: GithubPROpenedTrigger) => {
// Deterministic: fixed channel, fixed opener — use toolbox, no agent needed
const message = await toolbox.slack.sendMessage({
channelId: SlackChannel.Engineering.channelId,
message: `New PR from ${event.sender.login}: ${event.pullRequest.title}`,
thread_ts: "",
blocks: "",
})
// Agentic: only the summary needs judgment
const summary = await generateText({
prompt:
`Summarize the changes in this PR. ` +
`Focus on what changed, why it matters, and what reviewers should look at first. ` +
`Keep it concise. Context: ${event.formatForAgentRunner()}`,
skills: [Skills.github({ repos: [Repos.MyOrg.MyRepo] })],
})
// Deterministic: post the result back in thread
await toolbox.slack.sendMessage({
channelId: SlackChannel.Engineering.channelId,
message: summary,
thread_ts: message.message_ts,
blocks: "",
})
},
})