mcpField
T
terse-create

terse-create

github.com/terseai生産性39 回インストール
リポジトリ

terse-create について

terse-createは、Terseのジョブとして自動化ワークフローを構築するスキルです。生成済みの連携定義を調べ、設計、実装、テストを進め、必要に応じてデプロイする手順を案内します。

主な機能

  1. 01ワークフロー設計と実装の案内
  2. 02生成済み連携定義を正とする調査
  3. 03連携設定の案内
  4. 04テストとデプロイの手順
  5. 05進捗の説明とテスト結果の報告

活用シーン

  1. 01指定された自動化向けのTerseジョブ作成
  2. 02利用可能な連携トリガーやツールの確認
  3. 03サンプルイベントによるワークフローテスト
  4. 04完成したTerseワークフローのデプロイ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/terseai/terse --skill terse-create

インストール後、Claude Code で/terse-createを入力して読み込まれたか確認します。

同梱ファイルreferences/research-integrations.mdreferences/research-workspace.md

詳細

簡潔なワークフローを作成する

次の内容に基づいて、Terse ワークフローを作成します:$ARGUMENTS

ワークフローとは、自動化する対象です。コードではジョブとして定義されます(src/terse.jobs.ts の createJob)。

ワークフローの構築方法を学ぶ

Terse プラットフォームについて把握するには、次の情報源を参照してください。

  1. プロジェクト自体にコードがある場合は、そのコード(src/terse.jobs.ts、src/jobs/)
  2. 生成されたファイルを主要な信頼できる情報源として、Terse の仕組みを把握してください。src/terse.generated.ts がエントリーポイントです。そのヘッダーには、利用可能なすべての連携が記載されており、src/terse.generated/ フォルダーの行番号付き索引もあります。このフォルダーには、連携ごと、種類ごとに 1 つのファイルが格納されています(トリガーファクトリーとペイロード型の <integration>.triggers.ts、型付きツールメソッド、リソース、スキルの <integration>.tools.ts、attio.schemas.ts などのワークスペーススキーマファイル、および common.ts の共有処理)。索引を使って、ワークフローに必要なファイルだけを開いてください。生成されたファイルから、存在する機能とその正確な形を確認できます。生成されたファイルと最新のドキュメントの内容が異なる場合は、生成されたファイルを優先してください。ヘルパーが見つからない場合は terse generate を再実行し、生成されたファイルで裏付けられていないドキュメントの記述を前提にコードを書かないでください。
  3. terse CLI(terse --help)
  4. 最後の手段として、最新の 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 によってシークレットが露出したりリスクが高まったりする場合は、適切に別の方法へ切り替えてください。ユーザーが行う必要のある最小限の手動操作を説明し、その後は自動的に続行してください。
  • 以下の確認ルールとテスト安全ルールはすべて守ってください。コンピューター操作によって変わるのは操作する画面であり、許可の範囲ではありません。

手順

作業の進行を伝えてください。 作業そのものについて、カジュアルな一人称でユーザーに話しかけます。「プロジェクトがセットアップされているか確認します」「このユースケースについて、もう少し理解する必要があります」「これを実現する方法を調べるため、いくつかのリサーチエージェントを起動します」「調査結果を踏まえて、あといくつか質問があります」のように伝えます。以下の手順はフェーズに分かれています。ブートストラップ(ステップ 0)、ヒアリング(1)、調査(2)、認識の共有(3)、設計(4~8)、構築(9)、切り替えと検証(10~11)、デプロイ(12)です。ステップ番号は内部用で、口に出してはいけません。ただし、フェーズは伝えてください。フェーズが切り替わるたびに、どのフェーズが終わり、次はどのフェーズなのかを、全体の流れの中での位置とあわせて、気取らない言葉で伝えます。「調査は完了しました。今回の構築では、これで 5 フェーズ中 1 つ目です。次は設計について合意します」のように伝えてください。ユーザーが文章だけで作業の進捗を常に把握できるようにします。

ハーネスにユーザーから見えるタスク追跡ツール(ToDo/タスクリスト)がある場合は、チェックリストも作成します。ステップ 0 の直後に、カジュアルな言葉で項目を作成してください。項目は「構築方法を調査する」「設計について合意する」「ジョブを構築する」「テスト対象を切り替えて検証する」「デプロイについて尋ねる」です。各フェーズの完了時に項目にチェックを入れます。チェックリストは、口頭で伝えるフェーズの切り替えを補足するものであり、その代わりにはなりません。

すべての CLI 実行にマーカーを付けてください。 terse CLI による操作は、ユーザーが確認できる必要があります。ハーネスではツール呼び出しが折りたたまれることが多いため、その実行をナレーションで伝えます。各 terse コマンドの直前に、マーカー行を出力してください。

⏵ terse test run pr-triage --id 3f2c — サンプルの PR イベントでジョブを実行します

コマンドごとにマーカーを 1 つ付けます。バッククォート内にコマンドをそのまま記載し、続けて実行理由を短く添えます。連続する、コストのかからない読み取りコマンド(terse test list、terse test show)は、1 つのマーカーにまとめてもかまいません。状態を変更するコマンドや時間のかかるコマンド(integrate connect/wait、generate、deploy、test run)が完了したら、通常のナレーションで結果を伝えます。ユーザーに実行が見えないパイプライン、サブシェル、スクリプトの中に terse の呼び出しを隠してはいけません。

テストの実行前に、すべて説明してください。 terse test run または terse replay の場合、マーカー行だけでは不十分です。実行の直前に、何をテストするのか、なぜ今実行するのか、どのような結果が望ましいのかを、カジュアルな文章で 1~3 文で説明してください。「これからサンプルの PR イベントをジョブに送ってみます。エラーなく、テスト用チャンネルにトリアージの概要が投稿されれば成功です」のように伝えます。実行が完了したら、最初に伝えた期待に照らした判定を、同じ口調で伝えてください。コストのかからない読み取り(terse test list、terse test show)には、マーカーだけを付けます。

terse test run が TERSE TEST REPORT を出力した場合は、その内容をすぐにユーザーに伝えてください。最初の文は、「テストは期待どおりに実行されました」または「テストは期待どおりに実行されませんでした」のどちらかにします。その後、報告された副作用を正確に列挙し、最初に伝えた期待との不一致があれば説明します。

ナレーションを作業の妨げにしてはいけません。告知の後に返答を待ってはいけません。各手順で定められた質問をするときだけ、作業を一時停止します。

ヘッドレス実行。 回答するユーザーがいない場合は、手順に定められた質問をすべて省略し、推奨する回答を採用します。選択した内容とその理由を、最終的な要約でそれぞれ説明してください。ナレーションも通常どおり、作業と並行して行います。

0. Terse プロジェクトが存在することを確認する

作業ディレクトリに terse.config.json または src/terse.generated.ts があるか確認します。どちらかが存在する場合、プロジェクトはセットアップ済みです。ステップ 1 に進みます。

ユーザーが Terse Cloud でワークフローを構築するのではなく、Terse をセルフホストしたいと依頼した場合は、別のフローです。terse-self-host スキルに引き継ぎます。

どちらのファイルも存在しない場合は、新規プロジェクトとして terse init で Terse Cloud を利用する SDK プロジェクトをブートストラップします。Terse Cloud はコントロールプレーンとデータプレーンをホストします。 terse init を実行してください。 事前確認は不要です。何かを始める前に、環境を「確認」するための find、ls、terse --version、terse integrate list は実行しないでください。そのまま terse init を実行し、表示された内容に対応してください。CLI は既存プロジェクト、認証の不足、使用済みのディレクトリ名、package.json の不足を検出できます。エラーメッセージを信頼して、そこから復旧してください。

terse init <project-name>   # scaffold into a new subdirectory
terse init                  # scaffold into the current directory (must be empty of npm files)

ユーザーがプロジェクトディレクトリを指定した場合は、それを渡してください。指定がない場合は、現在のディレクトリにスキャフォールドを作成してください。

Bash のタイムアウトを 600000(10 分)に設定してください。依存関係のインストールとブラウザー経由の認証には、どちらも時間がかかることがあります。

terse init は認証を自動で処理します。ユーザーがログインしていない場合は、ブラウザーで WorkOS を開き、ユーザーが認証するまで待機します。実行前に、ブラウザーのタブが開くことがあり、そこでのサインインが完了するまでセットアップが一時停止することを、気軽にユーザーに伝えてください。CLI がログイン URL を表示し、ユーザーが困っているようであれば、クリックできる代替手段としてその URL を伝えてください。SSH 環境やコンテナー内では、自動でブラウザーを開く処理が何も表示せずに失敗することがあります。単一のコマンドで、ファイルのスキャフォールド作成、依存関係のインストール、必要に応じたログイン、リモートの Terse プロジェクトの作成、terse.config.json の書き込み、terse generate の実行が行われます。

エラーに対応してください。 terse init が失敗した場合は、エラーを読み、次のように復旧してください。

  • command not found: terse:npm i -g terse-cli(または、ユーザーの希望に応じて pnpm add -g terse-cli/yarn global add terse-cli)で CLI をインストールしてから、terse init を再試行してください。
  • Detected an existing npm project in this directory:このディレクトリには package.json があります。terse.config.json または src/terse.generated.ts も存在する場合、ユーザーはすでにセットアップ済みです。そこで停止し、再度初期化せずに、ステップ 1 に進んでください。それ以外の npm プロジェクトの場合は、terse attach(既存のプロジェクトに Terse を追加)を推奨するか、プロジェクト名を指定して再実行し、隣接するディレクトリにスキャフォールドを作成してください。
  • Directory "X" already exists:別のプロジェクト名を選ぶか、ユーザーに既存のディレクトリへ cd してもらい、そこから評価してください。
  • ACTION REQUIRED: Not authenticated:有効な保存済み認証情報がない場合の、非対話モード/CI モードでのみ発生します。タイムアウトを 10 分に設定して Bash から terse auth login を実行し、その後 terse init を再試行してください。
  • Failed to create Terse project:API キーは有効ですが、リモートでの作成に失敗しています。ローカルのスキャフォールドは残っています。エラーをユーザーに伝え、接続を修正してから再試行するか、ダッシュボードでプロジェクトを作成して terse.config.json を手動で記述するよう提案してください。

続行してください。 terse init が成功すると、最後に「Next steps」ブロックが表示されます。これをそのまま繰り返さないでください。要求されたワークフローに必要なインテグレーションを接続します。まず terse integrate describe <type> --json を実行して、インストールがフォーム形式か OAuth 形式か、また必要なフィールドを確認してから、ステップ 7 の手順に従って terse integrate connect <type> を実行してください。その後、ステップ 1 に進み、ユーザーが求めたワークフローを構築してください。

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 を実行すると、1 つのツールの入力/出力スキーマが返されます。ツールが実際に返す内容を確認するには、terse integrate tool run <tool> --params '<json>' を実行すると、1 回だけ実行できます。テスト規約の「CLI からのツール呼び出しのテスト」を参照してください。 既存のプロジェクトに src/terse.generated.ts がない場合は、ヘルパーを独自に作る前に terse generate を実行してください。ファイルは存在するものの、必要なヘルパーが公開されていない場合や、ヘッダーのインデックスと実際にリーフファイルに含まれている内容が一致しない場合は、terse generate を再実行してください。生成されたファイルを直接編集してはいけません。

1. 最初のヒアリング

調査を始める前にユーザーにヒアリングしてください。ユーザーの回答によって、ステップ 2 のワークスペースのスキャンと調査担当者の方向性が決まります。

決定事項と意図についてのみ質問してください。何を、どのイベントで、誰のために実行し、どのような結果が望ましいかを確認します。実装方法については質問しないでください。何が接続されているか、どのトリガーが存在するか、ドキュメントが何をサポートしているか、外部 API が何を提供しているかは、ステップ 2 のスキャンと調査担当者が取得する情報です。依頼から意図がすでに明確に定まっている場合は、そのままステップ 2 に進んでください。

関連する質問をまとめ、1 回のやり取りにつき最大 4 つまで質問してください。すべての質問に、推奨する回答を添えてください。

ヒアリング中は、ドメインで使われる用語を明確にしてください。

  • ユーザーが曖昧な用語や複数の意味を持つ用語を使った場合は、より正確で標準的な用語を提案してください。「『アカウント』とおっしゃっていますが、Customer と User のどちらを指していますか?この 2 つは異なるものです。」
  • 具体的なシナリオを使ってドメイン上の関係を検証し、境界となるケースを掘り下げ、概念間の境界を正確に定められるようにしてください。
2. ワークスペースをスキャンしてから、リサーチャーを割り当てます

ワークスペースのスキャンは必ず自分で行い、サブエージェントには任せないでください。 ステップ 9 でジョブを記述するには、正確な型とシグネチャを把握しておく必要があります。この読み取りを委任すると、無駄な作業が増えるだけです。次のファイルを最初から最後まで読みます。

  1. src/terse.generated.ts(ルートファイル)。ヘッダーには現在 Terse で利用可能なすべてのインテグレーションが記載されており、src/terse.generated/ フォルダーの行番号付きインデックスも含まれています。その下の再エクスポートには、接続済みのものが反映されています。
  2. src/terse.jobs.ts(またはプロジェクトのカスタムエントリーファイル、あるいはレガシーの src/index.ts)—このワークフローと重複または競合する既存のジョブがないか確認します。

次に、インデックスをたどって必要なリーフファイルを選択して読みます。このワークフローで使用するすべてのインテグレーションについて、コードを記述する前に terse.generated/<integration>.triggers.ts と <integration>.tools.ts を最初から最後まで読みます。ステップ 9 で正確なシグネチャを把握しておく必要があるためです。ワークフローに関係しないインテグレーションのファイルだけを読み飛ばします。インデックスの内容が実際のリーフファイルと一致しない場合は、バンドル全体が古いものとして扱い、terse generate を再実行します。

スキャン結果から、このワークフローに該当する正確なトリガーファクトリー(Triggers.*)、スキルファクトリー(Skills.*)、toolbox.<integration>.<method> のシグネチャ、リソース定数を引き継ぎます。生成ファイルに文字どおり記載されている名前だけを使い、名前を作り出してはいけません。また、不足しているものも引き継ぎます。各不足項目を、利用可能なインテグレーションに関するコメントに照らして分類します。一覧にはあるものの terse.generated/ に生成ファイルがないプラットフォームは、接続可能な組み込み機能です(terse integrate tool <type> --json でそのツールを確認できます)。一覧にないプラットフォームには Terse の組み込み機能がありません。その場合は、以下のリサーチャーに任せます。

リサーチャーは、references/ にある自己完結型のテンプレートで定義された、対象を絞った読み取り専用のバックグラウンド調査タスクです。それぞれ決まった形式の調査概要を作成します。

  • インテグレーションリサーチャー(research-integrations.md)—必要なサービスに Terse の組み込みインテグレーションがないことをスキャンで確認した場合に限り、割り当てます(規約セクションのインテグレーションラダーの段階 3〜4)。プラットフォームの公式 SDK、認証モデル、主要なエンドポイントを調査します。

リサーチャーの割り当て方。 各リサーチャーについて、次の手順に従います。

  1. references/ から、そのテンプレートファイルを読みます。
  2. 「オーケストレーターからのコンテキスト」というコメントブロックを実際の内容に置き換えます。ワークフローを 1 段落で説明し、関係するプラットフォーム、イベント、アクション、および調査範囲を絞り込むインタビューの回答とスキャン結果を記載します。テンプレートに「Objectives」ブロックがある場合は、この実行で回答が必要な具体的な質問を記入します。リサーチャーは、必要な情報を確定するために最小限の取得で目的に回答するため、目的が曖昧だと必要以上に読み込むことになります。
  3. ハーネスでサブエージェント(Agent/Task ツール)を起動できる場合は、記入済みテンプレート全体をサブエージェントへのプロンプトとして渡し、読み取り専用でウェブを利用できるエージェントタイプを使用します。テンプレートは自己完結型なので、サブエージェントに他のコンテキストは不要です。返答は調査概要になります。
  4. 起動できない場合は、記入済みテンプレートに自分で従って調査し、次に進む前に同じ形式の調査概要を作成します。

ハーネスで可能な場合はバックグラウンドで実行します。 スキャン結果から必要性が確認できたら、すぐにインテグレーションリサーチャーを割り当てます。ステップ 3 の実施中に調査を進められます。インラインで実行する場合は、先にテンプレートに従って調査します。

リサーチャーは読み取り専用です。ファイルを編集したり、状態を変更するコマンド(terse integrate connect、terse generate、terse deploy)を実行したりしてはいけません。

3. 共通認識を形成する

ワークスペースの調査結果をリサーチ概要と組み合わせ、設計に影響する内容をすべてユーザーに伝えます。

  • リサーチで明らかになった判断事項:接続または回避策が必要な未対応の統合、妥当なトリガーの選択肢が2つあること、ワークフローの別の形を示唆する機能がリサーチで見つかったこと。
  • ユーザーの要望と既存の内容との矛盾。表面化させ、黙って解決してはいけません。

同じインタビューのルールが適用されます。質問は一度に最大4つまでにし、それぞれの回答を推奨します。質問する代わりに、事実を調べます。設計ツリーの各分岐を順にたどり、判断事項間の依存関係を一つずつ解決します。

判断事項が解決したら、ワークフローを ASCII 図で描きます。各ステップを1つのボックスで表し、タイトルと重要な詳細を1〜2点記載します(判断が必要な箇所には agent: と記します)。トリガーを上部に、副作用を末尾に配置します。判断事項はボックスではなく接続線上に記します。フィルターは接続線のラベル、停止は幹線から分岐する ├─► の出口、実際の分岐は並列に配置して幹線に合流する2つのボックスで表します。コード識別子ではなく、インタビューで使ったドメイン用語を使い、ターミナルに収まる幅にしてください。

┌──────────────────────────────────────────────┐
│ Attio deal record updated                    │
│ object: Deals                                │
└──────────────────────┬───────────────────────┘
                       │ filter: stage = "Negotiation"
                       ▼
┌──────────────────────────────────────────────┐
│ Does #war-<deal-name> already exist?         │
└──────────────────────┬───────────────────────┘
        already exists ├─► stop, the war room is already up
                       ▼ new deal
┌──────────────────────────────────────────────┐
│ Create private channel #war-<deal-name>      │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ Resolve the invite list                      │
│ fixed group: Olivier + Thomas                │
│ deal owner matched to Slack by email?        │
└──────────┬───────────────────────────┬───────┘
           │ matched                   │ unmatched
           ▼                           ▼
┌─────────────────────────┐  ┌─────────────────────────┐
│ Add the owner to the    │  │ Invite the fixed group  │
│ invite list             │  │ and post a heads-up     │
└────────────┬────────────┘  └────────────┬────────────┘
             │                            │
             └──────────────┬─────────────┘
                            ▼
┌──────────────────────────────────────────────┐
│ Invite the deal team to the channel          │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ agent: write the deal brief                  │
│ pull value, stage, contacts from Attio       │
│ post and pin the brief in the channel        │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ Schedule the kickoff call                    │
│ location: Google Meet                        │
│ invites emailed to the deal team             │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ Post the calendar link in the channel        │
└──────────────────────────────────────────────┘

図の作成は設計に関する質問とは別のステップです。まず質問をまとめて行い、残りの判断事項をすべて解決してから図を描いてください。図は確認を求める文章(「これはご希望の内容と合っていますか?」)と一緒に、独立したメッセージとして送信し、そのターンをそこで終えてください。そのターンではツールを呼び出してはいけません。質問ダイアログが画面を占有し、同じターンのテキストが隠れてしまううえ、小さなターミナルではプレビューペインに長い内容が収まらないためです。この確認に質問ツールは使用しません。図は共通認識を可視化したものであり、仕様はそれに基づいて実装されます。ユーザーが実際に図を確認し、承認するまで実装を開始してはいけません。

4. エントリーファイルを開く

src/terse.jobs.ts(標準のジョブエントリーファイル)を開きます。CLI は従来のフォールバックとして src/index.ts も読み込めます。また、カスタムレイアウトでは --entry-file を使ってエントリーファイルを上書きできます。

リポジトリですでにカスタムエントリーファイルを使用している場合は、そのレイアウトに従い、以降の terse コマンドに --entry-file を指定します。 ジョブの配置は規約セクションのプロジェクトレイアウト規則に従います。各ジョブは src/jobs/<kebab-case-name>.ts に個別のファイルとして配置し、src/terse.jobs.ts は副作用を伴うインポートのマニフェストです。新しいジョブは src/jobs/ に独立したファイルとして作成し、そのインポート行を追加します。リポジトリでジョブを src/terse.jobs.ts 内に直接記述している場合は、そのジョブを編集する際に src/jobs/ 内の独立したファイルに移動します。 リポジトリに src/index.ts しかない場合は、競合する2つ目のエントリーファイルを作成せず、従来のフォールバックとして扱います。 ランタイムエントリーがまだない場合は、作成します。

import { createJob, generateText } from "terse-sdk"
5. 永続性を決める

永続性によってハンドラーの構成が変わるため、ハンドラーのコードを書く前に決めてください。確定した設計を、規約セクションの2種類のシグナル(「永続化が必要な場合」)に照らして確認します。

強制シグナルがある(設計上、人の入力または時間を待つ):永続性は選択事項ではないため、尋ねないでください。結果として1行で伝え、次に進みます。「このジョブは <承認/タイマー> を待つため、永続的に実行されます。」

判断シグナルのみがある(副作用のあるステージが3つ以上あり、待機がない):これは実際にトレードオフがあるため、推奨案をデフォルトとして示し、確認してください。次の文面を使用します。

このジョブを永続化しますか?

  • 永続化する — 再起動後も継続でき、一時停止して待機できます。例:「500ドルを超える返金が届いたら、Slackでマネージャーの承認を待ってから処理する。」ジョブは状態を失わずに数日間待機できます。
  • 永続化しない(デフォルト)— クラウド内のクロージャーです。例:「PRが作成されたら、Slackに要約を投稿する。」制約なしで一度だけ高速に実行されます。クラッシュしても、その実行が失敗するだけです。

トレードオフ:永続化するには、すべての副作用を step() 内に置き、すべてのステップデータをシリアライズ可能にする必要があります。

このワークフローでの推奨:<推奨案とその理由を1行で記述>

どちらのシグナルもない:ジョブは永続化しません。確認も告知もしないでください。

永続化するジョブでは、規約セクションのスタイルルール(「永続ジョブのスタイル」)に従ってハンドラーを作成してください。仕組み(step()、jobStep、sleep、waitForInput、リプレイモデル)については、https://docs.useterse.ai/core-concepts/durability を参照してください。

6. トリガーを選ぶ

ジョブが応答するイベントに基づいてトリガーを選択してください。ワークスペースのスキャンで候補が見つかっています。トリガーファクトリーとリソース定数を ./terse.generated からインポートしてください。

生成されたファイルに実際に存在するトリガーとリソースのみを使用してください。そこに定義されていない定数を作り出さないでください。

ユーザーが現地時間でスケジュールを指定した場合は、そのタイムゾーンを維持してください。Triggers.schedule.cron には任意の timezone("America/New_York" のような型付きの IANA 名。Timezone ユニオンから補完されます)を指定でき、式はそのゾーンの壁時計時刻として、夏時間を考慮して評価されます。ユーザーの時刻を手作業で UTC に変換しないでください。timezone を省略すると UTC になります。

7. スキルを選択してインテグレーションを接続する

スキルによって、generateText の実行中にモデルができることが決まります。 スキルは、モデルが選択できるツールの範囲を限定します。toolbox.* は範囲が限定されておらず、スキルがなくても動作します。

目安:

  • 手順が完全に決定論的な場合は、toolbox.<integration>.<method> を直接呼び出します。スキルは必要ありません。
  • generateText の実行中にモデルがインテグレーション上のアクションを選択する必要がある場合、そのインテグレーションを skills に含める必要があります。

ワークフローでプラットフォームが必要な場合、手順 2 では規約セクションの「プラットフォームとのインテグレーション」にあるインテグレーションの選択手順をすでに確認しています。ワークスペースのスキャンで、何が接続されているか、組み込みの型があるかが確定し、インテグレーションブリーフで外部の選択肢が確認できます。該当する選択肢を確かめ、最初に機能するものを採用します。コミュニティラッパーを選ぶ場合は、ブリーフの根拠を示してユーザーに選択してもらいます。

不足している組み込みインテグレーションの型を接続する場合:

  • 接続する前に、必ず説明を確認します。 最初に terse integrate describe <type> --json を実行します。これにより installationType(フォームまたは OAuth)が返されます。また、フォームでインストールする場合は、接続コマンドに必要な正確なフィールド(各フィールドの名前、型、必須かどうか、ヒント)と、ユーザーが先にコンソールで作業する必要がある場合はそのセットアップ URL も返されます。フィールド名を推測してはいけません。
  • フォームでインストールする場合は、describe の出力にある必須フィールドすべての値を用意します。導き出せない値はユーザーに尋ね、フィールドのヒントとセットアップ URL も伝えます。その後、terse integrate connect <type> --field key=value --fields-stdin を実行します。
  • シークレットは --field ではなく --fields-stdin に渡します。
  • OAuth でインストールする場合、ユーザーに情報を伝えるのはあなただけです。--json モードでは、CLI 自身の「ACTION REQUIRED」行が抑制され、ブラウザーの起動もベストエフォートです(SSH またはコンテナー内では、失敗しても通知されません)。そのため、次の順に進めます。
    1. まずユーザーに、これから何が起こるか、ユーザーが操作するまで実行が一時停止することを、気軽に伝えます。「今から Gmail を接続します。ブラウザータブが開くので、そこでサインインを完了してください。その後、こちらで続きから再開します。」
    2. terse integrate connect <type> --json を実行します。ブラウザーが開き、認証用の url と waitCommand を含む handoff ペイロードを返して、終了コード 2 で終了します。
    3. タブが開かなかった場合に備え、ペイロードの url をクリック可能な代替リンクとしてユーザーに渡します。「タブが開かなかった場合はこちらのリンクを使ってください:」
    4. waitCommand(例:terse integrate wait gmail)を実行し、ユーザーが認証を完了するまで待機します。接続が有効になると終了コード 0 で終了します。その後でのみ続行します。
  • 複数の OAuth インテグレーションが必要な場合は、一度に 1 つずつ進めます。告知 → connect <a> --json → wait <a> → 告知 → connect <b> --json → wait <b> の順です。接続コマンドをまとめて実行してはいけません。ユーザーは一度に 1 つのブラウザータブしか認証できません。
  • 接続または更新の後は、terse generate を再実行し、ヘッダーのインデックスと、このワークフローが使用するインテグレーションのリーフファイルを読み直します。

組み込みの選択肢より先に進む場合、認証情報については、規約セクションの「プラットフォームとのインテグレーション」にある認証情報のルールに従います。

認証情報の取得に手動のコンソール作業(サービスアカウントの作成、キーの生成、リソースの共有など)が必要な場合は、ユーザーが実際に操作する時点で、インテグレーションブリーフのセットアップ手順を提示します。早すぎるタイミングで提示したり、簡略化したりしてはいけません。番号付きの手順を、リンクとロール/スコープの付与内容をそのまま保って伝えます。曖昧な手順に言い換えてはいけません。認証情報のルールに従い、各シークレットについて正確な terse secrets add コマンドを追加します(ファイル認証情報の場合は base64 のワンライナーを使います)。ブリーフで (unverified) とマークされた手順には、そのマーカーを付けたまま伝えます。これにより、ユーザーがリンクやロール名をうのみにせず、再確認できます。

8. フィルターを検討する

特定のイベントをスキップする必要がある場合は、filter 関数を追加します。

  • ボットが生成したイベント(event.sender.login.includes("[bot]"))
  • ドラフト PR、WIP 項目、または特定のラベル
  • 特定のユーザーやリポジトリからのイベント

フィルターを使うと、不要なエージェントの実行を防ぎ、コストを節約できます。

9. ハンドラーを作成する

このファイルの末尾にある「簡潔なジョブコードの規約」セクションは、以下のハンドラーコードのすべての行に適用されます。正確な名前とシグネチャは、設計についての議論の記憶からではなく、ステップ 2 で生成ファイルを確認した際の内容に基づいてください。スキャン後に terse generate が再実行された場合に限り、ファイルを再度開いて確認してください(ステップ 7 では、インテグレーションの接続後に再実行します)。

規約セクションにある実例(永続的なものと非永続的なもの)が、目指すべき形を示しています。

作業の種類に応じて、使用するプリミティブを選びます。

  • 決定的な作業 — 既知のアクションをそれぞれ toolbox に割り当てます(エージェントは使用しません)。

    import { toolbox, SlackChannel } from "./terse.generated"
    
    await toolbox.slack.sendMessage({
        channelId: SlackChannel.Engineering.channelId,
        message: `New PR: ${event.pullRequest.title}`,
        thread_ts: "",
        blocks: "",
    })
    
  • エージェント的な作業 — 判断が必要な箇所(要約、トリアージ、下書き作成)では generateText を使用します。event.formatForAgentRunner() を使ってイベントのコンテキストをすべて含め、具体的なプロンプトを記述し、outputSchema(Zod スキーマ)を渡して、型付けされ検証済みのオブジェクトを取得します。run() を使ったストリーミングや、同じエージェントインスタンスを複数の呼び出しで再利用する場合に限り、TerseAgent.create() を使用します。

  • ユーザーのリクエストが完全に決定的な場合(例:「Y が発生したら X を Slack に投稿する」)、エージェントを作成しないでください。

テスト対象。 最初のテスト実行の前に、ジョブが外部の場所に書き込む場合は、ユーザーに一度だけ次のように尋ねます。「テスト実行にはどのチャンネル/リポジトリを使えばよいですか?(最後に実際の対象に切り替えます。)」関係する API に利用料金が発生する、または顧客に見える影響がある場合は、同じ質問の中で、作成時に使用できるテストモード用のキーがあるかも尋ねます(terse secrets add <NAME>)。作成中は、副作用のある呼び出しをそれらのテスト用リソースに向けてください。ユーザーが実際のリソースを使うよう希望した場合は、本番環境で進めます。このファイルの末尾にある「テストの安全に関する規約」セクションは、実データ、テストキー、本番環境への書き込み、調査を含むすべてのテスト実行に適用されます。

ジョブをローカルで実行する。 terse test は、保存済みのトリガーイベントを実際のハンドラーで再生します。

terse test list "<job-name>" --json
terse test show <id> "<job-name>" --json
terse test run "<job-name>" --id <id>

terse test list は保存済みのサンプルイベントを一覧表示し、terse test show はそのうちの 1 つを出力します。terse test run --id <id> はそのイベントを実行します。--event-file <path> または --event <json> を指定すると、用意したペイロードを実行します。ジョブが複数ある場合は、ジョブ名を明示的に指定してください。非対話型のジョブ読み込みでは、プロンプトを表示できません。TTY を使った手動セッション以外では、ジョブ名を指定しない terse test の実行は避けてください。

サンプルバッファーが空の場合(新しい Webhook には保存済みの配信がありません)、イベントファイルを手動で作成します。形式が一致しない場合、terse test run は期待されるエンベロープと正確な検証エラーを出力します。フィールドを推測するのではなく、そのエラー出力をもとにファイルを修正してください。

終了コード 0 が意味するのは、ハンドラーが実行されたということだけです。エージェントがテキストを生成した場合は、出力を読み、プロンプトの意図に沿っているか判断してください。

実装が確認済みの設計から逸脱した場合は、何が変わったのか、なぜ変わったのかを伝えてください。返事を待つ必要はありません。チェックリストが有効な場合は、該当項目にチェックを付けてください。

10. 実際の対象に切り替える

ステップ 9 で使用したテスト用リソースとテストモード用シークレットをすべて列挙し、1 つずつ実際のものに戻してください。漏れがないよう、すべてを明示的に列挙します。

実際の本番データを使って最後にもう一度実行するか、ワークフローをデプロイするだけにするか、ユーザーに尋ねます。

この実行では実際の副作用が一度発生します。実行する前にユーザーに伝えてください。

11. 最終確認

間違いやすい点を確認してください。

  • pnpm exec tsc --noEmit が成功すること(プロジェクトに合わせて npx tsc --noEmit を使ってもかまいません)。
  • Triggers.<integration>.…、Skills.<integration>(…)、リソース定数、ツール呼び出しがすべて生成されたファイルに存在すること。存在しない定数を作ってしまうのが、最もよくある失敗です。
  • 既知の呼び出しには toolbox を使い、判断が必要なステップにのみ generateText を使うこと。TerseAgent.create() は、ストリーミングまたはエージェントの再利用が必要な場合にのみ使います。
  • skills に、generateText の実行中にモデルが呼び出す必要のあるすべての連携機能が列挙されていること。
  • エージェントのプロンプトに、event.formatForAgentRunner() を介してイベントのコンテキスト全体が含まれていること。
  • インポートが terse-sdk および ./terse.generated の実在するエクスポートを参照していること。
  • このコードが規約セクションに従っていること:不要なキャストがない、網羅的な switch 文を使う、独自のエラークラスを作らない、ステップダウン順序に従う、型定義を最後に置く。
  • 永続ジョブの場合:すべての副作用がステップ内にあり、分岐ヘルパーが同じファイル内のジョブより後に置かれ、ステップ境界を越えるのがシリアライズ可能なデータのみであること。
  • ステップ 9 のすべてのテスト対象が、実際のリソースに戻されていること。
12. デプロイする前に確認する

terse deploy を自動的に実行しないでください。ジョブの作成と検証が完了したら、今すぐデプロイするかユーザーに確認してください。

確認の例:

ワークフローをローカルで実行できる状態になりました。terse deploy で本番環境にデプロイしますか?(プロジェクト内のすべてのジョブが同期され、削除済みのジョブはリモートから削除されます。)

  • ユーザーが「はい」と答えた場合は、terse deploy を実行し、結果を報告します。
  • ユーザーが「いいえ」と答えた場合や、変更を希望した場合は、デプロイせずに終了し、準備ができたら terse deploy を実行できることを伝えます。

テスト時の安全規約

この規約は、ジョブをローカルで実行するすべての場合に適用されます。terse test run、terse replay、アドホックなツール呼び出し(terse integrate tool run)が対象です。ローカル実行では実際の認証情報を使って実際のハンドラーが実行されます。安全な接続先を指定しない限り、テスト実行はサンドボックス化されません。

実際のイベントを入力し、テスト用の接続先に出力する

サンプルイベント(terse test list、terse replay)は本番環境の実データです。これらを入力として使うのが目的です。現実的なペイロードによって実際のバグを発見できます。境界となるのは副作用です。テスト実行で実在の人やサービスに書き込みを行ってはいけません。メール、メッセージ、チケットの更新、CRM への書き込みは、テスト実行中、顧客、イベント内のチャンネル、またはレコードではなく、ユーザーが指定したテスト対象に送信してください。

唯一認められる例外は、実際の接続先に戻した後、ユーザーに事前に伝えてから行う 1 回の検証実行です。

テスト用 API キー

Stripe、Resend など、課金が発生する、または顧客に見える影響がある外部 API を扱う場合は、テスト用接続先についての質問に含めて、テスト用またはサンドボックス用のキーがあるかをユーザーに一度だけ確認してください。「構築中に使うテストモードのキーはありますか?ある場合は、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 で、呼び出し時にシークレットの読み込みが必要な場合は、既存のジョブ内で読み取りを実行します。

  1. 読み取りを行って結果をログに記録し、return で終わる連続したブロックを、ジョブのハンドラーの先頭に挿入します。return より下のコードは実行されません。
  2. terse test run で実行します。これにより、実際のジョブと同じようにシークレットが読み込まれます。
  3. 結果を記録してから、ブロックを削除します。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/outputSchema
  • generateText の 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(...))、順次 await には for...of を使います。async コールバックを forEach に渡してはいけません。await されずに実行され、拒否が握りつぶされるためです。

.then より async/await を使います。 .then() チェーンではなく、async/await と Promise のコンビネーター(Promise.all、Promise.allSettled、Promise.race)を使います。

エラーには extend Error し、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/ 内の独自のファイルに配置し、ジョブ名に基づく kebab-case の名前を付けます(src/jobs/triage-bug-reports.ts)。また、プロジェクトにジョブが 1 つしかない場合でも、src/terse.jobs.ts は副作用を伴う import だけを記述したマニフェストにします。

import "./jobs/triage-bug-reports"
import "./jobs/weekly-digest"

createJob() は副作用によって登録するため、エントリーファイルから import されていないジョブファイルは、実行もデプロイもされず、気付かれないままになります。src/jobs/ 内のすべてのファイルに、マニフェスト内の対応する import 行が必要です。

ジョブファイルでは、生成されたヘルパーを ../terse.generated 経由で import します。複数のジョブで共有する、純粋でステップを含まないヘルパーは共有モジュールに配置できます。ステップを含むヘルパーは、そのジョブのファイル内に置きます(下記の「ブランチはヘルパーにする」を参照)。2 つのジョブで同じヘルパーが必要な場合は、ジョブファイル間で重複して記述します。

ライブラリ

一般的な問題(再試行、日付計算、解析、検証)では、自分で実装するよりライブラリを優先します。 人気があり、保守が行き届いているものを選び、採用前にダウンロード数と最近のリリースを確認します。

プラットフォームとの連携

次の順に確認し、目的を達成できる最初の段階で止めてください。

  1. 組み込みの Terse 連携。すでに接続済み — src/terse.generated.ts とその terse.generated/ フォルダー(toolbox.*、Skills.*、Triggers.*)にあるものを使用します。

  2. 組み込みの連携タイプ。まだ接続されていない — terse integrate connect で接続してから、terse generate を再実行します。

  3. 組み込みの連携がない — 公式であることを確認したうえで、プラットフォームの公式 TypeScript SDK を使用します。公式であることは、ベンダーの npm 組織から公開されているか、ベンダーの公式開発者向けドキュメントまたは GitHub 組織からリンクされていることで確認します(例:@slack/web-api、octokit、@linear/sdk、stripe)。組み込みの型を活用します。

  4. 公式 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 secrets は TERSE_ で始まる名前を拒否します。このいずれかを使おうとしている場合、設計に誤りがあります。

スカラー認証情報(API キー、トークン)はそのまま保存します。ファイル形式の認証情報(Google サービスアカウントの JSON、PEM キー、複数行の文字列など)は、_B64 を末尾に付けた名前で base64 エンコードして保存し、プレーンテキストのまま貼り付けてはいけません。生の JSON は対話形式のプロンプトを壊し、シェルの引用符の扱いを非常に困難にしますが、base64 なら安全な 1 つのトークンになります。ファイルのパスをユーザーに尋ね、ファイルから直接エンコードすれば、平文が会話に出ることはありません。

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: true を設定する場合に適用されます。仕組み(リプレイモデル、step()、jobStep、sleep、waitForInput)については https://docs.useterse.ai/core-concepts/durability を参照してください。そちらの情報が優先されます。

永続化が必要な場合。 シグナルは2種類あり、それぞれ扱いが異なります。

  • 必須シグナル — 人による入力や承認(waitForInput)、または時間指定の待機(sleep)です。これらのプリミティブは永続モードでのみ使用できるため、存在する場合は durable: true が必須です。ユーザーに判断を委ねるものではなく、結果を伝えるだけです。
  • 判断シグナル — 副作用のあるステージが3つ以上あり、実行途中の失敗によって作業が目に見える形で未完了のまま残る場合です。これは本当に判断が必要な選択です。durable: true を推奨し、ユーザーに決めてもらいます。

どちらのシグナルもない場合は、非永続を既定とします。

インラインの step() を既定とします。 各外部呼び出しを直接ラップし、await step(client.method(args)) のように記述すると、ハンドラーを順次実行されるブロックとして読めます。簡潔な SDK 呼び出し(toolbox.*、generateText、state.get/state.set)はすでに永続ステップなので、そのままにします。

ステップ境界を越えるのはデータだけです。 引数と解決された値はジャーナルに記録されるため、シリアライズ可能でなければなりません。クロージャー、コールバック、ストリーム、実行時オブジェクトは使用できません。呼び出し先はモジュールスコープに置く必要があります。複数の呼び出しをまとめる場合や、コールバックを受け取る API を使う場合は、モジュールスコープの関数を作成し、その呼び出しをラップします。例:await step(sendWithRetry(args))。

jobStep({ inputSchema, outputSchema, run }) は、信頼境界のために使います — 永続化境界を越える値の実行時検証が必要な場合、または1つのまとまりとしてジャーナルに記録する必要があり、モジュールスコープの関数呼び出しでは表現できないブロックに限ります。

分岐はヘルパーにします。 条件付きパスにステップ(step()、jobStep、toolbox.*、generateText、sleep、waitForInput)が含まれる場合に限り、そのパスを名前付きヘルパー関数として切り出します。値の計算だけを行う条件分岐はインラインのままにします。ヘルパーは同じファイル内のジョブより後に配置します。step() は createJob() を呼び出すファイル内でのみ変換されるため、ヘルパーを別ファイルに移すと、気づかないまま動作しなくなります。こうすることで、ハンドラーを上から順に読み、必要なときだけ各分岐を開いて確認できます。

ジャーナルに記録されたデータに基づいて分岐します。 分岐を選ぶ条件は、トリガーイベントまたはステップの結果から導出し、リプレイのたびに同じ経路を通るようにします。

ステップ外のコードは、リプレイのたびに再実行されます。 純粋で軽量な処理にとどめ、副作用はすべてステップ内に置きます。ステップ外にある単独の console.log はリプレイのたびに出力されます。一度だけ出力する行には、ジャーナルに記録されるステップである terse-sdk の await log(...) を使用します。

実例

両方の例にあるメソッド名と定数名は、プロジェクトで生成されたファイルに基づいています。決して独自に作成しないでください。

永続ジョブ

以下のジョブは、目標とする永続ジョブの形式を示しています。ハンドラーは冒頭で順次実行されるブロックとして記述し、副作用のある分岐ヘルパーには網羅的な 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>
非永続ジョブ

非永続ジョブの完全な例です。決定的な投稿、エージェントによる要約、決定的なスレッド返信を行います。

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: "",
        })
    },
})

コメント