Skip to content

Commit b5757e6

Browse files
committed
fix: resolve command references per integration type (dot vs hyphen)
Replace hardcoded /speckit.<cmd> references in templates with __SPECKIT_COMMAND_<NAME>__ placeholders that are resolved at setup time based on the integration type: - Markdown/TOML/YAML agents: separator='.' → /speckit.plan - Skills agents: separator='-' → /speckit-plan Changes: - Add resolve_command_refs() static method to IntegrationBase - Add invoke_separator class attribute (. for base, - for skills) - Wire into process_template() as step 8 - Update _install_shared_infra() to process page templates - Replace /speckit.* in 5 command templates and 3 page templates - Add unit tests for resolve_command_refs (positive + negative) - Add integration tests verifying on-disk content for all agents - Add end-to-end CLI tests for Claude (skills) and Copilot (markdown) Fixes #2347
1 parent 7f708b9 commit b5757e6

20 files changed

Lines changed: 274 additions & 30 deletions

src/specify_cli/__init__.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -723,19 +723,25 @@ def _install_shared_infra(
723723
script_type: str,
724724
tracker: StepTracker | None = None,
725725
force: bool = False,
726+
invoke_separator: str = ".",
726727
) -> bool:
727728
"""Install shared infrastructure files into *project_path*.
728729
729730
Copies ``.specify/scripts/`` and ``.specify/templates/`` from the
730731
bundled core_pack or source checkout. Tracks all installed files
731732
in ``speckit.manifest.json``.
732733
734+
Page templates are processed to resolve ``__SPECKIT_COMMAND_<NAME>__``
735+
placeholders using *invoke_separator* (``"."`` for markdown agents,
736+
``"-"`` for skills agents).
737+
733738
When *force* is ``True``, existing files are overwritten with the
734739
latest bundled versions. When ``False`` (default), only missing
735740
files are added and existing ones are skipped.
736741
737742
Returns ``True`` on success.
738743
"""
744+
from .integrations.base import IntegrationBase
739745
from .integrations.manifest import IntegrationManifest
740746

741747
core = _locate_core_pack()
@@ -786,7 +792,11 @@ def _install_shared_infra(
786792
if dst.exists() and not force:
787793
skipped_files.append(str(dst.relative_to(project_path)))
788794
else:
789-
shutil.copy2(f, dst)
795+
content = f.read_text(encoding="utf-8")
796+
content = IntegrationBase.resolve_command_refs(
797+
content, invoke_separator
798+
)
799+
dst.write_text(content, encoding="utf-8")
790800
rel = dst.relative_to(project_path).as_posix()
791801
manifest.record_existing(rel)
792802

@@ -1295,7 +1305,7 @@ def init(
12951305

12961306
# Install shared infrastructure (scripts, templates)
12971307
tracker.start("shared-infra")
1298-
_install_shared_infra(project_path, selected_script, tracker=tracker, force=force)
1308+
_install_shared_infra(project_path, selected_script, tracker=tracker, force=force, invoke_separator=resolved_integration.invoke_separator)
12991309
tracker.complete("shared-infra", f"scripts ({selected_script}) + templates")
13001310

13011311
ensure_constitution_from_template(project_path, tracker=tracker)
@@ -2074,7 +2084,7 @@ def integration_install(
20742084

20752085
# Ensure shared infrastructure is present (safe to run unconditionally;
20762086
# _install_shared_infra merges missing files without overwriting).
2077-
_install_shared_infra(project_root, selected_script)
2087+
_install_shared_infra(project_root, selected_script, invoke_separator=integration.invoke_separator)
20782088
if os.name != "nt":
20792089
ensure_executable_scripts(project_root)
20802090

@@ -2358,7 +2368,7 @@ def integration_switch(
23582368

23592369
# Ensure shared infrastructure is present (safe to run unconditionally;
23602370
# _install_shared_infra merges missing files without overwriting).
2361-
_install_shared_infra(project_root, selected_script)
2371+
_install_shared_infra(project_root, selected_script, invoke_separator=target_integration.invoke_separator)
23622372
if os.name != "nt":
23632373
ensure_executable_scripts(project_root)
23642374

@@ -2466,7 +2476,7 @@ def integration_upgrade(
24662476
selected_script = _resolve_script_type(project_root, script)
24672477

24682478
# Ensure shared infrastructure is up to date; --force overwrites existing files.
2469-
_install_shared_infra(project_root, selected_script, force=force)
2479+
_install_shared_infra(project_root, selected_script, force=force, invoke_separator=integration.invoke_separator)
24702480
if os.name != "nt":
24712481
ensure_executable_scripts(project_root)
24722482

src/specify_cli/integrations/base.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class IntegrationBase(ABC):
8484
context_file: str | None = None
8585
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
8686

87+
invoke_separator: str = "."
88+
"""Separator used in slash-command invocations (``"."`` → ``/speckit.plan``)."""
89+
8790
# -- Markers for managed context section ------------------------------
8891

8992
CONTEXT_MARKER_START = "<!-- SPECKIT START -->"
@@ -597,13 +600,32 @@ def remove_context_section(self, project_root: Path) -> bool:
597600

598601
return True
599602

603+
@staticmethod
604+
def resolve_command_refs(content: str, separator: str = ".") -> str:
605+
"""Replace ``__SPECKIT_COMMAND_<NAME>__`` placeholders with invocations.
606+
607+
Each placeholder encodes a command name in upper-case with
608+
underscores (e.g. ``__SPECKIT_COMMAND_PLAN__``,
609+
``__SPECKIT_COMMAND_GIT_COMMIT__``). The replacement uses
610+
*separator* to join the segments:
611+
612+
* ``separator="."`` → ``/speckit.plan``, ``/speckit.git.commit``
613+
* ``separator="-"`` → ``/speckit-plan``, ``/speckit-git-commit``
614+
"""
615+
return re.sub(
616+
r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__",
617+
lambda m: "/speckit" + separator + m.group(1).lower().replace("_", separator),
618+
content,
619+
)
620+
600621
@staticmethod
601622
def process_template(
602623
content: str,
603624
agent_name: str,
604625
script_type: str,
605626
arg_placeholder: str = "$ARGUMENTS",
606627
context_file: str = "",
628+
invoke_separator: str = ".",
607629
) -> str:
608630
"""Process a raw command template into agent-ready content.
609631
@@ -615,6 +637,7 @@ def process_template(
615637
5. Replace ``__AGENT__`` with *agent_name*
616638
6. Replace ``__CONTEXT_FILE__`` with *context_file*
617639
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
640+
8. Replace ``__SPECKIT_COMMAND_<NAME>__`` with invocation strings
618641
"""
619642
# 1. Extract script command from frontmatter
620643
script_command = ""
@@ -684,6 +707,9 @@ def process_template(
684707

685708
content = CommandRegistrar.rewrite_project_relative_paths(content)
686709

710+
# 8. Replace __SPECKIT_COMMAND_<NAME>__ with invocation strings
711+
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
712+
687713
return content
688714

689715
def setup(
@@ -1274,6 +1300,8 @@ class SkillsIntegration(IntegrationBase):
12741300
``speckit-<name>/SKILL.md`` file with skills-oriented frontmatter.
12751301
"""
12761302

1303+
invoke_separator = "-"
1304+
12771305
def build_exec_args(
12781306
self,
12791307
prompt: str,
@@ -1395,6 +1423,7 @@ def setup(
13951423
processed_body = self.process_template(
13961424
raw, self.key, script_type, arg_placeholder,
13971425
context_file=self.context_file or "",
1426+
invoke_separator=self.invoke_separator,
13981427
)
13991428
# Strip the processed frontmatter — we rebuild it for skills.
14001429
# Preserve leading whitespace in the body to match release ZIP

templates/checklist-template.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
**Created**: [DATE]
55
**Feature**: [Link to spec.md or relevant documentation]
66

7-
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
7+
**Note**: This checklist is generated by the `__SPECKIT_COMMAND_CHECKLIST__` command based on feature context and requirements.
88

99
<!--
1010
============================================================================
1111
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
1212
13-
The /speckit.checklist command MUST replace these with actual items based on:
13+
The __SPECKIT_COMMAND_CHECKLIST__ command MUST replace these with actual items based on:
1414
- User's specific checklist request
1515
- Feature requirements from spec.md
1616
- Technical context from plan.md

templates/commands/analyze.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ You **MUST** consider the user input before proceeding (if not empty).
4949
5050
## Goal
5151
52-
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
52+
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `__SPECKIT_COMMAND_TASKS__` has successfully produced a complete `tasks.md`.
5353
5454
## Operating Constraints
5555
5656
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
5757
58-
**Constitution Authority**: The project constitution (`/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
58+
**Constitution Authority**: The project constitution (`/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `__SPECKIT_COMMAND_ANALYZE__`.
5959
6060
## Execution Steps
6161
@@ -191,9 +191,9 @@ Output a Markdown report (no file writes) with the following structure:
191191
192192
At end of report, output a concise Next Actions block:
193193
194-
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
194+
- If CRITICAL issues exist: Recommend resolving before `__SPECKIT_COMMAND_IMPLEMENT__`
195195
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
196-
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
196+
- Provide explicit command suggestions: e.g., "Run __SPECKIT_COMMAND_SPECIFY__ with refinement", "Run __SPECKIT_COMMAND_PLAN__ to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
197197
198198
### 8. Offer Remediation
199199

templates/commands/checklist.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ You **MUST** consider the user input before proceeding (if not empty).
249249
- Actor/timing
250250
- Any explicit user-specified must-have items incorporated
251251
252-
**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
252+
**Important**: Each `__SPECKIT_COMMAND_CHECKLIST__` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
253253
254254
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
255255
- Simple, memorable filenames that indicate checklist purpose

templates/commands/clarify.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@ You **MUST** consider the user input before proceeding (if not empty).
5555
5656
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
5757
58-
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
58+
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `__SPECKIT_COMMAND_PLAN__`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
5959
6060
Execution steps:
6161
6262
1. Run `{SCRIPT}` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
6363
- `FEATURE_DIR`
6464
- `FEATURE_SPEC`
6565
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
66-
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
66+
- If JSON parsing fails, abort and instruct user to re-run `__SPECKIT_COMMAND_SPECIFY__` or verify feature branch environment.
6767
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
6868
6969
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
@@ -202,13 +202,13 @@ Execution steps:
202202
- Path to updated spec.
203203
- Sections touched (list names).
204204
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
205-
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
205+
- If any Outstanding or Deferred remain, recommend whether to proceed to `__SPECKIT_COMMAND_PLAN__` or run `__SPECKIT_COMMAND_CLARIFY__` again later post-plan.
206206
- Suggested next command.
207207
208208
Behavior rules:
209209
210210
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
211-
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
211+
- If spec file missing, instruct user to run `__SPECKIT_COMMAND_SPECIFY__` first (do not create a new spec here).
212212
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
213213
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
214214
- Respect user early termination signals ("stop", "done", "proceed").

templates/commands/implement.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ You **MUST** consider the user input before proceeding (if not empty).
169169
- Confirm the implementation follows the technical plan
170170
- Report final status with summary of completed work
171171
172-
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
172+
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `__SPECKIT_COMMAND_TASKS__` first to regenerate the task list.
173173
174174
10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root.
175175
- If it exists, read it and look for entries under the `hooks.after_implement` key

templates/commands/specify.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ You **MUST** consider the user input before proceeding (if not empty).
5454
5555
## Outline
5656
57-
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
57+
The text the user typed after `__SPECKIT_COMMAND_SPECIFY__` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
5858
5959
Given that feature description, do this:
6060
@@ -100,10 +100,10 @@ Given that feature description, do this:
100100
}
101101
```
102102
Write the actual resolved directory path value (for example, `specs/003-user-auth`), not the literal string `SPECIFY_FEATURE_DIRECTORY`.
103-
This allows downstream commands (`/speckit.plan`, `/speckit.tasks`, etc.) to locate the feature directory without relying on git branch name conventions.
103+
This allows downstream commands (`__SPECKIT_COMMAND_PLAN__`, `__SPECKIT_COMMAND_TASKS__`, etc.) to locate the feature directory without relying on git branch name conventions.
104104
105105
**IMPORTANT**:
106-
- You must only create one feature per `/speckit.specify` invocation
106+
- You must only create one feature per `__SPECKIT_COMMAND_SPECIFY__` invocation
107107
- The spec directory name and the git branch name are independent — they may be the same but that is the user's choice
108108
- The spec directory and file are always created by this command, never by the hook
109109
@@ -174,7 +174,7 @@ Given that feature description, do this:
174174
175175
## Notes
176176
177-
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
177+
- Items marked incomplete require spec updates before `__SPECKIT_COMMAND_CLARIFY__` or `__SPECKIT_COMMAND_PLAN__`
178178
```
179179
180180
b. **Run Validation Check**: Review the spec against each checklist item:
@@ -232,7 +232,7 @@ Given that feature description, do this:
232232
- `SPECIFY_FEATURE_DIRECTORY` — the feature directory path
233233
- `SPEC_FILE` — the spec file path
234234
- Checklist results summary
235-
- Readiness for the next phase (`/speckit.clarify` or `/speckit.plan`)
235+
- Readiness for the next phase (`__SPECKIT_COMMAND_CLARIFY__` or `__SPECKIT_COMMAND_PLAN__`)
236236
237237
9. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root.
238238
- If it exists, read it and look for entries under the `hooks.after_specify` key

templates/plan-template.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
44
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
55

6-
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
6+
**Note**: This template is filled in by the `__SPECKIT_COMMAND_PLAN__` command. See `.specify/templates/plan-template.md` for the execution workflow.
77

88
## Summary
99

@@ -39,12 +39,12 @@
3939

4040
```text
4141
specs/[###-feature]/
42-
├── plan.md # This file (/speckit.plan command output)
43-
├── research.md # Phase 0 output (/speckit.plan command)
44-
├── data-model.md # Phase 1 output (/speckit.plan command)
45-
├── quickstart.md # Phase 1 output (/speckit.plan command)
46-
├── contracts/ # Phase 1 output (/speckit.plan command)
47-
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
42+
├── plan.md # This file (__SPECKIT_COMMAND_PLAN__ command output)
43+
├── research.md # Phase 0 output (__SPECKIT_COMMAND_PLAN__ command)
44+
├── data-model.md # Phase 1 output (__SPECKIT_COMMAND_PLAN__ command)
45+
├── quickstart.md # Phase 1 output (__SPECKIT_COMMAND_PLAN__ command)
46+
├── contracts/ # Phase 1 output (__SPECKIT_COMMAND_PLAN__ command)
47+
└── tasks.md # Phase 2 output (__SPECKIT_COMMAND_TASKS__ command - NOT created by __SPECKIT_COMMAND_PLAN__)
4848
```
4949

5050
### Source Code (repository root)

templates/tasks-template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ description: "Task list template for feature implementation"
2929
============================================================================
3030
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
3131
32-
The /speckit.tasks command MUST replace these with actual tasks based on:
32+
The __SPECKIT_COMMAND_TASKS__ command MUST replace these with actual tasks based on:
3333
- User stories from spec.md (with their priorities P1, P2, P3...)
3434
- Feature requirements from plan.md
3535
- Entities from data-model.md

0 commit comments

Comments
 (0)