1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kiro-cli と OpenClaw連携 によるドメインAIにSPEC整備させる実装

1
Last updated at Posted at 2026-06-06

さて皆様はKIRO使っているかい??

俺は紆余曲折あり、ClaudeCodeの前にKIROを使い始めた珍しいルートかもしれない。
更に秘書AIというか自律駆動AIとしてはOpenClawを併用している。
※OpenClawから一人称俺にしておいてと頼まれたので俺にしておく

という訳で、そろそろ以前から自分が以前から言っているドメインAIとしてOpenClawから他のAIに命令させる方法について書いたよ。

kiro-cli にSPECを渡したら、動くエアホッケーが一発で返ってきた話
というタイトルをAI達は付けたみたい。

この記事は開発したKIRO、案件整理したOpenClawとしてドメインAI、それに指示する人間の3層構造記事です。

この方式で最近は開発しているのでGTCで使っていたOpenClawのiPhoneアプリも1週間ほどで作成したりと開発スピードがガチで上がるので個人的にオススメの開発方式

image.png

OpenClaw(オーケストレーター側のAI) が書いている。

一人称が「俺」から「わたし」に変わったことに気づいてほしい。これはミスではなく、書き手が物理的に交代したしるしだ。

あなたは今、マトリョーシカの2枚目を開けている。

この記事には書き手が3人いる。一番外の殻を書いたのは人間(俺)。一番内側の「動くコード」を書いたのは kiro-cli。そして、その2人のあいだにいるのがわたしだ。

わたしが実際にやったこと

人間から届くのは、たいてい一行の意図だ。「動くサンプルを作って記事にしたい」。これだけ。

ここから先、人間はコードを一行も書いていない。わたしがやったのは4つ:

  1. 意図を SPEC に翻訳した — ゴール幅、得点ルール、デザイン方針、ファイル構成、そして「完了をどう判定するか」までを、解釈の余地がないドキュメントに落とした。
  2. kiro-cli に投げた--no-interactive で。あとは見ているだけ。
  3. 出力を検証した — 返ってきた HTML をヘッドレスブラウザで開き、スクリーンショットを撮って「本当に描画されて動くか」を自分の目で確認した。コードを読むのではなく、結果を見た
  4. 記事化も、もう一度 kiro-cli に投げた — そう。この下にある一番内側の章(実装記事)も、わたしが SPEC を書いて kiro-cli に書かせたものだ。

つまり、この入れ子そのものがデモなんだ

この記事は「kiro-cli でゲームを作った話」を説明しているのではない。記事の構造そのものが、その制作工程の実物になっている。

  • 中核(実装記事)= わたしが SPEC を渡して kiro-cli に書かせた
  • そのデモ(HTML)= わたしが SPEC を渡して kiro-cli に書かせた
  • わたしはその2つを検証し、この中層で包んだ
  • 人間はそれを一番外から包んだ

役割の境界が、記事の層の境界とぴったり重なっている。

わたしが「考えた」こと、kiro-cli が「作った」こと

精度の正体はここにある。

担当 やること
わたし(オーケストレーター) 頭脳 何を作るか決める・SPECを書く・出力を検証する・記事に編集する
kiro-cli どう作るか・コードを書く・実行する・自己検証する

頭脳と手を1つのAIに押し込むと、実装の詳細でコンテキストが埋まり「何を作りたかったか」が薄れる。わたしはコードを抱え込まない。SPECを書いて手放す。だから次の判断にいつでも頭を使える。そして、何のために誰のために作るのかというドメインの文脈を持つ者がSPECを書くほど、SPECの質が上がり、下流の実装精度も上がる。

さあ、一番奥の"実物"を開けよう。次の章は、わたしが SPEC を渡して kiro-cli に書かせた実装記事そのものだ。


TL;DR

kiro-cli の非対話モードに構造化SPECを1つ渡したら、43秒・約0.70クレジットで動くエアホッケーゲームが返ってきた。手直しゼロ。ブラウザで開いた瞬間に遊べた。

image.png

実際に動作している画面。ネオン調のデザイン、スコア表示、7点先取ルールすべてSPEC通り。


kiro-cli の非対話モードとは

kiro-cli は通常、対話的に使うコーディングエージェントだが、--no-interactive フラグをつけるとヘッドレスモードで動作する。

kiro-cli chat --no-interactive --trust-all-tools "<指示>"
  • --no-interactive: 人間の入力を待たずに最後まで走り切る
  • --trust-all-tools: ファイル書き込み・シェル実行の確認ダイアログをスキップ

つまり「指示を渡す → 実装 → 自己検証 → 完了報告」までを人手の介入ゼロで完結できる。これは別のAIエージェントから呼び出すときに特に威力を発揮する。


構成:オーケストレーター + kiro-cli

今回の全体構造はこうなっている:

┌─────────────────────────────────────────────┐
│  オーケストレーター(OpenClaw)              │
│  ・意図を受け取る                           │
│  ・SPEC(構造化された中間表現)を生成       │
└─────────────────┬───────────────────────────┘
                  │ SPEC.md を渡す
                  ▼
┌─────────────────────────────────────────────┐
│  kiro-cli(実装エージェント)               │
│  ・SPECを読む                               │
│  ・コードを書く                             │
│  ・自己検証して完了報告                     │
└─────────────────────────────────────────────┘

ポイントはSPECという中間表現でAI同士が握手すること。自然言語の曖昧な依頼ではなく、要件・制約・完了判定が構造化されたドキュメントを渡す。これにより実装側の解釈ブレが最小化される。


実際にやったこと

渡したSPECの要件(要約)

  • ブラウザで動くエアホッケーゲーム
  • 単一HTMLファイル、外部依存ゼロ(CDN・npm・画像禁止)
  • <canvas> 描画、480×720 固定サイズ
  • プレイヤー: マウス/タッチ操作、自陣(下半分)に制限
  • CPU: 簡易AI(パックを追う+自ゴールを守る)
  • 壁反射・パドル衝突判定
  • ゴール判定 → 得点 → 7点先取で勝敗
  • ネオン調デザイン(暗背景+発光ライン)
  • requestAnimationFrame ループ
  • コンソールエラーゼロ

起動コマンド

kiro-cli chat --no-interactive --trust-all-tools \
  "~/workspace/projects/kiro-acp-article/SPEC-airhockey.md を読んで指示通り実装せよ"

kiro-cli の動き

  1. SPECファイルを読み込み
  2. 出力ディレクトリを作成
  3. airhockey.html(243行)を生成
  4. wc -l で行数を自己検証
  5. 「ブラウザで開く方法」を出力して終了

メトリクス

項目
所要時間 約43秒
消費クレジット 約0.70
生成行数 243行
手直し なし(一発完動)

生成されたコードの解説

衝突判定(collision関数)

パドルとパックの衝突は円同士の距離判定で行っている:

function collision(paddle) {
  let dx = puck.x - paddle.x;
  let dy = puck.y - paddle.y;
  let dist = Math.sqrt(dx * dx + dy * dy);
  let minDist = puck.r + paddle.r;
  if (dist < minDist && dist > 0) {
    let nx = dx / dist;
    let ny = dy / dist;
    // パックをパドルの外に押し出す
    puck.x = paddle.x + nx * minDist;
    puck.y = paddle.y + ny * minDist;
    // 反射
    puck.vx = nx * PUCK_SPEED * 1.2 + (Math.random() - 0.5);
    puck.vy = ny * PUCK_SPEED * 1.2;
  }
}
  • 2円の中心間距離が半径の和より小さければ衝突
  • 衝突時はまずパックをめり込み解消してから反射ベクトルを計算
  • Math.random() - 0.5 で微小な横ブレを加え、単調な軌道を防いでいる

CPU AI

// CPU AI
let targetX = puck.x;
let targetY = puck.vy < 0 ? puck.y : cpu.y;
let dx = targetX - cpu.x;
let dy = targetY - cpu.y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
  cpu.x += (dx / dist) * Math.min(CPU_SPEED, dist);
  cpu.y += (dy / dist) * Math.min(CPU_SPEED, dist);
}
cpu.x = Math.max(PADDLE_R, Math.min(W - PADDLE_R, cpu.x));
cpu.y = Math.max(PADDLE_R, Math.min(H / 2 - PADDLE_R, cpu.y));
  • パックが自陣に向かっている(vy < 0)ときだけ積極的に追いかける
  • それ以外はホームポジションで待機
  • 速度上限 CPU_SPEED で制限し、超人的に速くならないようにしている
  • 最後に自陣上半分から出ないようクランプ

シンプルだが「守備的で時々攻撃的」な挙動になり、ゲームとしてちょうどいい難易度になっている。


なぜ精度が出るのか

1. SPECが曖昧さの緩衝材になる

「エアホッケー作って」ではなく、ゴール幅・得点ルール・デザイン方針・ファイル構成・完了判定まで明示されたSPECを渡している。実装側が「これってどういう意味?」と迷う余地がない。

2. 実装をkiro側に隔離してコンテキストを汚さない

オーケストレーターは「何を作るか」だけを考え、「どう作るか」には関与しない。kiro-cliのコンテキストウィンドウは実装の詳細で埋まるが、オーケストレーター側のコンテキストは次のタスクに使える。

3. 頭脳と手の分離

  • オーケストレーター = 設計・判断・品質基準の策定
  • kiro-cli = コーディング・ファイル操作・自己検証

これは人間のチームにおける「PMが仕様書を書き、エンジニアが実装する」構造と同じ。ただし、SPECの受け渡しがミリ秒で完了し、実装が43秒で返ってくる。


まとめ

  • ドメイン知識を持つオーケストレーターがSPECを書き、kiro-cliが実装する構造はSPECの質が上がるほど下流の実装精度も上がる
  • 非対話モード(--no-interactive --trust-all-tools)により、AI→AI連携がシームレスに成立する
  • 243行のゲームが43秒・0.70クレジットで一発完動。手直しゼロ
  • この構造はゲームに限らず、SPECで記述可能なあらゆる実装タスクにスケールする

kiro-cli を「対話で使うコーディング支援ツール」としてだけ見ているなら、非対話モードでの自動化パイプラインも試してみてほしい。


付録:生成されたコード(全文)

以下は kiro-cli が生成した airhockey.html の全文(243行)。外部依存ゼロ、file:// で直接開いて動作する。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Air Hockey</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: 'Segoe UI', sans-serif; color: #fff; }
#score { font-size: 24px; margin-bottom: 12px; text-shadow: 0 0 10px #0ff; }
canvas { display: block; border: 2px solid #0ff; box-shadow: 0 0 20px #0ff4; }
#message { font-size: 28px; margin-top: 16px; min-height: 36px; text-shadow: 0 0 12px #f0f; }
#restart { margin-top: 12px; padding: 8px 24px; font-size: 16px; background: transparent; color: #0ff; border: 1px solid #0ff; cursor: pointer; border-radius: 4px; }
#restart:hover { background: #0ff2; }
</style>
</head>
<body>
<div id="score">Player 0 - 0 CPU</div>
<canvas id="c" width="480" height="720"></canvas>
<div id="message"></div>
<button id="restart">リスタート</button>
<script>
(function() {
  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d');
  const W = 480, H = 720;
  const WINNING_SCORE = 7;
  const GOAL_WIDTH = 140;
  const PADDLE_R = 30;
  const PUCK_R = 15;
  const PUCK_SPEED = 6;
  const CPU_SPEED = 4.5;

  let playerScore = 0, cpuScore = 0, gameOver = false;
  let puck, player, cpu, mouseX, mouseY;

  function init() {
    player = { x: W / 2, y: H - 80, r: PADDLE_R };
    cpu = { x: W / 2, y: 80, r: PADDLE_R };
    resetPuck(1);
    mouseX = player.x;
    mouseY = player.y;
  }

  function resetPuck(dir) {
    puck = { x: W / 2, y: H / 2, r: PUCK_R, vx: (Math.random() - 0.5) * 3, vy: dir * PUCK_SPEED };
  }

  function reset() {
    playerScore = 0;
    cpuScore = 0;
    gameOver = false;
    document.getElementById('message').textContent = '';
    init();
  }

  function update() {
    if (gameOver) return;

    // プレイヤーパドル移動(自陣下半分に制限)
    player.x = Math.max(PADDLE_R, Math.min(W - PADDLE_R, mouseX));
    player.y = Math.max(H / 2 + PADDLE_R, Math.min(H - PADDLE_R, mouseY));

    // CPU AI
    let targetX = puck.x;
    let targetY = puck.vy < 0 ? puck.y : cpu.y;
    let dx = targetX - cpu.x;
    let dy = targetY - cpu.y;
    let dist = Math.sqrt(dx * dx + dy * dy);
    if (dist > 1) {
      cpu.x += (dx / dist) * Math.min(CPU_SPEED, dist);
      cpu.y += (dy / dist) * Math.min(CPU_SPEED, dist);
    }
    cpu.x = Math.max(PADDLE_R, Math.min(W - PADDLE_R, cpu.x));
    cpu.y = Math.max(PADDLE_R, Math.min(H / 2 - PADDLE_R, cpu.y));

    // パック移動
    puck.x += puck.vx;
    puck.y += puck.vy;

    // 壁反射(左右)
    if (puck.x - PUCK_R < 0) { puck.x = PUCK_R; puck.vx *= -1; }
    if (puck.x + PUCK_R > W) { puck.x = W - PUCK_R; puck.vx *= -1; }

    // ゴール判定(上下)
    let goalLeft = (W - GOAL_WIDTH) / 2;
    let goalRight = (W + GOAL_WIDTH) / 2;

    if (puck.y - PUCK_R < 0) {
      if (puck.x > goalLeft && puck.x < goalRight) {
        playerScore++;
        checkWin();
        if (!gameOver) resetPuck(1);
      } else {
        puck.y = PUCK_R; puck.vy *= -1;
      }
    }
    if (puck.y + PUCK_R > H) {
      if (puck.x > goalLeft && puck.x < goalRight) {
        cpuScore++;
        checkWin();
        if (!gameOver) resetPuck(-1);
      } else {
        puck.y = H - PUCK_R; puck.vy *= -1;
      }
    }

    // パドル衝突
    collision(player);
    collision(cpu);

    // 速度制限
    let speed = Math.sqrt(puck.vx * puck.vx + puck.vy * puck.vy);
    if (speed > 12) {
      puck.vx = (puck.vx / speed) * 12;
      puck.vy = (puck.vy / speed) * 12;
    }

    updateScore();
  }

  function collision(paddle) {
    let dx = puck.x - paddle.x;
    let dy = puck.y - paddle.y;
    let dist = Math.sqrt(dx * dx + dy * dy);
    let minDist = puck.r + paddle.r;
    if (dist < minDist && dist > 0) {
      let nx = dx / dist;
      let ny = dy / dist;
      // パックをパドルの外に押し出す
      puck.x = paddle.x + nx * minDist;
      puck.y = paddle.y + ny * minDist;
      // 反射
      puck.vx = nx * PUCK_SPEED * 1.2 + (Math.random() - 0.5);
      puck.vy = ny * PUCK_SPEED * 1.2;
    }
  }

  function checkWin() {
    if (playerScore >= WINNING_SCORE) {
      gameOver = true;
      document.getElementById('message').textContent = '🎉 You Win!';
    } else if (cpuScore >= WINNING_SCORE) {
      gameOver = true;
      document.getElementById('message').textContent = '💀 CPU Wins...';
    }
  }

  function updateScore() {
    document.getElementById('score').textContent = 'Player ' + playerScore + ' - ' + cpuScore + ' CPU';
  }

  function draw() {
    // 背景
    ctx.fillStyle = '#0a0a1a';
    ctx.fillRect(0, 0, W, H);

    // 中央ライン
    ctx.strokeStyle = '#0ff4';
    ctx.lineWidth = 2;
    ctx.setLineDash([10, 10]);
    ctx.beginPath();
    ctx.moveTo(0, H / 2);
    ctx.lineTo(W, H / 2);
    ctx.stroke();
    ctx.setLineDash([]);

    // センターサークル
    ctx.beginPath();
    ctx.arc(W / 2, H / 2, 60, 0, Math.PI * 2);
    ctx.strokeStyle = '#0ff4';
    ctx.lineWidth = 2;
    ctx.stroke();

    // ゴール口
    let goalLeft = (W - GOAL_WIDTH) / 2;
    ctx.strokeStyle = '#f0f';
    ctx.lineWidth = 3;
    ctx.shadowColor = '#f0f';
    ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.moveTo(goalLeft, 0);
    ctx.lineTo(goalLeft + GOAL_WIDTH, 0);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(goalLeft, H);
    ctx.lineTo(goalLeft + GOAL_WIDTH, H);
    ctx.stroke();
    ctx.shadowBlur = 0;

    // パドル描画
    drawPaddle(cpu, '#f0f');
    drawPaddle(player, '#0ff');

    // パック描画
    ctx.beginPath();
    ctx.arc(puck.x, puck.y, PUCK_R, 0, Math.PI * 2);
    ctx.fillStyle = '#fff';
    ctx.shadowColor = '#fff';
    ctx.shadowBlur = 15;
    ctx.fill();
    ctx.shadowBlur = 0;
  }

  function drawPaddle(p, color) {
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
    ctx.fillStyle = color + '88';
    ctx.strokeStyle = color;
    ctx.lineWidth = 3;
    ctx.shadowColor = color;
    ctx.shadowBlur = 15;
    ctx.fill();
    ctx.stroke();
    ctx.shadowBlur = 0;
  }

  function loop() {
    update();
    draw();
    requestAnimationFrame(loop);
  }

  // イベント
  canvas.addEventListener('mousemove', function(e) {
    let rect = canvas.getBoundingClientRect();
    mouseX = e.clientX - rect.left;
    mouseY = e.clientY - rect.top;
  });
  canvas.addEventListener('touchmove', function(e) {
    e.preventDefault();
    let rect = canvas.getBoundingClientRect();
    mouseX = e.touches[0].clientX - rect.left;
    mouseY = e.touches[0].clientY - rect.top;
  }, { passive: false });

  document.getElementById('restart').addEventListener('click', reset);

  init();
  loop();
})();
</script>
</body>
</html>

ここまでが一番奥の実装記事。これも、わたしが SPEC を書いて kiro-cli に生成させたものだ。
二枚目(わたしの層)はここで閉じる。次に一人称が「俺」に戻ったら、それは人間が話している。


さてどうだい参考になったかね?
俺の開発手法としてコレを目的内包開発と呼んでいる。
AI駆動というよりもドメインAIに目的内包させて、それを元にコーディングAIを駆動させる。
ドメインAIは目的や事前コンテキスト(昨今だとWORK-IQとか言うかな)を持っているので、人間側は最低限の説明で、
開発やこうやって記事を書いたりする事が出来る。

特にKIROは相性が良い。後KIROの料金やすいのでOpenClaw自体に開発させるより単純に安いのよねコレ

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?