Problem
components/<id>/component.json declares an eight-field environment block, and
./install.sh --environments renders all eight as TSV. Four of the fields are decorative.
Every consumer reads them and then derives the value from the identifier instead.
bootstrap/lib/environments.sh:81 reads the whole record:
while IFS=$'\t' read -r id status container ini packages toolchain \
bootstrap router inference home; do
[ -n "$id" ] && [ "$container" != - ] || continue
create_development_environment "$repo_root" "$container"
create_development_environment then rebuilds the path the module already declared:
# bootstrap/lib/environments.sh:64
run distrobox assemble create --file "$repo_root/distrobox/$name.ini" &&
$ini holds distrobox/web-dev.ini. It is read into a variable and never referenced.
bootstrap/lib/devbox.sh:66-69 has the same shape: it reads ten fields to use id and
router.
components/python-dev/install.sh:26 hard-codes box_bootstrap=(./bootstrap/python-dev.sh)
rather than reading the bootstrap field its own manifest declares.
The toolchain field is also a half-truth. bootstrap/python-dev.sh:29-33 sources two
manifests, and the module declares one:
. "$REPO_ROOT/manifests/python-dev.env"
# Agent installer URLs are currently shared with web-dev in this manifest.
. "$REPO_ROOT/manifests/toolchain.env"
Check E3 in verify/17-environments.sh:31-37 can only prove that a declared path exists. It
cannot see the undeclared one.
A distribution-specific command in a shared library
manifests/capabilities.json declares container-runtime as an any probe over podman
and docker:
{
"id": "container-runtime",
"summary": "an OCI container runtime",
"probe": { "kind": "any", "value": [
{ "kind": "command", "value": "podman" },
{ "kind": "command", "value": "docker" } ] }
}
bootstrap/lib/environments.sh:60 asks one runtime:
if podman container exists "$name" 2>/dev/null; then
ok "container $name already exists (left untouched)"
On a machine that has Docker and not Podman, the capability check passes, the existence
probe fails on every run, and distrobox assemble create is attempted again. AGENTS.md
rule 3 requires an installer to check the current state first and change only what does not
match, so the rule does not hold on that machine.
AGENTS.md rule 11 states that manifests/platforms.json is the only place that names a
distribution, a package manager or a distribution-specific command. Check P5 in
verify/16-platform.sh:16 searches for bazzite|ujust|rpm-ostree|apt-get|pacman|dnf. It
does not catch podman.
bootstrap/lib/host-packages.sh:18-19 has the same shape in the other direction:
for candidate in "$(command -v brew 2>/dev/null || true)" \
/home/linuxbrew/.linuxbrew/bin/brew; do
manifests/capabilities.json declares the same two locations, and the macOS adapter in
manifests/platforms.json declares /opt/homebrew and /usr/local. The library already
sources platform.sh and calls platform_hint for the failure message at :26. It asks
the adapter how to obtain Homebrew, and not where to find it.
A manual list stated twice
bootstrap/host.sh:141-151 hard-codes reminders that the manifests already declare:
manual 'Restore the SSH key (see docs/secrets.md), then: ssh-add ~/.ssh/id_ed25519'
manual 'Authenticate GitHub on the host: gh auth login --git-protocol ssh'
The same sentences appear in the manual arrays of components/daniel/component.json and
components/agentbox/component.json. Plan.manual in bin/toolkit-install:153-162 already
computes the de-duplicated set for a profile, and it has a unit test. The copies have
drifted: bootstrap/host.sh never states the Claude and Codex sign-in that
components/web-dev/component.json declares.
verify/17-environments.sh:48 hard-codes for expected in android-dev dotnet-dev python-dev rust-dev web-dev, nine lines after check E6 fails any file in bootstrap/ that names an
environment.
Goal
Make the environment block an interface that its consumers read, and keep a runtime-specific
command behind the capability layer.
Proposed direction
- Read every declared field by name.
environments.sh reads $ini. install.sh reads the
bootstrap field. A module can then name a file that does not follow the convention, and
adding an environment is adding one directory.
- Declare every manifest an environment reads, including the shared
manifests/toolchain.env.
- Add a lookup beside
platform_hint that answers where a capability lives, and let
environments.sh and host-packages.sh use it. Move podman and
/home/linuxbrew/.linuxbrew/bin/brew behind it.
- Extend check P5 beyond package-manager names, so a hard-coded runtime command or install
location fails.
- Let
bootstrap/host.sh read its manual list from the daniel closure.
- Replace the hard-coded environment names in
verify/17-environments.sh:48 with the module
list that --environments already emits.
The alternative is to delete ini, packages, toolchain and bootstrap and state the
convention once. Either is better than a declared interface that its own implementation
ignores. Reading the fields gives more leverage, because --environments then becomes the
one interface between the Python resolver and the shell half.
Acceptance criteria
Problem
components/<id>/component.jsondeclares an eight-field environment block, and./install.sh --environmentsrenders all eight as TSV. Four of the fields are decorative.Every consumer reads them and then derives the value from the identifier instead.
bootstrap/lib/environments.sh:81reads the whole record:create_development_environmentthen rebuilds the path the module already declared:$iniholdsdistrobox/web-dev.ini. It is read into a variable and never referenced.bootstrap/lib/devbox.sh:66-69has the same shape: it reads ten fields to useidandrouter.components/python-dev/install.sh:26hard-codesbox_bootstrap=(./bootstrap/python-dev.sh)rather than reading the
bootstrapfield its own manifest declares.The
toolchainfield is also a half-truth.bootstrap/python-dev.sh:29-33sources twomanifests, and the module declares one:
Check E3 in
verify/17-environments.sh:31-37can only prove that a declared path exists. Itcannot see the undeclared one.
A distribution-specific command in a shared library
manifests/capabilities.jsondeclarescontainer-runtimeas ananyprobe overpodmanand
docker:{ "id": "container-runtime", "summary": "an OCI container runtime", "probe": { "kind": "any", "value": [ { "kind": "command", "value": "podman" }, { "kind": "command", "value": "docker" } ] } }bootstrap/lib/environments.sh:60asks one runtime:On a machine that has Docker and not Podman, the capability check passes, the existence
probe fails on every run, and
distrobox assemble createis attempted again.AGENTS.mdrule 3 requires an installer to check the current state first and change only what does not
match, so the rule does not hold on that machine.
AGENTS.mdrule 11 states thatmanifests/platforms.jsonis the only place that names adistribution, a package manager or a distribution-specific command. Check P5 in
verify/16-platform.sh:16searches forbazzite|ujust|rpm-ostree|apt-get|pacman|dnf. Itdoes not catch
podman.bootstrap/lib/host-packages.sh:18-19has the same shape in the other direction:manifests/capabilities.jsondeclares the same two locations, and the macOS adapter inmanifests/platforms.jsondeclares/opt/homebrewand/usr/local. The library alreadysources
platform.shand callsplatform_hintfor the failure message at:26. It asksthe adapter how to obtain Homebrew, and not where to find it.
A manual list stated twice
bootstrap/host.sh:141-151hard-codes reminders that the manifests already declare:The same sentences appear in the
manualarrays ofcomponents/daniel/component.jsonandcomponents/agentbox/component.json.Plan.manualinbin/toolkit-install:153-162alreadycomputes the de-duplicated set for a profile, and it has a unit test. The copies have
drifted:
bootstrap/host.shnever states the Claude and Codex sign-in thatcomponents/web-dev/component.jsondeclares.verify/17-environments.sh:48hard-codesfor expected in android-dev dotnet-dev python-dev rust-dev web-dev, nine lines after check E6 fails any file inbootstrap/that names anenvironment.
Goal
Make the environment block an interface that its consumers read, and keep a runtime-specific
command behind the capability layer.
Proposed direction
environments.shreads$ini.install.shreads thebootstrapfield. A module can then name a file that does not follow the convention, andadding an environment is adding one directory.
manifests/toolchain.env.platform_hintthat answers where a capability lives, and letenvironments.shandhost-packages.shuse it. Movepodmanand/home/linuxbrew/.linuxbrew/bin/brewbehind it.location fails.
bootstrap/host.shread its manual list from thedanielclosure.verify/17-environments.sh:48with the modulelist that
--environmentsalready emits.The alternative is to delete
ini,packages,toolchainandbootstrapand state theconvention once. Either is better than a declared interface that its own implementation
ignores. Reading the fields gives more leverage, because
--environmentsthen becomes theone interface between the Python resolver and the shell half.
Acceptance criteria
create_development_environmentuses the declaredinipath.components/*/install.shuses the declaredbootstrappath.inipath does not follow the convention, and proves the declared path is used. It needs no container.bootstrap/host.shstates no manual step that a component manifest already declares.verify/17-environments.shnames no environment identifier../verify.shremains green.