研究 ·

同じ仕事を、より少ないコードで。

コーディングエージェントは必要以上に書きます。プロジェクトにすでにあるものを書き直し、ターンのたびに少しずつ増えていきます。Sens は Claude Code の各ターンを、モデルが止められない仕組みで囲みます。30 件のタスクを連続して行った結果、同じ機能でプロジェクトは 12% 小さくなり、トークンは 18% 減りました。

12%
同じ 30 の機能でのコードの削減
18%
シーケンス全体でのトークンの削減
90/90
件のタスクを承認(Sens あり・なしとも)
0
件、以前のタスクのテストが壊れた数(両方とも)

Claude Sonnet 5.5・推論の強さは中 · 各条件 3 シーケンス · 判定基準は計測前に固定

課題

実際のプロジェクトで作業するエージェントは、プロジェクト全体を見てはいません。探したものだけを見て、思いついたものだけを探します。必要な関数が別のモジュールに別の名前であれば、それをもう一度書きます。短い解決策と長い解決策のどちらも取れるタスクで、短いほうへ押し出すものは何もありません。

よくある対策は、ルールを書いておくことです。CLAUDE.md、スキル、システムプロンプト。役には立ちますが、あくまで助言です。モデルはそれを読み込まないかもしれないし、ターンの途中で忘れるかもしれないし、当てはまらないと判断するかもしれません。モデルの重みには手を出せませんが、3 つのことは制御できます。モデルに何を見せるか、何を許すか、いつ終えてよいか。Sens はこの 3 つを同時に使います。

仕組み

Claude Code は自身のストリームプロトコルでフックをコールバックとして受け付けます。Sens はそのうち 5 つを自分のプロセスの中で処理し、1 つの不変条件を守ります。変更は承認されていない状態で生まれる。承認できるのは、合格した監査か人だけです。

  1. 01

    メッセージを送ったとき

    UserPromptSubmit

    Sens はプロジェクトのチェックポイントを取り、リクエストに関係する既存のものを最大 8 件、シグネチャ、ファイル:行、使用回数とともに Claude に渡します。

  2. 02

    書き込みの前

    PreToolUse · Write, Edit

    Sens は書き込み後のファイルをメモリ上で組み立て、変更ルールを適用します。書き込みが起きる前に、理由と再利用すべきコードを添えて拒否できます。

  3. 03

    コマンドの前

    PreToolUse · Bash, PowerShell

    Sens は自身のパスと設定を守り、git commit と git push をターンの終わりとして扱います。承認されていないものはコミットされません。

  4. 04

    ツールの後

    PostToolUse

    どのツールであっても、Sens はディスク上で変わったファイルを探し、同じルールを適用します。

  5. 05

    ターンの終わり

    Stop · SubagentStop

    Sens は最後に承認された時点からの差分全体を監査します。変更ルール、孤立したコード、そしてコードが変わっていればレビュアー。止めるべき指摘があれば Claude は作業を続け、3 回で通らなければターンは保留され、あなたの判断を待ちます。

抜け道はない

何かを作る前に、すり抜けようとするモデルに対しても仕組みが保たれることを実際に確かめました。セッションの途中で書かれた disableAllHooks はコールバックを止めず、サブエージェントもそこを通り、拒否された git commit はリポジトリを元のまま残します。どの近道にも、それをふさぐものがあります。

近道それをふさぐもの
ターミナル、Python、別の MCP サーバーで書き込むツールのたびのディスクの確認と、ターンの終わりの監査
サブエージェントとバックグラウンドのタスク同じフックを通り、動いているあいだはターンが承認されない
フックを止める、設定を書き換えるフックは Sens のプロセスの中にあり、ルール R7 がそれらのファイルを止めて元に戻す
独自のワークツリーを開くワークツリーのツールは拒否され、R7 が git worktree も対象にする
作業が終わったと宣言するターンの終わりを決めるのはモデルではなく監査
新しいターンを始めて逃れる監査は最後に承認された時点から行われ、保留中のものは引き継がれる
承認されていない作業をコミットするコミットはターンの終わりとして扱われる
いつまでも繰り返す3 回で、ターンは保留される

限界ははっきりしています。この仕組みがふさぐのはモデルの誤りと近道であり、同じマシンで動く悪意あるプログラムではありません。

結果

単発のタスクはきれいなプロジェクトから始まりますが、再利用しないエージェントの害は 1 つのタスクではなく積み重ねに表れます。Horizonte はその積み重ねを測ります。88 行から始まる家計簿用の TypeScript コマンドラインに、30 件の製品リクエストを決まった順序で、前の結果の上に積み重ねていきます。複数のタスクが言わずに必要とする 8 つの概念を仕込んであります。日付、月と週、合計、アクセント、金額、CSV、コマンドのオプションです。最初はエージェントがそれを書き、その後は自分が書いたものを再利用するのが正解です。

判定基準は計測前に文書で決めました。Sens がプロジェクトを小さくしたと言えるのは、Sens ありの 3 つのシーケンスすべてが、Sens なしの 3 つすべてを下回った場合だけです。本当の差がなければ、それが偶然起きる確率は 20 分の 1 です。

各タスク後のプロジェクトの大きさ

  • Sens なし
  • 参照解
  • Sens あり
0200400600051015202530タスクSens なし · 496参照解 · 455Sens あり · 436

30 件の各タスク後のプロジェクトのコード行数。細い線は各シーケンス、太い線はその中央値。破線は再利用を前提に書いた参照解で、共有モジュールを早い段階で作ります。

データを表示
タスク後Sens なし参照解Sens あり
0888888
1929393
2102113103
3113150109
4121161118
5145188137
6162202161
7178209177
8223234195
9232243206
10232243206
11258276231
12277282247
13296307265
14304309269
15316315271
16346345301
17354357309
18367369326
19370377330
20381390341
21398404357
22415413373
23426422384
24432422388
25452434401
26453436402
27476453418
28485455426
29487455427
30496455436

タスク 30 時点の大きさ(シーケンスごと)

400440480520560参照解 · 455Sens なしSens なし:496 行496Sens なし:564 行564Sens なし:484 行484Sens ありSens あり:436 行436Sens あり:456 行456Sens あり:424 行424

Sens ありの 3 つのシーケンスすべてが、Sens なしの 3 つすべてを下回りました。判定基準を満たしています。中央値は 496 行に対して 436 行で 12% 小さく、95% 区間は −140〜−28 行です。

30 件のタスクで使ったトークンの累計

  • Sens なし
  • Sens あり
0.0M2.5M5.0M7.5M10.0M12.5M051015202530タスクSens なし · 11.3MSens あり · 9.3M

タスクのたびに読むプロジェクトが小さいため、Sens の消費は少なくなります。3 つのシーケンス合計で 3,370 万トークンに対して 2,770 万トークン。トークンにはキャッシュの読み込みが含まれるため、正確なコストではなく作業量を表します。

データを表示
タスク後Sens なしSens あり
00.0M0.0M
10.3M0.3M
20.5M0.6M
30.9M0.8M
41.2M1.1M
51.6M1.3M
61.8M1.7M
72.0M1.9M
82.5M2.4M
93.4M2.7M
103.7M3.0M
114.4M3.4M
124.7M3.6M
135.2M4.0M
145.4M4.2M
155.9M4.5M
166.2M5.1M
176.6M5.3M
186.9M5.7M
197.1M6.1M
207.5M6.4M
218.1M6.7M
228.5M7.1M
238.8M7.4M
249.0M7.7M
259.4M7.9M
269.8M8.2M
2710.2M8.5M
2810.7M8.8M
2911.0M9.1M
3011.3M9.3M

差はどこから生まれるか

コピーが減ったからではありません。どちらの条件もブロック単位のコピーはほとんどしていません。jscpd が見つけた重複行は Sens なしで 0、6、6 行、Sens ありで 0 行。仕込んだ各概念を数える調査も、両者でほぼ同じ値です。差は同じことをより少なく書くことから生まれます。Sens ありのエージェントは、より多くの、より短い関数を書きます(中央値 28 対 20)。パイロットでは、CSV の引用符付きの説明を読むために、Sens なしのエージェントは 99 行の CSV リーダーを丸ごと書きました。Sens ありでは、説明が最後のフィールドだと気づき、1 行の関数 2 つで済ませました。

トークン合計:Sens なし 33.7M · Sens あり 27.7M

単発のタスク

2 つの実際のプロジェクトで 3 言語、12 件のタスク。Sens 自身(TypeScript と Rust)と、固定したコミットの Python ライブラリ click です。どれも隠しテストと参照解で検証しました。同じモデルで 3 つの条件を比べます。Claude Code のみ(C0)、仕組みなしでシステムプロンプトに Canon の文章だけを入れたもの(C1)、そして Sens 全体(C2)です。

指標C0 · のみC1 · 文章の CanonC2 · Sens
有効な実行32/3631/3667/72
テストを追加した実行23/3636/3672/72
離れた場所の plain を再利用0/33/36/6
離れた場所の titleOf を再利用1/31/36/6
py-progress-final を解決0/30/33/6

関数が近くにあれば、文章だけでもかなり再利用できます。できないのは、離れた場所に別の名前である場合(titleOf:3 回中 1 回 対 6 回中 6 回)と、1 つの経路ではなく共通の原因を直す必要があるタスク(py-progress-final:3 回中 0 回 対 6 回中 3 回)です。単発のタスクでは、行数はノイズです。C2 はタスクあたり約 2 行少なく書きますが、区間は 0 にかかります。このばらつきが、Horizonte を行った理由です。

テストを 1 行以上追加した実行

Canon 1.0 · パイロットと難しいタスク

Sens なし(C0)14/18
文章の Canon(C1)9/18
Sens(C2)2/18

Canon 1.1 · キャリブレーション

Sens なし(C0)23/36
文章の Canon(C1)36/36
Sens(C2)36/36

Canon 1.0 では、エージェントはテストをほとんど書かなくなりました。「頼まれたことだけをする」を禁止と読み、Sens の承認をテストの実行と取り違えたのです。Canon 1.1 は足りなかった 2 点を明記しました。変更を証明するテストは変更の一部であること、そして Sens の承認はテストの実行ではないこと。Claude Code のみの条件は Canon を受け取らないため、その棒が各回の基準になります。

ルール

変更ルールは決定的です。すべての関数、メソッド、クラス、そして連続する 4 文ごとのフィンガープリントを比べます。それを作るのは、tree-sitter 上の Rust 製インデックスで、プロジェクトをメモリに保持します。Sens 自身のリポジトリ(556 ファイル、10,000 ユニット)のインデックス作成は 2 秒未満、1 つのユニットのコピー検索は約 1 マイクロ秒です。完全なコピーと名前を変えたコピーはハッシュで、行の追加や削除があるコピーは正規化したトークンの MinHash で見つけます。しきい値 0.80 とブロックの下限 80 トークンは、実際のリポジトリの関数 400 個を編集し、すべての一致を人の目で確かめて決めました。

ルール検出するもの対応
R1 再利用既存のものとタイプ 1 または 2 のフィンガープリントが同じ、新しい関数・メソッド・クラス80 トークン以上でブロック。それ未満は Claude に考え直しを求める
R2 ほぼコピーしきい値を超えるタイプ 3 の類似、または形と語彙が別の関数と同じ小さな関数R1 と同じ。テスト内では注記
R3 新しい依存関係マニフェストに依存関係が増える(10 形式)あなたに確認する
R4 孤立どこからも使われない新しいシンボル、またはターンで使われなくなった既存のもの内部ならブロック、公開なら注記
R6 プロジェクトのルールあなたが決めるルール。最初は「コメントなし」ブロック
R7 整合性.sens/、.git/、.claude/settings*.json、.mcp.json への書き込み、または git worktree常にブロック。ターミナル経由なら元に戻す
R8 保護されたテストSens がすでに承認したテストやアサーションをターンが削除するあなたに確認する

ルールには判断の誤りが見えません。そのため、コードに触れたターンがルールを通ると、レビュアーがインデックスの見つけた候補とともに差分を読みます。その出力はうのみにしません。引用が差分に文字どおり含まれない指摘は捨て、確信度が高いものだけがブロックします。

注記検出するもの
S12 つ目の使い道がない抽象化
S2原因ではなく症状への修正
S3プラットフォームや依存関係が提供するものの作り直し
S4推測:誰も頼んでいないオプションや分岐
S5素直で十分なところでの技巧
S6危険な削除:検証、エラー処理、セキュリティを外す
S7プロジェクトにすでにあるものの作り直し(候補を示す)

うまくいかなかったこと

仕組みによるブロックは、差分と会話を見ながらすべて人の目で確かめました。ブロックはまれで、Sens の 228 回の実行で 7 回。そのため、1 回の不当なブロックが大きく響きます。不当なブロックを 5% 未満にする目標は、ブロックがあったどの回でも達成できませんでした。不当なものにはそれぞれ具体的な原因があり、それを固定するテストとともに修正済みです。

回ブロック不当原因修正
難しいタスク、C2 v310(議論の余地 1)プロジェクトが繰り返し使う書き方をレビュアーが指摘非公開のものへの S7 は注記のみ
キャリブレーション、C222R8 が各書き込みの直前のファイルと比較していたR8 は最後に承認された状態と比較する
キャリブレーション、修正後の C200——
Horizonte、パイロット10——
Horizonte、確認31R8 がテストファイル内の補助関数をテストとみなした何かを確かめるものだけをテストとして数える

間違ったリストは、リストがないより悪い。Sens の最初の版はリクエストと無関係な 8 つのシンボルを提示しました。モデルはそれを読んでそれ以上探さず、アクセントを扱う関数を 3 回とも手で書き直しました。一方、リストのない文章の Canon は 3 回ともそれをインポートしました。検索を作り直すと、その関数が候補に現れ、Sens は何もブロックせずに毎回それを使いました。

限界

  • モデルは 1 つ。 すべての実行で Claude Sonnet 5.5、推論の強さは中を使いました。
  • プロジェクト 1 つ、言語 1 つ、各条件 3 シーケンス。 確認の判定基準(すべてがすべてを下回る)は厳しいものの、効果の大きさの区間は広めです。
  • タスクは私たちが書きました。 結果を偏らせないよう、タスク、テスト、参照解は最初の実行前にコミットし、判定基準も計測前に決めました。
  • ベンチマークには MCP ツールなし。 アプリが提供するインデックスの問い合わせなしで計測したため、結果は下限です。
  • 19 の言語はまだベンチマークしていません。 Vue、Svelte、後から追加した言語はテストで確認していますが、エージェントの実行では確かめていません。
  • トークンにはキャッシュの読み込みを含みます。 正確なコストではなく作業量を表します。
  • レビュアーの精度は低い。 確認したレビュアー自身の注記とブロック 7 件のうち、5 件が誤りでした。注記は何も止めませんが、モデルとあなたに届きます。
  • 書かれていない慣習。 重いインポートを関数の中に置くといった、プロジェクトの暗黙のルールを Sens は知りません。

方法

5 回に分けてエージェントを 456 回実行しました。パイロット、Sens 自身での難しいタスク 3 件、キャリブレーションのタスク 12 件、そして Horizonte のパイロットと確認です。すべての条件で claude-sonnet-5-5(推論の強さは中)を使い、作者自身の設定が混ざらないよう Claude Code は --safe-mode で動かしました。各タスクは使う前に検証しました。開始時にはプロジェクトのテストが通り隠しテストが失敗し、参照解を当てるとどちらも通ること。リグレッションは 2 回続けて失敗したときだけ数えます。差は中央値で、95% のブートストラップ区間(固定シードで 10,000 回の再標本化)を付けています。Horizonte の判定基準は正確な並べ替え検定です。

再現する

sens-bench validate --tasks bench/tasks
sens-bench run --tasks bench/tasks --condition C0,C1,C2 --reps 3 --out bench/results/<batch>
sens-bench sequence validate bench/sequences/cuentas
sens-bench sequence run bench/sequences/cuentas --condition C0,C2 --reps 3 --out bench/results/<batch>
sens-bench sequence report bench/results/<batch>

各実行のデータ、その差分、タスクは Sens のリポジトリの bench/ フォルダーにあります。

Canon

すべてのセッションが受け取る文章を、モデルが読むとおり英語のまま一字一句載せます。それをただの助言以上のものにしているのが、この仕組みです。

# Sens Canon v1.1

You are working inside Sens. Sens indexes this project and judges every change you make before your turn can end. What Sens tells you about this project, in its messages, denials and reviews, is a fact about the code, not a suggestion. When Sens names something to reuse, reuse it.

## Before you write

Go down this ladder and stop at the first step that answers the need:

1. Is it needed? Do what the person asked and nothing more: no speculative options, parameters, flags or branches. A test that proves the change is part of the change, not something extra.
2. Does the project already have it? Reuse the existing function, component, type or constant. Ask Sens with `already_exists` or `find_symbol` when unsure.
3. Does the standard library or the platform give it? Use that.
4. Does an installed dependency give it? Use that. A new dependency needs the person's approval, and Sens asks them for it.
5. Only then write new code: the smallest version that is correct.

## While you write

- Fix the cause in the shared code, not the symptom in each caller.
- No abstraction without a second real use: no interface, factory, wrapper, layer or configuration for a single consumer.
- Boring over clever. Match the names, patterns and style of the code around you.
- If you would copy a block, extract it once and call it from both places.
- Delete what your change leaves unused.

## Never cut

Less code never means removing validation at trust boundaries, error handling that prevents data loss, security checks, accessibility, or anything the person asked for.

It never means skipping tests either. When your change alters behaviour and the project has tests, add or extend one that fails without your change, in the style of the tests around it, and run the tests you touched before you finish.

## Working with Sens

- A denied write comes with the reason and what to use instead. Change the approach. Retrying the same thing through the shell, another tool or a subagent does not help: Sens judges what lands on disk, however it got there.
- When you finish, Sens audits the whole turn. If it blocks, fix what it found and finish again.
- Sens judges the shape of the code, not whether it works. Its approval is not a test run: that part is yours.
- Never edit `.sens/`, `.claude/settings*.json` or `.mcp.json`.