やってみた 2026年8月15日

spec-kitをAIエージェント抜きで動かした。SPECIFY_FEATUREで指定した機能とは別のフォルダにplan.mdが出た

XECIN Python開発ツールCLIプロセス設計

GitHubのトレンドで github/spec-kit が上がっているのを見かけました。Spec-Driven Development(仕様駆動開発)のツールキット、スター128,833。

私の職業病だと思うのですが、この手のものを見ると真っ先に浮かぶのが「で、結局リポジトリに何が置かれるんですか」という問いなんですよね。プロセス系のフレームワークは、思想の説明と、実際にチームの手元に残る成果物との間に、だいたい距離があります。現場でテーラリングを判断するときに効くのは前者ではなく後者です。

なので今回は、思想の話は一旦脇に置いて、使い捨てのコンテナに specify を入れて、AIコーディングエージェントを一切繋がない状態で触ってみました。エージェントが無いと何ができないのか、逆に何が単体で動くのか。そこが分かれば、導入検討のときに「どこまでを自分たちのプロセスとして引き受けるか」を判断できます。

結果としては、導入の軽さは想像以上でした。ただ、素の状態でスクリプトを叩いていたら、指定したはずの機能とは違うフォルダに plan.md が書かれる挙動に当たりました。今回はそこが主題になります。

試した環境

ホストを汚したくないので、検証は使い捨ての Docker コンテナの中だけで完結させています。以下の数値はすべてその中での実測です。

  • 対象: github/spec-kit(PyPI 経由で specify-cli==0.16.4、リリースタグ v0.16.4、MIT)
  • ベースイメージ: python:3.12-bookworm(Python 3.12.13 / Debian GNU/Linux 12 / x86_64)
  • リソース上限: メモリ 4GB / CPU 2
  • AIコーディングエージェント: 一切インストールしていない

入れるところは、拍子抜けするほど速い

まず導入。README には uv tool install specify-cli とあります。

# uv を入れる
$ pip install -q uv && uv --version
uv 0.12.5 (x86_64-unknown-linux-gnu)
uv_install_sec=2

# specify-cli 本体
$ uv tool install specify-cli
Prepared 15 packages in 156ms
Installed 15 packages in 44ms
 + click==8.4.2
 + rich==15.0.0
 + specify-cli==0.16.4
 + typer==0.27.1
 (他11パッケージ)
Installed 1 executable: specify
specify_install_sec=1

# 依存ツリーの実サイズ
$ du -sh /root/.local/share/uv/tools/specify-cli
22M

# エージェントを1つも入れていない状態で specify check を叩く
$ specify check
Check Available Tools
├── Antigravity (not found)
├── Claude Code (not found)
├── GitHub Copilot (IDE-based, no CLI check)
├── Codex CLI (not found)
(…全39件)
└── Visual Studio Code Insiders (not found)

Specify CLI is ready to use!
Tip: Install a coding agent for the best experience

$ specify check >/dev/null 2>&1; echo $?
0

1秒、15パッケージ、22MB。依存は typer / rich / pyyaml あたりの純Pythonだけで、ビルドも外部バイナリもありません。128k スターのリポジトリを前にすると身構えてしまいますが、CLI 本体は素直な Python パッケージでした。

手が止まったのは、ひとつ前のコードブロックの後半です。39個のエージェントを見に行って、CLI系は全部 not found、IDE系は「IDEベースなのでCLIチェック無し」。それでいて最後は「ready to use」で、終了コードは 0 でした(パイプを挟むと $? が別物になるので、リダイレクトで測っています)。

つまり check は前提条件のゲートではなく、あくまで案内です。理屈はそうなんですが、check という名前のコマンドが常に成功するのは、CI に組み込むときには一度気に留めておいたほうがいい性質だと思います。

何がリポジトリに置かれるのか

本題の init です。ヘルプに「初期化はネットワークアクセスを必要とせず、テンプレートはインストール済み CLI のバージョンと一致する」と明記されていて、実際そのとおりでした。

$ specify init my-project --integration copilot --ignore-agent-tools
Project ready.
RC=0
init_sec=0

$ find . -type f | wc -l
30
$ du -sh .
376K

# 内訳(行数)
$ wc -l .specify/scripts/bash/*.sh | tail -1
 1791 total
$ cat .github/skills/*/SKILL.md | wc -l
2447
$ wc -l .specify/templates/*.md
   45 checklist-template.md
   50 constitution-template.md
  113 plan-template.md
  131 spec-template.md
  252 tasks-template.md
  591 total

# git リポジトリは作られない
$ [ -d .git ] && echo "GIT YES" || echo "GIT NO"
GIT NO

体感ゼロ秒、30ファイル、376KB。置かれるものは大きく3つでした。

置かれるもの実体実測
エージェント向けの指示書.github/skills/speckit-*/SKILL.md10ファイル / 計2,447行
シェルスクリプト.specify/scripts/bash/*.sh6ファイル / 計1,791行
ドキュメントの雛形.specify/templates/*.mdmemory/constitution.md5ファイル / 計591行

constitution.md(プロジェクトの原則を書く場所)は # [PROJECT_NAME] Constitution ### [PRINCIPLE_1_NAME] のようなプレースホルダのままです。spec-template.md にも NEEDS CLARIFICATION のマーカーが2箇所あって、埋めるのは人間かエージェントの仕事、という作りになっています。

ここは正直に言うと好感を持ちました。プロセス系のツールは「最初から答えが埋まっている」ほうが導入は楽なのですが、その答えは他所の現場のものなので、結局あとで剥がすことになります。空欄で渡してくるのは、テーラリングを前提にした設計だと受け取りました。

エージェント無しでも、スクリプトは普通に動く

.specify/scripts/bash/ に置かれた6本は、エージェントから呼ばれる前提のヘルパーです。ただ、中身は素の bash なので直接叩けます。

# git リポジトリでない場所でも通る
$ .specify/scripts/bash/create-new-feature.sh --json "add csv export to the report screen"
{"BRANCH_NAME":"001-csv-export-report-screen","SPEC_FILE":"/work/my-project/specs/001-csv-export-report-screen/spec.md","FEATURE_NUM":"001"}
RC=0

# --json の出力ストリームはきちんと分かれている
$ .specify/scripts/bash/create-new-feature.sh --json "fourth feature" 2>/dev/null   # stdout のみ
{"BRANCH_NAME":"003-fourth-feature","SPEC_FILE":"/work/my-project/specs/003-fourth-feature/spec.md","FEATURE_NUM":"003"}

$ .specify/scripts/bash/create-new-feature.sh --json "fourth feature" 2>&1 1>/dev/null  # stderr のみ
# To persist: export SPECIFY_FEATURE=003-fourth-feature
#              export SPECIFY_FEATURE_DIRECTORY=/work/my-project/specs/003-fourth-feature

init は git リポジトリを作らないのに、create-new-feature.sh は git が無くても連番を振って specs/001-.../spec.md を作ってくれます。番号は specs/ 配下を走査して次を採る方式でした。

そして --json を付けたときの stdout は純粋な JSON 一行で、人間向けのヒントは stderr に出ます。この分離はきちんとしています。JSON をそのままパースする側から見ると地味に重要なところです。

……で、この stderr のヒントが、今回のハマりどころの入り口でした。

指定したはずの機能と違うフォルダに書かれる

ヒントは SPECIFY_FEATURESPECIFY_FEATURE_DIRECTORY の両方を export しろと言っています。私は最初、前者だけで足りるだろうと思ったんですね。名前からして、こちらが「いま作業中の機能」を決める変数に見えたので。

新しいプロジェクトを作り直して、機能を3つ(001-alpha-report / 002-bravo-import / 003-charlie-audit)作った状態で、読み取り専用の check-prerequisites.sh --json --paths-only を使って1ケース1プロセスで測りました。この時点で .specify/feature.json{"feature_directory":"specs/003-charlie-audit"} を指しています。

# A: 対照群(環境変数なし)
{"BRANCH":"003-charlie-audit","FEATURE_DIR":"/work/exp/specs/003-charlie-audit", ...}

# B: SPECIFY_FEATURE=001-alpha-report だけ
{"BRANCH":"001-alpha-report","FEATURE_DIR":"/work/exp/specs/003-charlie-audit", ...}

# C: SPECIFY_FEATURE_DIRECTORY=specs/001-alpha-report だけ
{"BRANCH":"001-alpha-report","FEATURE_DIR":"/work/exp/specs/001-alpha-report", ...}

# E: SPECIFY_FEATURE=999-does-not-exist(存在しない機能)
{"BRANCH":"999-does-not-exist","FEATURE_DIR":"/work/exp/specs/003-charlie-audit", ...}

# F: SPECIFY_FEATURE_DIRECTORY=specs/999-does-not-exist(存在しないフォルダ)
{"BRANCH":"999-does-not-exist","FEATURE_DIR":"/work/exp/specs/999-does-not-exist", ...}
paths_only_missing_dir_rc=0

# ここから書き込み側。ケースBと同じ指定で setup-plan.sh を叩く
$ SPECIFY_FEATURE=001-alpha-report .specify/scripts/bash/setup-plan.sh --json
{"FEATURE_SPEC":"/work/exp/specs/003-charlie-audit/spec.md",
 "IMPL_PLAN":"/work/exp/specs/003-charlie-audit/plan.md",
 "SPECS_DIR":"/work/exp/specs/003-charlie-audit",
 "BRANCH":"001-alpha-report"}
RC=0

$ find specs -name plan.md
specs/003-charlie-audit/plan.md

表にすると、どちらの変数が何を動かしているかがはっきりします。

ケース与えた環境変数BRANCHFEATURE_DIR(実際に読み書きされる場所)
A(対照)なし003-charlie-audit003-charlie-audit
BSPECIFY_FEATURE=001-alpha-report001-alpha-report003-charlie-audit(指定が効かない)
CSPECIFY_FEATURE_DIRECTORY=specs/001-alpha-report001-alpha-report001-alpha-report
D両方001-alpha-report001-alpha-report
ESPECIFY_FEATURE=999-does-not-exist999-does-not-exist003-charlie-audit(エラーなし)
FSPECIFY_FEATURE_DIRECTORY=specs/999-does-not-exist999-does-not-exist999-does-not-exist(存在確認なし・終了コード0)

ケースBが今回の本丸です。SPECIFY_FEATURE に 001 を渡すと、返ってくる JSON の BRANCH は素直に 001-alpha-report になります。ところが実際に読み書きされる FEATURE_DIR は 003 のままなんですよね。出力を見ている限りは指定が通ったように見えるのに、対象は別物という状態です。

読み取り専用のコマンドで終わっているうちは実害がありません。実害が出るのは書き込み側で、上のコードブロックの末尾がそれです。同じ JSON の中で BRANCH は 001、書き込み先は 003。終了コードは 0。alpha の実装計画を作ったつもりで、charlie の下に plan.md が生えました。

なぜこうなるのか

原因は .specify/scripts/bash/common.sh(918行)に書いてあって、読んでみると挙動としては筋が通っていました。

  • get_current_branch()SPECIFY_FEATURE があればそれを返すだけ。これが JSON の BRANCH になる
  • 実際の作業ディレクトリを決める get_feature_paths() の優先順位は、コメントに明記されていて (1) SPECIFY_FEATURE_DIRECTORY → (2) .specify/feature.jsonfeature_directory → (3) エラーSPECIFY_FEATURE はこの並びに入っていない
  • feature.jsoncreate-new-feature.sh が最後に書き換える。だから「直近に作った機能」に固定される

さらに、エージェント向けの speckit-specify/SKILL.md には「ブランチ名は spec ディレクトリ名を決定しない」「spec ディレクトリ名と git ブランチ名は独立している」とはっきり書かれています。つまり 分離は意図された設計であって、バグというより、名前とヒントの見え方の問題です。

面白かったのは、エージェント向け指示書10本のうち、speckit-specify だけが .specify/scripts/bash/ を一度も参照していなかったことです。他の9本は check-prerequisites.sh などを呼ぶのに、仕様作成の入口だけは「エージェント自身が mkdir してテンプレートをコピーして feature.json を書け」と指示されています。

$ for f in .github/skills/*/SKILL.md; do
    echo "$(grep -c 'scripts/bash\|check-prerequisites\|create-new-feature\|setup-plan' "$f")  $f"
  done
1  .github/skills/speckit-analyze/SKILL.md
1  .github/skills/speckit-constitution/SKILL.md
1  .github/skills/speckit-implement/SKILL.md
1  .github/skills/speckit-plan/SKILL.md
0  .github/skills/speckit-specify/SKILL.md ここだけ 0
1  .github/skills/speckit-tasks/SKILL.md
(他4本は 1)

エージェント経由の正規ルートを通れば、feature.json は毎回きちんと更新されるので、この食い違いは起きにくい構造になっています。逆に言うと、私がやったようにスクリプトを直接叩く運用に寄せると、この前提が外れるわけです。

現場で spec-kit をパイプラインに組み込む場合、.specify/scripts/bash/ を CI から直接呼びたくなる場面はかなりあると思います。そのときの実務上の結論はシンプルで、機能を固定したいなら SPECIFY_FEATURE_DIRECTORY を使うSPECIFY_FEATURE は表示用のラベルだと割り切る。ケースFのとおり存在しないパスでも終了コード0で通ってしまうので、呼び出し側で FEATURE_DIR が期待どおりかを assert しておくのが安全です。

ここは好みが分かれるところですが、私は create-new-feature.sh が出すヒントの並び順(SPECIFY_FEATURE が先、SPECIFY_FEATURE_DIRECTORY が後)が、片方だけ拾う動機を作ってしまっている気がしています。

その他、触ってみて分かったこと

  • specify preset listNo presets installed.specify workflow listFull SDD Cycle (speckit) v1.0.0 が1本だけ。初期状態はかなり素です
  • init の直後に「エージェントフォルダに認証情報が残ることがあるので .github/.gitignore に入れることを検討してください」という警告が出ます。エージェント連携ツールとしては誠実な注意書きだと思いました
  • --integration copilot を指定しても入るのはスキル定義のマークダウンだけで、Copilot 本体を要求されることはありませんでした

試してみての所感

導入コストという意味では、想像していたよりずっと軽いツールでした。1秒で入って、376KB のファイルがリポジトリに置かれて、あとは全部マークダウンとシェルスクリプト。ベンダーロックの匂いがしないのは、素直に良いところだと思います。

一方で、フレームワークは目的ではなく道具なので、導入判断で見るべきは「置かれた2,447行のスキル定義と591行のテンプレートを、自分たちのプロセスとして本当に維持できるか」のほうです。空欄の constitution.md を誰が埋めて、誰がレビューして、誰が腐らせないのか。そこが決まっていない状態で入れると、教科書どおりにやって重すぎて回らなくなる、いつものパターンになります。私自身、過去にそれで一度失敗しています。

私はまず、specs/ の運用ルール(1機能1ディレクトリ、番号の採り方、レビューのタイミング)だけを決めて、スキル定義は3〜4本に絞って始めるつもりです。全部入りで始めるのは、たぶん二周目でいい。

そして CI に載せるときは、SPECIFY_FEATURE_DIRECTORY を明示して、FEATURE_DIR を検証してから次の処理へ進む。今回の実測で一番持ち帰る価値があったのは、その一行だったりします。