同じような指示を出しているのに、返ってくるまでが 40 秒の日と 4 分の日がある。
個人開発で日々の細かい修正を CLI へ流している身としては、この差が読めないのは地味に困ります。最初はモデル側の混み具合だと思っておりました。ログを見返すと、そうではありませんでした。前の作業で /effort を上げたまま、次の細かい文言修正にもその設定を引きずっていただけでした。
しかも困ったことに、時間がかかった方の差分がいつも良いとは限りません。「ついでに周辺も整えておきました」という余計な変更が混ざり、レビューで戻す手間が増えていた回が何度もありました。
そこで、タスクの種別から effort を機械的に決める小さなルーターを挟み、6週間分のログを取りました。想定と一致した部分と、逆だった部分の両方が出ましたので、実測値ごと共有いたします。
/effort が変えているもの、変えていないもの
/effort は Antigravity CLI v1.1.5(2026年7月21日)で加わったスラッシュコマンドで、推論にかける手間の度合いをタスクごとに切り替えられます。
手元で観察できた範囲では、段階を上げると次の傾向が出ました。
- 計画フェーズが長くなり、実装前の探索(ファイル横断の読み込み)が増える
- 差分の粒度が大きくなる。頼んでいない周辺の整理が同じ差分に入りやすい
- 失敗したときの復帰が丁寧になる。テストが落ちた際に原因まで戻って直す確率が上がる
逆に、段階を上げても変わらなかったものもあります。
- 指示そのものが曖昧なときの成果物の質。曖昧さは effort では埋まりません
- 参照するファイルの取り違え。これはワークスペースの構造とルールファイル側の問題でした
なお、利用できる段階名は CLI のバージョンで変わりますので、起動後に /effort のヘルプで手元の値を確認してください。以下では私の環境の3段階を便宜的に low / medium / high と表記します。後述するコードでも、段階名はコードに埋め込まず設定ファイル側へ逃がしています。
タスクを4種に分ける
振り分けの前に、自分が CLI へ投げている作業を分類しました。過去のログを 100 件ほど眺めて、次の4種に落ち着いております。
| 種別 | 内容 | 判断の目印 |
| mechanical | 文言修正・リネーム・import 整理・フォーマット | 正解が事前に決まっている |
| localized | 1〜2ファイルに収まるバグ修正・テスト追加 | 変更範囲が読める |
| structural | 設計変更・モジュール分割・移行作業 | 変更範囲が事前に読めない |
| exploratory | 原因調査・比較検討・方針の相談 | コードを書かずに終わることがある |
個人開発ですと mechanical の比率が高く、私の場合は全体の4割強を占めておりました。ここを速く回せるかどうかが、一日の体感を左右します。
この分類の肝は「変更範囲が事前に読めるかどうか」です。読める作業に探索の余地を与えても、探索した分だけ余計なものが増えます。
振り分けを自動化する
分類とルーティングを1本のスクリプトにまとめました。実行ログを JSONL へ追記し、後から採用率を集計できるようにしています。
#!/usr/bin/env node
/**
* effort-route.mjs
* タスク記述から effort 段階を決めて Antigravity CLI(agy) を起動し、
* 実行結果を JSONL に追記します。
*
* node effort-route.mjs "AdMob の初期化を Application クラスへ移す"
* node effort-route.mjs --dry-run "ボタンの文言を修正"
* node effort-route.mjs --mark <id> accept
*/
import { spawn } from "node:child_process";
import { appendFile, readFile } from "node:fs/promises";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { randomUUID } from "node:crypto";
const HERE = dirname(fileURLToPath(import.meta.url));
const POLICY_PATH = resolve(HERE, "effort-policy.json");
const LOG_PATH = resolve(HERE, "effort-runs.jsonl");
// 段階名は CLI のバージョンで変わるため、コードには埋め込まず policy 側に置きます。
const DEFAULT_POLICY = {
fallback: "medium",
rules: [
{ class: "mechanical", level: "low", match: "(typo|文言|リネーム|import 整理|フォーマット|整形)" },
{ class: "localized", level: "medium", match: "(バグ|不具合|fix|テスト追加|1ファイル)" },
{ class: "structural", level: "high", match: "(設計|分割|移行|リファクタ|アーキ|置き換え)" },
{ class: "exploratory", level: "high", match: "(調査|原因|なぜ|比較|検討|方針)" }
]
};
async function loadPolicy() {
try {
return JSON.parse(await readFile(POLICY_PATH, "utf8"));
} catch (err) {
if (err.code === "ENOENT") return DEFAULT_POLICY;
// 壊れた JSON を黙って握りつぶすと全件が fallback に落ち、
// 「なぜか全部 medium」という状態に何日も気づけません。必ず止めます。
throw new Error(`effort-policy.json を読めませんでした: ${err.message}`);
}
}
function classify(task, policy) {
for (const rule of policy.rules) {
if (new RegExp(rule.match, "i").test(task)) {
return { taskClass: rule.class, level: rule.level };
}
}
return { taskClass: "unclassified", level: policy.fallback };
}
function runAgy(task, level) {
// 非対話起動では /effort を本文の1行目に置く形にしています。
// 将来フラグが用意されたら、この args の組み立てだけを差し替えます。
const prompt = `/effort ${level}\n${task}`;
return new Promise((done) => {
const started = Date.now();
const child = spawn("agy", ["-p", prompt], { stdio: ["ignore", "inherit", "inherit"] });
child.on("error", (err) =>
done({ ok: false, code: null, ms: Date.now() - started, error: err.message })
);
child.on("close", (code) =>
done({ ok: code === 0, code, ms: Date.now() - started })
);
});
}
async function main() {
const argv = process.argv.slice(2);
if (argv[0] === "--mark") {
const [, id, verdict] = argv;
if (!id || !["accept", "rework"].includes(verdict)) {
console.error("usage: --mark <id> accept|rework");
process.exit(2);
}
await appendFile(LOG_PATH, JSON.stringify({ type: "mark", id, verdict }) + "\n");
return;
}
const dryRun = argv[0] === "--dry-run";
const task = (dryRun ? argv.slice(1) : argv).join(" ").trim();
if (!task) {
console.error("タスク記述を渡してください");
process.exit(2);
}
const policy = await loadPolicy();
const { taskClass, level } = classify(task, policy);
const id = randomUUID().slice(0, 8);
console.error(`[effort-route] ${id} class=${taskClass} effort=${level}`);
if (dryRun) return;
const result = await runAgy(task, level);
await appendFile(
LOG_PATH,
JSON.stringify({
type: "run", id, taskClass, level,
ms: result.ms, ok: result.ok, error: result.error ?? null,
at: new Date().toISOString()
}) + "\n"
);
console.error(`[effort-route] ${id} ${result.ok ? "ok" : "failed"} ${Math.round(result.ms / 1000)}s`);
console.error(`[effort-route] 採用可否: node effort-route.mjs --mark ${id} accept|rework`);
if (!result.ok) process.exit(1);
}
main().catch((err) => {
console.error(`[effort-route] ${err.message}`);
process.exit(1);
});
--mark を分けたのは意図があります。実行直後に採用可否を判定できることは稀で、差分を読んでから決めることがほとんどでした。標準出力に ID を出しておき、レビュー後に思い出して打てる形にしています。
集計側は素朴で構いません。
#!/usr/bin/env node
/** effort-report.mjs — 種別×段階ごとの一発採用率と所要時間を集計します */
import { readFile } from "node:fs/promises";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const LOG_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "effort-runs.jsonl");
const lines = (await readFile(LOG_PATH, "utf8")).split("\n").filter(Boolean).map(JSON.parse);
const runs = new Map();
for (const e of lines) {
if (e.type === "run") runs.set(e.id, { ...e, verdict: null });
if (e.type === "mark" && runs.has(e.id)) runs.get(e.id).verdict = e.verdict;
}
const buckets = new Map();
for (const r of runs.values()) {
if (!r.verdict) continue; // 未判定は母数から外します
const key = `${r.taskClass}/${r.level}`;
const b = buckets.get(key) ?? { n: 0, accepted: 0, ms: [] };
b.n += 1;
if (r.verdict === "accept") b.accepted += 1;
b.ms.push(r.ms);
buckets.set(key, b);
}
const p50 = (a) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)] ?? 0;
for (const [key, b] of [...buckets].sort()) {
const rate = ((b.accepted / b.n) * 100).toFixed(0);
console.log(`${key.padEnd(24)} n=${String(b.n).padStart(3)} 一発採用 ${rate}% p50 ${Math.round(p50(b.ms) / 1000)}s`);
}
未判定を母数から外している点は、後から効いてきました。判定を忘れた実行を「不採用」に丸めていた時期があり、採用率が実態より 10 ポイント以上低く出ておりました。
6週間の実測
2026年6月中旬から7月下旬にかけて、判定済み 212 件が溜まりました。個人の作業ログですので母数は小さく、あくまで私の作業内容での傾向としてご覧ください。
| 種別 | 段階 | 件数 | 一発採用率 | 所要時間 p50 |
| mechanical | low | 88 | 84% | 41 秒 |
| localized | medium | 63 | 71% | 1 分 52 秒 |
| structural | high | 38 | 52% | 4 分 36 秒 |
| exploratory | high | 23 | — | 2 分 09 秒 |
exploratory に採用率を出していないのは、そもそも差分が出ない相談が多く、同じ物差しに乗らないためです。無理に一つの指標へ寄せると、判断を誤ります。
structural を意図的に medium へ落とした対照回(14 件)では、一発採用率が 52% から 44% へ下がりました。ここは段階を上げる価値がありました。テストが落ちたときに原因まで遡って直してくれるかどうかの差が、そのまま出ております。
上げるほど悪くなった領域
想定と逆だったのが mechanical です。
計測を始める前の2週間、私は全タスクを high 固定で回しておりました。当時のログを同じ集計にかけ直すと、mechanical の一発採用率は 61% でした。low に落とした後の 84% より、23 ポイント低い数字です。所要時間の p50 も 2 分 18 秒で、3 倍以上かかっておりました。
差分を読み返すと、理由ははっきりしていました。「文言を1箇所直す」という依頼に対して、周辺の同種の文言も揃えたり、使われていない import を掃除したり、ついでに型を締めたりという変更が同じ差分に入っていたのです。
一つ一つは悪い変更ではありません。ただ、こちらは1行の確認をするつもりでいたので、レビューの姿勢が追いつきません。結果として「趣旨は分かるが今回はここまで要らない」と差し戻すことになり、往復が増えました。
探索の余地を与えると、探索してくれる。当たり前のことなのですが、余地を与えるかどうかを自分で選べると気づいていませんでした。
正解が事前に決まっている作業には、探索の余地を渡さない。これが計測から得た一番の学びです。
effort より効いたもの
もう一つ、段階の差より大きな差を作っていた要因があります。
structural のうち、依頼文に受け入れ条件を2〜3行書いたものが 21 件ありました。「既存のテストが全て通ること」「公開 API のシグネチャを変えないこと」といった、完了の定義にあたる箇所です。
この 21 件の一発採用率は 73% でした。受け入れ条件を書かなかった 17 件は 41% です。32 ポイントの差になります。
段階を high から medium へ落としたときの差は 8 ポイントでした。書き方の差の方が、4 倍ほど大きかったことになります。
/effort は便利ですが、指示の曖昧さを埋める機能ではありません。曖昧なまま段階だけ上げると、曖昧なものを丁寧に作り込んだ差分が返ってきます。丁寧な分、こちらが読む量も増えます。
私は今、依頼文のテンプレートに「完了とみなす条件」という行を常に置くようにしております。空欄のまま投げそうになったら、そのタスクはまだ言語化できていないという合図です。
エージェント定義に既定値を持たせる
CLI v1.1.6(7月24日)で、カスタムエージェントを Markdown ファイルで定義できるようになりました。役割ごとに既定の段階を書いておけると、ルーターとの二重管理が減ります。
私は次のような形で運用しております。
---
name: mechanical-fixer
description: 正解が事前に決まっている修正だけを担当します
effort: low
---
## 担当する作業
- 文言・表記ゆれの修正
- リネーム、import の整理
## 担当しない作業
- 設計判断を伴う変更
- 依頼に書かれていない周辺の整理
## 完了の条件
- 依頼された箇所以外のファイルを変更していないこと
- 既存のテストが通ること
「担当しない作業」を明記した点が効きました。役割の説明だけを書いていた頃は、周辺整理が入り込む余地が残っていました。やらないことを先に書く方が、境界がはっきりします。
定義ファイルはプレーンテキストですので、そのままバージョン管理へ載せられます。私は変更をコミットに残し、採用率が動いたときに定義側の変更と突き合わせられるようにしております。段階を変えたのか、指示文を変えたのか、後から切り分けられる状態を保つためです。
なお、フロントマターの effort を CLI が解釈するかはバージョン依存です。手元では起動時のログで反映を確認してから運用へ入れました。反映されない場合はルーター側の policy に残す形でも同じ効果が得られます。
導入するときの順序
一度に全部を入れると、どの変更が効いたのか分からなくなります。私は次の順で進めました。
- 1週間、何もせずログだけ取る —
effort-route.mjs を --dry-run で挟み、分類結果と現在の段階だけを記録します。この時点では挙動を変えません
- 判定の習慣をつける — 差分をレビューしたら
--mark <id> accept|rework を打つ。ここが続かないと以降の数値が出ません
- mechanical だけを low に落とす — 影響が読みやすく、戻すのも簡単な種別から始めます
- 2週間分を集計して比較する — 前後で一発採用率と p50 を並べ、悪化していないかを確認します
- 依頼文に「完了とみなす条件」の行を足す — 段階の調整より先に、ここを埋める習慣を作ります
- エージェント定義へ既定値を移す — ルーターと定義ファイルの二重管理を解消するのは最後で構いません
状況別の当て方は、次のように考えております。
| 状況 | おすすめの段階 | 理由 |
| 正解が事前に決まっている修正 | low | 探索の余地が余計な差分になります |
| 変更範囲が1〜2ファイルに収まる | medium | 復帰の丁寧さと速度の折り合いが良い帯です |
| 範囲が読めない設計変更・移行 | high | テスト失敗時に原因まで遡る確率が上がります |
| 調査・比較・方針の相談 | high | 差分が出ないため、速度より深さを取ります |
| 依頼文がまだ曖昧なとき | 触らない | 段階ではなく、条件を書く方が効きます |
まとめ
計測して分かったことを3行に畳むと、次のようになります。
正解が事前に決まっている作業には、探索の余地を渡さない。範囲が読めない作業には渡す。そして、段階を触る前に完了の条件を1行書く。
もし手元で試されるのでしたら、最初の一歩は集計スクリプトの方をおすすめいたします。振り分けの設計は自分の作業内容によって変わりますが、「今どの段階で何を投げていて、どれくらい戻しているか」が見えないうちは、どの設計が正しいかも判断できません。1週間ログを取るだけで、意外な偏りが見えてくるはずです。
私自身まだ調整の途中で、exploratory の物差しは決めきれておりません。同じ課題に取り組まれている方がいらっしゃいましたら、どこで線を引かれたか伺ってみたいところです。お読みいただきありがとうございました。