← 記事に戻るRepositories Adopting the Base in 2025
Conformance Tests by Contract Family
発見
Copy article
最後の CLI: 私のツールが構築されたベース51を再構築する
すべてのモデルが異なるライブラリセットを求めたので、私は1つのベースを構築し、すべてのプロジェクトをそれにアップストリームにしました。 そして私はクロード・フェーブルに十年後も人類がまだ使っているであろうCLIを尋ねた。そしてオプス5と私は戻ってきたものを作った — 46条項、92テスト、ゼロ依存関係。
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
私はこのマシンに51 コマンドラインツールを持っています。 その数を計画していませんでした。 それはCLIがアイデアと実際に実行できるものとの最短距離であり、過去数年間、私は一人で書くよりも速く書く手助けを得ていたために起こりました。
その手助けは、GPT-3.5と最初のClaudeモデルと作業していた頃に気づいた習慣とともに来ました。 3つの異なるモデルにCLIをスキャフォールドさせると、CLIとは何かについて3つの異なる意見が得られます。 1つはCommanderを求めます。 1つはプロンプティングのためにInquirerを求めます。 1つは出力を色付けするためにChalkを求めます。 各回答は擁護可能です。 それらは合わさって税金です、なぜなら今私は3 コードベースを所有しており、引数解析、失敗の見え方、そして今私が監視する責任があるライブラリについて意見が分かれているからです。
すべてのモデルが異なるライブラリセットを選び、私はそれらすべてを使わなければならなかった。
税金はライブラリではない。 改善が移動を止めるのはそれだ。
3 CLIがコマンドの失敗報告方法で意見が分かれると、1つの修正は1つの修正になる。 それをアップストリームに送るものはない。 作業は複利にならず、10番目のツールの後はレバレッジを構築しているわけではなく、近いミスのポートフォリオを維持しているだけだ。
その逆を望んでいると既に書いていた。 How to Turn AI Gains Into Compounding Infrastructure の全議論は、利益がすべての依存プロジェクトに継承されると耐久性が生まれるということだ。 共有機能表面。 プロモーションルール。 改善が着地し広がる1つの場所。
私はAI機能、ワークフロー、運用のためにその層を構築した。 私が実際に最も頻繁に作るもののためにそれを構築したわけではない。
そこで私は1つのベースを作り、すべてのCLIプロジェクトの改善をそれにアップストリームしました
ルールはシンプルで、私が強制するものです:私の財産のCLIがより良いものを必要とするとき — サービス登録のよりクリーンな方法、より良いエラー経路、テストヘルパーでスイートを読みやすくする — その改善はプロジェクトに残りません。 それはベースに入り、ベースは他のものに出ました。
それが全体の設計です。 ベースは意図的に小さいです。 あなたのツールが何をするかについては意見を持ちません。 コマンドが何であるかについて強い意見を持っている: 引数を取り、作業を行い、何が起こったかを報告し、終了する。
フォルダーは2025年7月6日に作成され、私のツールの2はバージョン1.0に依存していました。0 同じ日。 それが示すのは、推測的に構築されて採用されたわけではないということです。 それはすでに存在していた作業から抽出され、プロジェクト間で同じスキャフォールドをコピーすることが合理的でなくなった時点で抽出されました。
それは迅速に広がりました、広げることが全体のアイデアだったからです。 8 リポジトリは25日以内にそれに乗りました。 10 は11週間以内に。
Chart data
| repositories | |
|---|---|
| Jul 6 | 2 |
| Jul 8 | 4 |
| Jul 17 | 5 |
| Jul 23 | 7 |
| Jul 30 | 8 |
| Sep 20 | 10 |
Git はそれらのいずれよりも後に来ました。 リポジトリは2025年11月12日に初期化され、4か月後に公開されました — それがバージョン履歴と実際の履歴が不一致になる理由であり、私がこの文章を書くために座ったときにコミットログを信じるのではなくファイルシステムを確認した理由です。
それらの10ツールは Cloudflare 管理を行います。 ローカル DNS と nginx 管理。 Coolify に対するデプロイ。 ブラウザ自動化。 モデルプロバイダー横断的なコスト報告。 ほとんどが非公開であるため、名前ではなく機能で説明している。 公開されているものは aia で、複数のモデルを並列に参照し、ベース自体も含む。 vssh、私の保護されたリモート実行ツールは公開されており、同じ本能から生まれた — オペレータ表面を一度きり、正しく構築し、再構築を止める。
配当は実際にあり、退屈だったが、インフラ配当の正しい形だった。 一つのツールでのハードニングがすべてに現れた。 コマンドが赤いエラーメッセージを出力しつつ
0 で終了できることを発見したとき、ヒューマンには失敗を、シェルには成功を伝える — 修正はそのツールの 16 の場所に適用されなかった。 それはベースに入り、すべてのツールが継承した。13か月後、私はそれを修正ではなく再構築したいと思いました
2026年8月までにベースは動作しており、私はそれを消したいと思っていました。
それが壊れていたからではありません。 それが蓄積したからです。 私が最も誇りに思っていた終了コードルールが設計ではなく改造されたからです。 それが書かれた世界がその下で変わったからです:私のCLIの呼び出しのほとんどはもう私がタイプしません。 それらはエージェントによって発行され、stdout、stderr、そして
$?だけを感覚として読み取ります。したがって修正ではなく、条件を別に設定しました。 私はClaude Fableに1つの指示を与え、意図的に大きくしました:
もしこれが人類が構築した最後の CLI フレームワークであったなら — それはまだサービス中のもの
そこから設計してください。
私は文書を返してもらうとは思いませんでした。 私は計画を期待していました。 私は計画を期待していました。
ファブルは条約を持って戻ってきた、そして制約は約束が少なくなることだった
到着したのは機能リストではなかった。 それは条約として構成され、硬い壁で真ん中で分割された。
半分は契約だった:このベースに構築されたすべてのCLIがすべての観測者に保証するもの、RFC-2119言語で番号付き条項として書かれた — MUST、MUST NOT、SHOULD、MAY。12のファミリーがある。 終了コード。 ストリーム規律。 機械出力。 自己記述。 文法。 環境。 キャンセル。 決定論。 パフォーマンス予算。 互換性。
もう一方の半分は作成表面で、成長を許可され、契約を満たすことが抵抗の最小経路になるように存在した。
その下にある推論は私が説得力があると感じた部分だった。 十年持続することを意図したデザインはファッションに賭けることはできません、なぜならファッションは期限が切れるものだからです。 それは賢さに賭けることができない、なぜなら賢さはあなたが予測できないものだから、年8. それは1970たちから動かされていないインターフェースだけに賭けることができる:引数ベクトル、3 ストリーム、8-ビット終了コード、環境変数。 そしてそれは本当に新しい事実を指摘した — それらのインターフェースの大多数の読者は今やフォローアップ質問を尋ねることができない機械である。
すべてを整理した条項は、最初に開いたものだった:
1つの結果、複数のレンダリング。 コマンドは単一の結果を計算する。 終了 コード、人間のテキスト、JSON 文書、そしてストリーミング行はすべて その一つの値の投影。 彼らは互いに矛盾できません、なぜなら ただ一つのソースしかありません。
それが全体の再構築が依存する文です。
Diagram source
graph LR
A["execute() は
一つの値を返します"] --> B["終了コード"]
A --> C["レンダリングされたテキスト
stdout"]
A --> D["JSON 封筒
--json"]
A --> E["NDJSON ストリーム
--ndjson"]
F["logger.error()
ctx.emit()"] -.-> B
F -.-> G["イベント
stderr"]Opus 5 と私は、仕様が論文については正しく、3 については間違っていることを発見しました
ここで作業は私のものではなく、私たちのものになりました。
私は仕様を Opus 5 に持ち込み、私たちはそれを1日で構築しました。 きれいな日ではありませんでした。 有用な部分は、文書が遺産に合致し、失われた場所です。
仕様は
ctx.args を名前付き引数のレコードにしたいと考えていました。 それは...
より良い設計です。 それはまた、すべてのコマンドをすべて壊してしまったでしょう
10のツールのすべてで、彼らはすべて ctx.args を配列として読み取っていたからです。 私たちは配列を保持しました
そして、入力された引数を ctx.namedArgs に隣に置きました。 決定したルールは
それはすでに契約書に書かれており、1つ上の条項で:決して...
消費者はリポジトリ内の他のすべての値よりも優先され、契約の...
自身の完全性。仕様は動詞のないコマンドグループを使用エラーとしたいと考えていました。 実行する
サブコマンドのない親コマンドは
2 で終了します。 正当化でき、そしてそれは
単独のグループ化コマンドを実行するすべてのスクリプトの動作を変更し、
そのヘルプを表示します。 私たちはヘルプを表示し続け、0 で終了しました。仕様はストリーミングと単一の JSON 文書が同じ機能だと仮定していました
機能です。彼らはそうではありません。 百万個のアイテムを定数メモリでストリーミングすることは、
点は一方では可能で、もう一方では不可能です。なぜなら、呼び出し元が ...
単一文書を単一であるように求められた。 私たちは振る舞いを分割し、書き留めた
どの条項がどれを統治するか。
私たちはまた、仕様が知らなかったことを発見した、なぜならそれらはアーティファクトからしか見えなかったからだ。 テストファイルが0テストを実行し、成功を報告したが、ランナーを途中で停止させた。 Ctrl-C で
0 を終了するシグナル処理 — 成功したと報告する中断されたコマンド。 それぞれの行を文字通りのバックスラッシュ-n で結合する二つの出力ヘルパー、したがってすべてのテーブルが一行で戻ってきた。 依存関係を置き換えた後、静かに自分の型シグネチャを狭め、文字を一文字も変更していないコードを壊した色ヘルパー。その最後のものは座っている価値がある。 私たちが書いたテストでも捕捉されなかった。 それは移行中に consumer's の型チェックで浮上した、唯一それが起こり得た場所だった。
契約は、条項にテストがない場合にビルドが失敗するためにのみカウントされます
何もチェックしない約束はコメントです。
したがって、コンフォーマンススイートは契約ファイルを解析し、MUST を含むすべての条項を見つけ、いずれかに登録テストがない場合にビルドを失敗させます。 同じコミットでそれを証明するものを追加せずに、このプロジェクトに約束を追加することはできません。
Chart data
| Value | |
|---|---|
| Grammar | 20 |
| Exit codes (truth) | 12 |
| Machine output | 11 |
| Self-description | 10 |
| Environment | 8 |
| Prompt safety | 6 |
| Streams | 5 |
| Cancellation | 5 |
| Determinism | 5 |
46 規範条項。 92 それらにマッピングされたテスト。 184 テストの合計。
そして、これらのコンフォーマンステストのいずれもソースに対して実行されません。 彼らは自身のビルドスクリプトでパッケージをビルドし、
npm pack を実行し、tarball を解凍し、解凍されたエントリーポイントをインポートするフィクスチャ CLI を書き、Node、Bun、Deno の下でそれらを起動します — シェルがそれらを見るときと同じように終了ステータスとバイトをアサートします。その形は美的選択ではありませんでした。 このパッケージはかつて 65 KB スタブを出荷しました。 単一の
"sideEffects": false フラグにより、バンドラはルーターと exit-code モジュールをアーティファクトからツリーシェイクし、名前はエクスポートリストに残ります。 ビルドは 0 で終了しました。 ソーススイートは時間を通じて緑色のままでした。 証拠はアーティファクトだけで、アーティファクトを見ているものは何もありませんでした。7 ツールの移行は、存在することを誰も知らなかった3 ゲートを発見しました
私たちは同じ日に10 CLI の7 を移行し、移行は設計が実際の評価を受けた場所です。
配当はすぐに着地し、何も費用はかかりませんでした:古いバージョンのコマンドはすでに値を返していたため、フレームワークはそれらをエグジットコードを導出するためだけに使用し、破棄しました — それらの返り値のすべてがアップグレード日にJSON ペイロードになりました。 7 ツールは、1つのコマンドも書き換えずに機械可読出力を取得しました。
私たちが予期しなかったのは、3 異なるツールで同じ欠陥があり、互いに知らなかったことです。 それぞれがルーターの前にゲートを持っていました:有効なコマンド名の手動管理リスト、または何かが実行される前に資格情報を要求するスタートアップステップ。 すべての場合で、新しい
manifest コマンド — ツール全体の表面を1回の呼び出しで説明するもの、したがってエージェントはソースを読むことなく学習できるもの — は「不明なコマンド」または「欠落したトークン」と回答しました。そのうちの1つは、コマンドリストの2番目のコピーと手書きのヘルプ画面を保持しており、どちらもツールが実際に行っていることから逸脱していました。 両方を削除すると、スイートは52 から3 が失敗して57 が成功するようになりました0. セット内で最大のツールは364 テストを持ち、ソース変更なしでアップグレード前後に合格しました。
このパターンは十分に一般化され、書面化された手順になり、パッケージ自体に組み込まれました。 それは9 ステップであり、時間を消費する2 ステップは2 予想されないものです。
監視が不要な唯一の数値はゼロです
ベースには 2 ランタイム依存関係 がありました。 それは現在、何もありません。
それは部分的に美的で、主に算術的でした。 2025年9月8日、攻撃者は Josh Junon(JavaScriptで最も依存されているパッケージのいくつかのメンテナ)の npm アカウントをフィッシングし、偽のドメインとライブワンタイムコードを使用しました。 18個のパッケージが悪意のあるバージョンで公開されました、
chalk と debug を含む — それらの間で週に約2.6億ダウンロードを持つパッケージ。 ペイロードは crypto-clipper でした。 メンテナはそれを検知し、約 2 時間以内にロールバックしましたが、侵害されたバージョンはその窓内で約 2.6 万回ダウンロードされました。Chalk は、私が CLI を求めたときにモデルが頻繁に手に取る 3 つのライブラリの一つです
ベースは影響を受けませんでした — Chalk に依存していなかったためです — そして私はそれについて劇的に語るよりも正確に言いたいのです。なぜなら、インシデントの発生後 2 ヶ月で作成されたからです。 関連性は、私たちが何かを回避したということではありません。 この事象はリスクのクラスを正確に説明しています:すべての依存関係は他人のリリース決定の十年であり、あなたは自分が管理していないアカウントを信頼しています。 1つの依存関係を置き換えた色処理は、約 60 行です。 他のものを置き換えたプロンプトは120についてです。 監視が不要な唯一の数値はゼロです。
基本が現在返すもの
配布されたバージョンは 85 KB で、未圧縮、実行時依存関係がなく、Node、Bun、Deno 上で動作します。 それに基づいて構築されたすべてのコマンドは、コマンドごとのコードなしで、:
| 保証 | 実際に意味すること |
|---|---|
| 正直な終了コード | 人間に報告されたエラーはシェルに報告されます |
--json と --ndjson | あなたのコマンドが返す値は、機械が解析できる形です |
manifest | 1 決定論的呼び出しで記述されたツール全体は、何もロードしません |
| ストリーム規律 | stdout はペイロードであり、すべてのログ行は stderr にあります |
| 使用エラー | 「呼び出しが間違っている」には 2 を、 「試みて失敗した」には 1 を使用します |
| プロンプトの安全性 | ターミナルがないプロンプトは、永遠にハングする代わりに数ミリ秒で失敗します |
| キャンセル | Ctrl-C はコマンドのシグナルを中止し、130 を終了します |
私が何度も戻ってくるのは、そのリストの中の単一の項目ではありません。 それはリストが今 チェック可能 であるということです。 README の自前の例は公開された tarball に対するテストとして実行され、本文で引用される数値はスイートが生成する数値と一致させられます — これは書かれた直後の 1 分以内に最初のエラーを検出したルールで、ページでは 87 KB と記載されており、アーティファクトは 85 でした。
ベースは github.com/light-merlin-dark/merlin-cli でオープンソースであり、契約はウェブサイト上の主張ではなくリポジトリ内のファイルです。
4 年前、問題はすべてのモデルが CLI が何であるべきかについて異なる意見を持っていたことでした。 答えは意見と議論することではありませんでした。 それは彼ら全員が構築するベースを所有し、ビルドが失敗する可能性のある場所に約束を書き留めることでした。
#AI#developer-tools#cli#architecture#open-source#testing#supply-chain#experiential#insights