Summary
specify_cli.extensions._load_core_command_names() never discovers the bundled command templates. Both candidate paths are off by one directory, so the function silently returns the hardcoded _FALLBACK_CORE_COMMAND_NAMES on every call — in both wheel installs and source checkouts.
Root cause
The function resolves its command dirs with bespoke Path(__file__) math:
candidate_dirs = [
Path(__file__).parent / "core_pack" / "commands",
Path(__file__).resolve().parent.parent.parent / "templates" / "commands",
]
These were correct when the code lived at src/specify_cli/extensions.py (anti-shadowing guard added in #1994). The refactor that moved it into the package at src/specify_cli/extensions/__init__.py (#3014, 826e193 — diff shows {extensions.py => extensions/__init__.py}) pushed the file one directory deeper but did not update the parent counts. They now resolve to:
- wheel →
specify_cli/extensions/core_pack/commands — real path is specify_cli/core_pack/commands
- source →
src/templates/commands — real path is repo-root templates/commands
Neither exists, so commands_dir.is_dir() is always false and the loop falls through to the fallback set.
This is the same off-by-one class @mnriem identified for the presets loader (parent.parent.parent after the #2826 presets.py → presets/__init__.py move, re #3086), but in the extensions module — which the preset-scoped fix does not touch.
Reproduction
From a source checkout:
from specify_cli.extensions import _load_core_command_names, _FALLBACK_CORE_COMMAND_NAMES
# Discovery is meant to read repo-root templates/commands. Instead:
assert _load_core_command_names() == _FALLBACK_CORE_COMMAND_NAMES # passes — discovery is dead
Both candidate dirs resolve to non-existent paths (verifiable by printing them).
Impact (honest assessment)
Currently latent, not user-visible: _FALLBACK_CORE_COMMAND_NAMES happens to equal the 10 real command stems today, so nothing breaks right now.
The risk is silent drift. CORE_COMMAND_NAMES (derived from this function) guards extensions against shadowing core command names (#1994). With dynamic discovery dead, that guard depends entirely on someone hand-editing the fallback whenever a core command is added or removed — which has already happened: converge was manually appended to _FALLBACK_CORE_COMMAND_NAMES in #3001 (0c29d89). A future add/remove that forgets the fallback would silently desync the guard.
Proposed fix
Delegate path resolution to the canonical _assets resolvers (_locate_core_pack / _repo_root), exactly as presets/__init__.py already does. They are anchored to the package root, so discovery is immune to future module moves. Add regression tests that pin live discovery (they fail on the current code and pass after the fix).
Filed by @v-dhruv with assistance from GitHub Copilot (model: Claude Opus 4.8). The investigation and proposed patch are agent-generated and human-directed.
Summary
specify_cli.extensions._load_core_command_names()never discovers the bundled command templates. Both candidate paths are off by one directory, so the function silently returns the hardcoded_FALLBACK_CORE_COMMAND_NAMESon every call — in both wheel installs and source checkouts.Root cause
The function resolves its command dirs with bespoke
Path(__file__)math:These were correct when the code lived at
src/specify_cli/extensions.py(anti-shadowing guard added in #1994). The refactor that moved it into the package atsrc/specify_cli/extensions/__init__.py(#3014,826e193— diff shows{extensions.py => extensions/__init__.py}) pushed the file one directory deeper but did not update the parent counts. They now resolve to:specify_cli/extensions/core_pack/commands— real path isspecify_cli/core_pack/commandssrc/templates/commands— real path is repo-roottemplates/commandsNeither exists, so
commands_dir.is_dir()is always false and the loop falls through to the fallback set.This is the same off-by-one class @mnriem identified for the presets loader (
parent.parent.parentafter the #2826presets.py→presets/__init__.pymove, re #3086), but in the extensions module — which the preset-scoped fix does not touch.Reproduction
From a source checkout:
Both candidate dirs resolve to non-existent paths (verifiable by printing them).
Impact (honest assessment)
Currently latent, not user-visible:
_FALLBACK_CORE_COMMAND_NAMEShappens to equal the 10 real command stems today, so nothing breaks right now.The risk is silent drift.
CORE_COMMAND_NAMES(derived from this function) guards extensions against shadowing core command names (#1994). With dynamic discovery dead, that guard depends entirely on someone hand-editing the fallback whenever a core command is added or removed — which has already happened:convergewas manually appended to_FALLBACK_CORE_COMMAND_NAMESin #3001 (0c29d89). A future add/remove that forgets the fallback would silently desync the guard.Proposed fix
Delegate path resolution to the canonical
_assetsresolvers (_locate_core_pack/_repo_root), exactly aspresets/__init__.pyalready does. They are anchored to the package root, so discovery is immune to future module moves. Add regression tests that pin live discovery (they fail on the current code and pass after the fix).Filed by @v-dhruv with assistance from GitHub Copilot (model: Claude Opus 4.8). The investigation and proposed patch are agent-generated and human-directed.