一つのアプリといくつかのスキルを作った。開発速度は2倍に、トークン消費は半分に減った。

私はプロダクトセキュリティを主力とする会社のCTOで、0歳と2歳、二人の息子を育てている。日中は会議が多く、夜は妻が休めるように子どもの面倒を見る。そして時々、変な時間にセキュリティの問題が起きる。react2shell、litellm、tanstackを狙ったサプライチェーン攻撃などなど。そのたびに、業務時間外でも顧客企業のシステムが露出していないか確認して連絡しなければならない。人生がめちゃくちゃ忙しい。

そこで自分のためのメモアプリ、今のnaholoを作った。顧客やチームから来た短いアイデアやフィードバックを、思いついた瞬間にすぐ放り込める場所が必要だった。実質、自分一人で喋るだけの簡単なスレッドベースのチャットアプリだった。メモを残して、余裕ができたときに溜まったものを眺めて、一つずつ片付けていく感じだった。

NAHOLO OPERATION LOGS/NOTES

#222 Redact ai transcripts

Junyoung
Users might be afraid to use the app because it uploads full transcripts. Need to remove all messages from both users and agents
Some tool usages are exposing file path. Should be better to redact all string other than we need for calculating stats.
Type a message…

そのうち、このアプリへのフィードバックもこのアプリで管理し始めた。アプリが気に障るたびにメモを残し、時間ができるたびに一つずつ直した。そしてふと、MCPサーバーをつないでこれらのアイデアをそのままVSCodeに引き込めれば、毎回のコンテキストスイッチなしに直接受け取ってエージェントに任せられるんじゃないか、と思った。そして小さなCLIアプリを作り、MCPサーバーとコマンドでWebアプリと連携できるようにした。

ここまではうまくいった。だが、次々とボトルネックが見えてきた。

まず、Claude Codeの/planを使うとき、毎セッションがギャンブルのように感じられた。コンテキストのサイズと情報の密度によって、セッションの質がはっきり変わるのが分かった。コンテキストが多すぎるとエージェントはハルシネーションを起こし始め、詳細すぎると数行直すために長い文章を読まなければならなかった。これを避けようと適切なサイズと密度を探したが、簡単ではなかった。最初のコンテキストを準備すること自体が、一つの仕事になった気分だった。

そのうち、ふと気づいた。粗いアイデアでも断片的なフィードバックでも壮大な計画でも、サイズと密度は異なるが、それぞれにユーザーを苦しめる根本的な何かがあるということを。そこでエージェントに、チケットに隠れたペインを引き出させるよう仕向けた。「これを作りたい、でもこれはなぜ必要なんだろう?」結果はとても良かった。エージェントはコンテキストのサイズがどうであれ、要約だけは見事にこなす。だからそれを少しひねってペインに加工するのは、本当に簡単なことだった。そこでこれを、すべてのチケットのベースラインコンテキストにすることにした。

次は概念作業、つまりどの解法が有効かというコンセプトをエージェントに作らせてみた。だが、かなり不安定だった。ありきたりなタスクなら大きな問題はなかったが、少しでも複雑なアプローチが必要になると、抽象的な計画はただの空想でしかなく、具体的な計画を立てたり実際に実装したりするうちに、決定を覆すことが多かった。そこでコンセプトから一歩踏み込んだ、現実的なアイデアになりうる設計をやってみることにした。そのとき**アーキテクチャ決定記録(Architecture decision record)という概念を借りて、制約(Constraints)を作ってみようとした。**

だが、また別の問題が浮かび上がった。主に小さいタスクの場合、エージェントが無理やり制約を絞り出すことが多く、こうした細かい制約はむしろ次の計画とコード変更の邪魔になった。 そこで決定の情報密度に制限をかけた。厳しい長さ制限を設けて、たった二文、一つはそれが何なのか、一つはなぜ必要なのかを伝えさせ、さらにファイル名や実際に変更するコードは、それが唯一の解決策でない限り書くのを控えさせるなど、いろいろな対策を試した。これによってエージェントはアーキテクチャの計画を絞り出せなくなり、おかげでコンテキストのサイズがどうであれ、一定の情報密度を保てるようになった。もちろん、適切なコンテキストの大きさと密度はそのトピックに対する私の理解度により揺れるためた少し可笑しい時もはあったが、だいぶ制御できるようになった。制約が強すぎれば少し取り除けば良かったし、曖昧なら再び掘り下げることで十分だった。 また掘り下げると、自分が知らなかった概念を学んだり、エージェントのミスに早く気づけたりした。ミスが見つかった場合は、まだ具体的な計画とコード変更はまだなので、イテレーションも軽く回った。タスクのサイズによって違うが、だいたい英語で200〜400ワード以内で全体像がひと目で見えるので、レビューもストレスなく、とても簡単にできるようになった。

これで全体像を掴んだので、実際の実装計画を立てさせた。タスクをコミット一つ単位に分割して実装できるようにした。 結果はまあまあ良かった。だいたい4〜5ファイル以内の変更に分割してくれるので、コードレビューが楽だった。だが、まだギクシャクする部分が見えた。先に話したのと似た、情報密度の問題だった。しばしばエージェントは具体的な計画をほぼコード変更レベルで書き、同じ内容をもう一度読むことで生じる負担は、次第に無視できなくなっていった。 そこでいっそ、すぐ実装させてコードを書かせたらどうかとも考えた。だが計画を飛ばすと、また元のバイブコーディングに戻るだけで、エージェントとずっと格闘するしかなかった。

そこで考えた。どうすれば良い情報密度を絞り出せるだろうか?私が見つけた適切なポイントは、モジュールレベルの契約だった。 スキルに、モジュールごとに外部へ公開された関数とインターフェースの変更だけを描写できるよう、その変更を一文で強制した。そうやって純粋に、モジュール間の責任範囲だけを簡単に検討できるようになった。

もちろん、ある変更は一文では足りなかった。そこでエージェントに、少しだけ息のつける余地を与えた。変更が契約に関するものなら、その契約変更だけを際立たせる疑似コード(pseudocode)を書く。内部フローならASCIIダイアグラムでスケッチする。UIも同じく、ASCIIワイヤーフレームを描く。ASCIIで画面をどうモックするんだ、と聞かれるかもしれないが、タスクがコミット一つ分の大きさになれば、触れる表面積はごく小さいので、ラフなワイヤーフレームで十分すぎるほどだ。

モジュールレベルの契約変更に対する疑似コード

// src/lib/operation-search.ts
type OperationSearchConditions = {
  // ...
  exactOperationNumber: number | null // was: operationNumber: string | null
}

function toggleSearchToken(
  search: string,
  kind: 'label' | 'assignee',
  value: string,
): string // new — append the token when absent, strip it when present

関数フローのプレビュー

active polar sub? ──yes──► usedSeats > seat cap ? seats-exceeded : active
        └────────no──────► usedSeats > 1        ? suspended      : free

レイアウト変更モックアップ #1 - オペレーションページの構造変更(タスク1)

before                       after
┌───────────┬───────────┐    index route             detail route
│ list panel│ detail /  │    ┌──────────┐           ┌─────────────────┐
│ (always)  │ "select…" │ →  │nav │list │ row click │nav │bread crumb │
│           │           │    │    │     │ ────────► │    │op detail   │
└───────────┴───────────┘    └──────────┘           └─────────────────┘

レイアウト変更モックアップ #2 - オペレーションページのナビゲーション(タスク2)

┌──────────────────┐
│ ⬡ Operations   ✎ │
│ All operations   │  → ?search cleared
│ ASSIGNEES        │
│  (RO) rokt33r(me)│  → ?search=assignee:rokt33r
│  (JC) junyoung   │
│ LABELS           │
│  «bug»           │  → ?search=label:bug
│  «infra»         │
└──────────────────┘

タスク1でnavと表現された部分が、次のタスクで詳細に描写される。

レイアウト変更モックアップ #3 - オペレーションリストアイテム(タスク3)

◉  #275  Renew operations page      «infra»        (RO)(JC)   11:38
▲status ▲num ▲title (truncates)     ▲label pills   ▲avatars   ▲date

タスク1でlistと表現された部分が、後のタスクで詳細に描写される。

そうやって開発が楽になるかと思いきや、新しい問題を見つけた。エージェントは小さな断片から作り、最後に全部を繋ぐ傾向が強いということを。今やっている作業が後でどう使われるかを把握するには、すべてのタスクを読まなければならなかった。もっと悪いことに、UIではエージェントが付属コンポーネントから作るせいで、ページ全体が存在するまではそれらをレンダリングして見ることすらできなかった。そのとき気づいた。エージェントは人のようには働かない。エージェントはすべてをワンショットで終わらせるよう最適化されているが、我々はそうではない。人が作るときは、大きな形から始めて必要に応じてディテールを埋めていく。 絵を描くようにだ。ラフスケッチをまず描いて、それからディテールを足して、また足して、描写が満足いくまで。

そこで順序を逆にした。まずドメインレベルのスキーマとインターフェースから。次に大きな塊、エンドポイント一つやページ一つを、プレースホルダーのコメントやスケルトンコンポーネントで立てておく。それからゆっくりディテールを埋める。そうやって大きな塊の実装を検討しながら、同時に次に何が起きるかを把握できるようになった。おかげで、すべてのタスクを前もって読む必要がなくなった。コード変更の内容が、次のタスクのプレビューになる。 最初のタスクを完了すれば、もう二番目のタスクで何が起きるか分かるし、より速くレビューできる。もう前のタスクに戻って直す必要がなくなった。これでエージェントがミスをしても、より早く気づき、実際のコードを書く前に直せるようになった。

最後に、エージェントがコードを実装するとき、もう一つ息抜きの穴を開けてやった。タスクごとに小さなゴールがあり、そのゴールを達成するために計画では想定しきれなかったことをやらざるを得ない場合があった。そこでエージェントには即興で対処させつつ、それを計画の進行記録に強調して残させた。計画と同じフォーマットで、ただし計画から外れたという概念でDeviations(偏差)と名づけた。 逆に、エージェントだけでなく私が作ったDeviationsも記録させた。実装中に考えが変わったら、軽く契約を変更できるように。そうしてエージェントには二種類のDeviationsを記録させた。エージェントが間違えたか、私が間違えたか。この二つのケースを分析しながら、自分でも計画をどう検討すべきかをさらに磨けたし、開発体験はさらに快適になった。

この全体の流れが定着すると、私はコンテキストの状態がどうであれ、心穏やかに開発できるようになった。今やエージェントはコンテキストがいくら大きくなっても一定の情報密度で働くので、大きなタスクでも依然として5分のレビュー枠に収まる。私がやることは、ただ淡々と5分のレビューをこなしていくことだけだった。 エージェントが吐き出すテキスト量が限られているので、事実上バーンアウトが起こりようがない。チケットを分割すべき場合も簡単に把握できるようになった。だいたい制約が20個、タスクが10個を超えるとき、タスクを分割すると本当に楽に進められた。そうして私も、エージェントも、両方とも正気を保ったまま、アイデアから計画、コードまで徹底的にレビューしながら、ハルシネーションなく、バーンアウトなく開発を続けられるようになった。

NAHOLO OPERATION STATS
Approx. cost$52.98
Transcripts1
Messages708 (298 user / 410 asst)
Transcript time2h 6m
claude-opus-4-710.53M (weighted, in 614 · out 216.4k · c5m 0 · c1h 2.39M · r 46.64M)
$52.65
claude-sonnet-4-6111.9k (weighted, in 12 · out 1.7k · c5m 0 · c1h 39.9k · r 236.4k)
$0.34
Skills
splashclaude-opus-4-7 4.8M
opordclaude-opus-4-7 1.3M
exfilclaude-opus-4-7 812.1k
warnoclaude-opus-4-7 212.9k
infilclaude-sonnet-4-6 111.9k
fobclaude-opus-4-7 108.9k
reconclaude-opus-4-7 81k
(none)claude-opus-4-7 3.21M
weighted tokens attributed to each (skill, model)
Tools
258
Edit 105 · Bash 84 · Read 60 · Write 5 · Skill 2 · Agent 1 · AskUserQuestion 1

ここからさらに一歩進んで、各フェーズのトークンコストも計測し始めた。主観的な品質で評価するのではなく、客観的な数字でも評価してみたかった。このサイクルを作って数百のチケットを扱ってみると、平均消費量は次のようだった。

  • コンセプトおよび制約の決定: 7.3%
  • タスクの切り分け: 17.1%
  • 実装: 65.4%
  • その他: 10.2%

見るとこのワークフローがなぜ快適なのかが分かった。制約を決める7.3%やタスクを分ける17.1%の区間でイテレーションを回す場合、覆すコストがタスクを作る前なら1/9、実装前なら1/3のレベルで、トークン消費が少ないということを。トークン消費が少ないのでコンテキストを大きく汚さず、エージェントもハルシネーションが起きる頻度が明らかに減った。最終的に、私のトークン使用量も大きく減った。覆すコストも安く、頻度も明らかに減ってしまったので、業務時間中にエージェントを三つ同時に、それぞれ別のプロジェクトとワークツリーで回しても、逆に自分の100ドルのClaude x5プランの30%以上を使うのが難しくなった。(去年の育休前まではx20でも毎回使い切って、Sonnetまで使っていたが。)

そうやって私は、再びコーディングを楽しめるようになった。CTOとして山のような会議や顧客のセキュリティ事故を処理しながらも、私は今でも自分のコードをこつこつとすべてレビューを行いながら実装を続けている。そしてこのサイドプロジェクトは、退勤後、二人の子どもの面倒を見ながら、隙間ができるたびにレビューし、計画とコードを削っていく。今でもエージェントがミスをするときはあるが、すべての段階に厳しい制限がかかっているので、問題が手に負えないほど大きくなる前に戻せた。さらにワークフローに従っていくと、エージェントはそのセッションの中で自ら手なずけられ、ワークフローから外れた行動をしても、また元の軌道に簡単に戻せた。もちろん、コンテキストのサイズが大きくなりすぎてハルシネーションを繰り返し始めても大きな問題はない。そもそもチケットの全コンテキストを計画ファイル一つで管理しているので、/compactなしで新しいエージェントをすぐ始めればいい。新しいエージェントもワークフローを理解しているので、ただ前のエージェントが作ったファイルを一度ロードするだけで、そのままコンテキストを引き継げる。しかも/compactを使わないので、/compactを待つ時間もなくなり、変に要約されて(重要な部分を抜かして、どうでもいい部分だけ残す場合)ハルシネーションが起きることも完全になくなった。

今月、ついにチームを説得してこれを使わせた。みんな懐疑的だった。Webアプリにスキルセット、そしてエージェントにツールとコマンドを提供するCLIまで合わさったソリューションなので、いったいこれが何なのか一文で説明するのが難しく、理解してもらうのに苦労が多かった。それでも一度使ってみると、彼らもすぐに違いを体感した。開発速度が2〜3倍になった。エージェントのミスが減り、エージェントの計画とコードレビューにかかる時間が明らかに減り、ストレスそのものも劇的に下がったので、チケットに置いていたバッファスケジュールを使う頻度が顕著に減った。5日のタスクが1日半でデプロイされ、チームのトークン使用量はタスクごとに30〜60%まで下がり、特に大きなタスクほどトークン消費量の変化が劇的だった。

チームメンバーが褒めてくれたもう一つの点は、どう使うかを前もって学ぶ必要がほとんどないということだった。フローもシンプルで、エージェントが何をすべきか常に案内してくれる。いわゆる「AIヒーロー」たちは数十通りのケースのために数十のスキルを提供するが、私はたった五つのコアスキルしか渡さない。そしてその五つで、ほぼすべてをカバーする。粗いアイデアや顧客の不満コメントから始めて、機能実装まで一気に処理できるようになった。

# コアスキル
/infil   (Infiltration) Webアプリからコンテキストを取得
/warno   (WARNING ORDER) コンセプトと制約を確定
/opord   (OPERATION ORDER) タスクを切り分け
/splash  (砲撃・空爆を要請する掛け声、"splash out / splash over") 各タスクを実装
/exfil   (Exfiltration) Webアプリへコンテキストを送信

# 追加スキル
/recon   計画変更のドラフト作成
/chop    イシューを分割
/fob     (Forward Operating Base) コードベースから直接チケットを作成
/frago   (FRAGMENTARY ORDER) 制約とタスクを変更

ところで、なぜ軍隊用語なのか?

  1. ふと、自分の韓国陸軍時代を思い出したから。(まったく懐かしくはないが、)
  2. Call of Dutyが好きだから。年末に出る「Modern Warfare 사(4)」が待ちきれない。
  3. 米軍の意思決定プロセス(MDMP)がいろいろとヒントをくれたから。ついでに用語も丸ごと置き換えた。

マッピングはこうなった。

  • issue → operation
  • member → operator
  • concept and constraints → WARNING ORDER (WARNO)
  • tasks → OPERATION ORDER (OPORD)
  • epic → campaign(制作中)
  • milestone → rallypoint(制作中)
  • plan change → FRAGMENTARY ORDER (FRAGO)

おまけに、チームが会議でこの妙にタクティカルな喋り方をするようになって、正直かなり気に入っている。「今回の作戦、準備命令(WARNO)を用意したんだけど、レビューしてくれる?」

とにかく、AIコーディングに疲れたけれど、バイブコーディングに妥協はしたくなくて、また作る楽しさを感じたいなら、一度使ってみてほしい。ソロユーザーには無料だ。チームでも使えるし、コラボレーション機能もさらに強化される予定だ。そして全部オープンソースなので、GitHubリポジトリに立ち寄って、スターを押してフォークもしてくれたら嬉しい。

長い文章を読んでくれてありがとうございました。