Skip to content

Commit 63ec272

Browse files
fix: only use materialized command output for the active stack row
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com>
1 parent c45c4bf commit 63ec272

2 files changed

Lines changed: 93 additions & 5 deletions

File tree

‎src/specify_cli/artifacts/__init__.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ def _derive_source_path(
429429
project_root: Path,
430430
kind: ArtifactKind,
431431
name: str,
432+
*,
433+
active: bool,
432434
) -> str | None:
433435
"""Return the repo-relative concrete file backing a preset/extension layer.
434436
@@ -437,6 +439,11 @@ def _derive_source_path(
437439
(``preset_id``/``pack_dir`` or ``extension_id``/``extension_dir``)
438440
alongside ``lookupId``; core and project rows intentionally do not produce
439441
a source path here.
442+
443+
The tracked materialized agent output is shared by every stack row that
444+
contributed the same command name, so it only reflects the winning
445+
(``active``) row's content. Lower ``replace``/``merge`` rows must report
446+
their own installed pack file instead of that shared output.
440447
"""
441448
lookup_id = layer.get("lookupId", "")
442449
layer_kind = layer_kind_from_lookup_id(lookup_id)
@@ -447,7 +454,7 @@ def _derive_source_path(
447454
from ..presets import PresetRegistry
448455

449456
metadata = PresetRegistry(project_root / ".specify" / "presets").get(pack_id)
450-
if kind == "command":
457+
if kind == "command" and active:
451458
materialized = _materialized_command_source_path(
452459
project_root, metadata, name, source="preset"
453460
)
@@ -460,7 +467,7 @@ def _derive_source_path(
460467
from ..extensions import ExtensionRegistry
461468

462469
metadata = ExtensionRegistry(project_root / ".specify" / "extensions").get(extension_id)
463-
if kind == "command":
470+
if kind == "command" and active:
464471
materialized = _materialized_command_source_path(
465472
project_root, metadata, name, source="extension"
466473
)
@@ -471,8 +478,9 @@ def _derive_source_path(
471478
# artifact contract's perspective, so their sourcePath stays null.
472479
return None
473480

474-
# Non-command preset/extension layers, and command layers without a tracked
475-
# materialized agent output, report the installed pack file from the raw
481+
# Non-active command layers, non-command preset/extension layers, and
482+
# active command layers without a tracked materialized agent output all
483+
# report the installed pack file from the raw
476484
# PresetResolver.collect_all_layers() row's concrete ``path`` key.
477485
path = layer.get("path")
478486
if isinstance(path, Path):
@@ -547,7 +555,7 @@ def _build_stack(
547555
hidden = idx > first_replace_idx
548556

549557
layer_kind, source_id, lookup_id = _public_layer_shape(layer)
550-
source_path = _derive_source_path(layer, project_root, kind, name)
558+
source_path = _derive_source_path(layer, project_root, kind, name, active=active)
551559

552560
if layer_kind == PROJECT_OVERRIDE_LAYER:
553561
rows.append(

‎tests/test_artifact_command.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,86 @@ def test_list_json_rows_include_stack(self, spec_kit_project: Path, monkeypatch:
743743
info = json.loads(info_result.stdout)
744744
assert row["stack"] == info["stack"]
745745

746+
def test_hidden_command_layer_source_path_is_own_pack_file(
747+
self, spec_kit_project: Path
748+
):
749+
"""A hidden (non-active) command row must not report the winner's
750+
shared materialized agent output as its ``sourcePath``.
751+
752+
Both presets below register the same command name and the same
753+
agent skill name, so the tracked materialized output is a single
754+
shared file. Only the active (winning) row may report that shared
755+
file; the hidden loser row must report its own installed pack file.
756+
"""
757+
pack_low = install_preset(
758+
spec_kit_project,
759+
"aaa-low-priority-preset",
760+
{
761+
"commands": [
762+
{
763+
"name": "speckit.compliance.plan",
764+
"file": "commands/speckit.compliance.plan.md",
765+
"description": "Loser",
766+
}
767+
]
768+
},
769+
priority=20,
770+
)
771+
(pack_low / "commands").mkdir()
772+
(pack_low / "commands" / "speckit.compliance.plan.md").write_text(
773+
"---\ndescription: Loser\n---\nloser body\n", encoding="utf-8"
774+
)
775+
PresetRegistry(spec_kit_project / ".specify" / "presets").update(
776+
"aaa-low-priority-preset",
777+
{"registered_skills": {"copilot": ["speckit-compliance-plan"]}},
778+
)
779+
780+
pack_high = install_preset(
781+
spec_kit_project,
782+
"zzz-high-priority-preset",
783+
{
784+
"commands": [
785+
{
786+
"name": "speckit.compliance.plan",
787+
"file": "commands/speckit.compliance.plan.md",
788+
"description": "Winner",
789+
}
790+
]
791+
},
792+
priority=5,
793+
)
794+
(pack_high / "commands").mkdir()
795+
(pack_high / "commands" / "speckit.compliance.plan.md").write_text(
796+
"---\ndescription: Winner\n---\nwinner body\n", encoding="utf-8"
797+
)
798+
PresetRegistry(spec_kit_project / ".specify" / "presets").update(
799+
"zzz-high-priority-preset",
800+
{"registered_skills": {"copilot": ["speckit-compliance-plan"]}},
801+
)
802+
803+
skill_file = (
804+
spec_kit_project
805+
/ ".github"
806+
/ "skills"
807+
/ "speckit-compliance-plan"
808+
/ "SKILL.md"
809+
)
810+
skill_file.parent.mkdir(parents=True)
811+
skill_file.write_text("---\nname: speckit-compliance-plan\n---\n", encoding="utf-8")
812+
813+
info = ArtifactCatalog(spec_kit_project).get_artifact_info("speckit.compliance.plan")
814+
stack = info["stack"]
815+
assert stack[0]["active"] is True
816+
assert stack[0]["sourcePath"] == ".github/skills/speckit-compliance-plan/SKILL.md"
817+
818+
hidden_rows = [layer for layer in stack if layer["active"] is False]
819+
assert hidden_rows
820+
for row in hidden_rows:
821+
assert row["sourcePath"] != stack[0]["sourcePath"]
822+
assert row["sourcePath"] == (
823+
".specify/presets/aaa-low-priority-preset/commands/speckit.compliance.plan.md"
824+
)
825+
746826
def test_list_json_stack_source_path_contract(
747827
self, spec_kit_project: Path, monkeypatch: pytest.MonkeyPatch
748828
):

0 commit comments

Comments
 (0)