0022: spec-kit is installed per project by `specify init` at provisioning, and Orion pins the feature directory
- Status: Accepted
- Date: 2026-09-07
- Amends: 0020 β vendoring stays global for a skills repository; it is not how spec-kit is installed.
- Related: 0009, 0014, 0021
Contextβ
0020 decided
two things: a toolkit ships skills/<name>/SKILL.md, and every clone lands
under <ORION_HOME>/vendor/<repo-name>, shared by every project on the
machine. Its own postscript then found that a clone of spec-kit is not an
install. spec-kit's commands live in templates/commands/ as inputs to its
installer; specify init --here --integration claude writes
.claude/skills/speckit-*/SKILL.md into the project β the layout Validate
wanted, produced per project. The amendment was stated in a postscript and
nowhere a reader configuring a toolkit would find it, and the stale clone
under vendor/spec-kit kept making orion doctor grade a directory no
stage could run.
Where that installer runs is decided by what it is, not by the network:
specify init --help states that "project files are scaffolded from assets
bundled inside the specify-cli package, so initialization does not need
network access". It is provisioning -- deterministic, no model, the same
class of work as creating the remote -- and it has to have happened before
the first stage that delegates to spec-kit reads the commands it writes. So
it runs in Orion's own process as a frame step of the chain, not inside a
stage. (An earlier draft of this record said the installer downloads and
that egress denial forced the placement; the placement is right and that
reason was wrong.)
One fact about spec-kit's own resolution decides what Orion must tell it.
/speckit-specify derives a two-to-four-word name from the description and
creates specs/NNN-<that-name>/, persisting the choice to
.specify/feature.json, which is gitignored and machine-local. Its
resolution order (scripts/bash/common.sh, get_feature_paths) reads the
SPECIFY_FEATURE_DIRECTORY environment variable first and only invents a
name when it is unset. Left unset, one piece of work has two names β the
slug 0009 exists to make unique, and a
directory spec-kit made up β recorded in a file that is not committed.
Decisionβ
orion plan installs spec-kit into the workspace repository as a frame
step, before any stage, and skips when it is already there. The step runs
specify init --here --force --non-interactive --integration claude --
non-interactive because a chain has nobody at the installer's prompt, force
because a provisioned workspace is never an empty directory -- then specify preset add --dev .specify/presets/orion for the preset
0021 adopts, and commits what they wrote.
A project whose stages name no spec-kit command has nothing to install and
the step reports done. .specify/ present means done; a
resumed chain never re-initialises. specify absent is an error naming the
install line (uv tool install specify-cli --from git+https://github.com/github/spec-kit.git), raised before anything spends.
Orion exports SPECIFY_FEATURE_DIRECTORY=<paths.specs>/001-<slug> to
every supervised run. One helper, config.FeatureDir(slug), spells that
path for the prompt, the artifact gate, the discovery gate and the
environment, so the four cannot disagree. spec-kit honours the variable
first and persists it to .specify/feature.json, so the machine-local file
records Orion's name rather than inventing one.
Vendoring stays global for a skills repository and is not used for
spec-kit. <ORION_HOME>/vendor/<repo-name> remains where nj-agents and
any toolkit laid out as skills is cloned. orion doctor resolves a toolkit
installed inside the project β .claude/skills or .claude/commands under
the project root β as the first candidate, grades every toolkit.stages
command against a file that exists there or in the toolkit root, and FAILs
naming the stage when one does not. orion doctor --fix never clones
spec-kit; it prints the install line instead. The stale
~/.orion/vendor/spec-kit clone is deleted.
orion doctor reads spec-kit's version and feature roster. specify version --features --json returns {"version": β¦, "features": {β¦}}; the
feature keys are what Orion depends on, not the version string, which on a
development build (1.0.5.dev0) does not compare as semver. A missing
required feature is a WARN naming it and uv tool upgrade specify-cli. No
network is used for this; there is no "latest release" comparison.
Consequencesβ
- 0020's header gains
Amended by: 0022. Its first decision β a toolkit ships the skills layout β stands unchanged;specify initis precisely what produces that layout. - Two projects on one machine may run different spec-kit versions, which 0020 listed as the reason to revisit global vendoring. Accepted: doctor reports the version per project, and a per-project install is what spec-kit's own documentation describes.
- Under a delegated
specorplanstage, the artifact Orion owes is<FeatureDir>/spec.md,<FeatureDir>/plan.mdand<FeatureDir>/tasks.md, and the artifact gate checks those files. Without delegation the oldspecs/<slug>.spec.mdandplans/<slug>.mdpaths are unchanged. Go still decides which artifact a stage owes (internal/supervisor/artifact.go); a configured command selects between two layouts Orion owns and can name no other path. 001-is pinned for the chain's feature. A second feature on an existing project needs a counter and an entry point; both are out of scope here and recorded in 0023.- The CLI is pinned to one release (
provision.SpecKitTag): the gates read strings its templates contain, and a contract test against the real CLI (speckit_contract_test.go, skipped wherespecifyis absent) is what makes moving the pin a checked change rather than a discovered one. specify preset add --dev <path>copies the preset into the project's own.specify/presets/orion/and recomposes the installed skill in place; nothing is written under~/.specify. Verified on 1.0.5.dev0, so "per project" above is exact, and the preset's presence there is what the toolkit step checks before deciding it has nothing to do.