@@ -85,32 +85,63 @@ def test_legacy_commands_disables_skills(self):
8585 assert bob .is_skills_mode ({"legacy_commands" : True }) is False
8686
8787 def test_existing_commands_layout_preserved_on_use (self , tmp_path ):
88- """Regression (review #3415): an existing Bob 1.x project (only
89- ``.bob/commands/`` on disk, no stored ``legacy_commands``) must NOT be
90- treated as skills mode when re-resolved with a project_root, so
91- ``use``/``switch``/``upgrade`` never silently migrate it to skills.
88+ """Regression (review #3415): an existing Bob 1.x project (managed
89+ ``.bob/commands/speckit.*.md`` on disk, no stored ``legacy_commands``)
90+ must NOT be treated as skills mode when re-resolved with a
91+ project_root, so ``use``/``switch``/``upgrade`` never silently migrate
92+ it to skills.
9293 """
9394 bob = get_integration ("bob" )
94- (tmp_path / ".bob" / "commands" ).mkdir (parents = True )
95+ cmds = tmp_path / ".bob" / "commands"
96+ cmds .mkdir (parents = True )
97+ (cmds / "speckit.plan.md" ).write_text ("# plan" , encoding = "utf-8" )
9598 # No parsed options at all — the pre-existing-install scenario.
9699 assert bob .is_skills_mode (None , project_root = tmp_path ) is False
97100 assert bob .is_skills_mode ({}, project_root = tmp_path ) is False
98101
99102 def test_existing_skills_layout_stays_skills_on_use (self , tmp_path ):
100- """A ``.bob/skills/ `` project resolves to skills mode."""
103+ """A project with managed ``speckit-* `` skills resolves to skills mode."""
101104 bob = get_integration ("bob" )
102- (tmp_path / ".bob" / "skills" ).mkdir (parents = True )
105+ (tmp_path / ".bob" / "skills" / "speckit-plan" ).mkdir (parents = True )
106+ assert bob .is_skills_mode (None , project_root = tmp_path ) is True
107+
108+ def test_managed_commands_with_unrelated_skills_dir_stays_legacy (
109+ self , tmp_path
110+ ):
111+ """Regression (review #3415, 4723246468): a legacy Spec Kit install
112+ (managed ``.bob/commands/speckit.*.md``) that *also* carries unrelated
113+ Bob 2 skills (a ``.bob/skills/`` dir with no managed ``speckit-*``
114+ skills) must stay in command mode — the mere presence of a skills
115+ directory is not evidence that Spec Kit is skills-based.
116+ """
117+ bob = get_integration ("bob" )
118+ cmds = tmp_path / ".bob" / "commands"
119+ cmds .mkdir (parents = True )
120+ (cmds / "speckit.plan.md" ).write_text ("# plan" , encoding = "utf-8" )
121+ # An unrelated (non-Spec-Kit) skill the user authored.
122+ (tmp_path / ".bob" / "skills" / "my-own-skill" ).mkdir (parents = True )
123+ assert bob .is_skills_mode (None , project_root = tmp_path ) is False
124+ assert bob .effective_invoke_separator (None , project_root = tmp_path ) == "."
125+
126+ def test_managed_skills_win_when_both_layouts_present (self , tmp_path ):
127+ """When managed Spec Kit skills exist, skills mode wins even if a stale
128+ managed command file is still on disk (upgrade leftover)."""
129+ bob = get_integration ("bob" )
130+ cmds = tmp_path / ".bob" / "commands"
131+ cmds .mkdir (parents = True )
132+ (cmds / "speckit.plan.md" ).write_text ("# plan" , encoding = "utf-8" )
133+ (tmp_path / ".bob" / "skills" / "speckit-plan" ).mkdir (parents = True )
103134 assert bob .is_skills_mode (None , project_root = tmp_path ) is True
104135
105136 def test_fresh_project_defaults_to_skills_with_project_root (self , tmp_path ):
106- """A project with no ``.bob/`` layout yet still defaults to skills."""
137+ """A project with no managed ``.bob/`` artifacts yet defaults to skills."""
107138 bob = get_integration ("bob" )
108139 assert bob .is_skills_mode (None , project_root = tmp_path ) is True
109140
110141 def test_explicit_legacy_flag_wins_over_disk_layout (self , tmp_path ):
111142 """An explicit ``--legacy-commands`` overrides on-disk detection."""
112143 bob = get_integration ("bob" )
113- (tmp_path / ".bob" / "skills" ).mkdir (parents = True )
144+ (tmp_path / ".bob" / "skills" / "speckit-plan" ).mkdir (parents = True )
114145 assert (
115146 bob .is_skills_mode ({"legacy_commands" : True }, project_root = tmp_path )
116147 is False
@@ -519,7 +550,9 @@ def test_update_init_options_preserves_legacy_commands_project(self, tmp_path):
519550 from specify_cli import load_init_options
520551
521552 # Existing Bob 1.x project: legacy commands dir on disk, no ai_skills.
522- (tmp_path / ".bob" / "commands" ).mkdir (parents = True )
553+ cmds = tmp_path / ".bob" / "commands"
554+ cmds .mkdir (parents = True )
555+ (cmds / "speckit.plan.md" ).write_text ("# plan" , encoding = "utf-8" )
523556 bob = get_integration ("bob" )
524557
525558 # Simulate the use/switch path: no parsed options were stored.
@@ -538,7 +571,7 @@ def test_update_init_options_keeps_skills_project_as_skills(self, tmp_path):
538571 )
539572 from specify_cli import load_init_options
540573
541- (tmp_path / ".bob" / "skills" ).mkdir (parents = True )
574+ (tmp_path / ".bob" / "skills" / "speckit-plan" ).mkdir (parents = True )
542575 bob = get_integration ("bob" )
543576
544577 _update_init_options_for_integration (tmp_path , bob , parsed_options = None )
@@ -556,7 +589,9 @@ def test_with_integration_setting_stores_dot_separator_for_legacy(self, tmp_path
556589 """
557590 from specify_cli .integration_runtime import with_integration_setting
558591
559- (tmp_path / ".bob" / "commands" ).mkdir (parents = True )
592+ cmds = tmp_path / ".bob" / "commands"
593+ cmds .mkdir (parents = True )
594+ (cmds / "speckit.plan.md" ).write_text ("# plan" , encoding = "utf-8" )
560595 bob = get_integration ("bob" )
561596
562597 # Simulate the use/switch path: no parsed options stored.
@@ -694,3 +729,36 @@ def test_active_bob_skills_still_uses_hyphen(self, tmp_path):
694729 content = written [0 ].read_text (encoding = "utf-8" )
695730 assert "/speckit-plan" in content
696731 assert "/speckit.plan" not in content
732+
733+ def test_inactive_bob_command_output_uses_dot_even_with_skills_dir (
734+ self , tmp_path
735+ ):
736+ """Regression (review #3415, 4723246468): for an inactive Bob install
737+ the registrar's separator must match the layout it is actually writing
738+ (``.bob/commands/*.md`` — command layout), not on-disk sibling dirs.
739+ Even when a ``.bob/skills/`` directory (with managed ``speckit-*``
740+ skills) coexists, command-layout files must keep ``/speckit.<cmd>``.
741+ """
742+ from specify_cli ._init_options import save_init_options
743+ from specify_cli .agents import CommandRegistrar
744+
745+ # Both layouts on disk; the active agent is something else entirely.
746+ (tmp_path / ".bob" / "commands" ).mkdir (parents = True )
747+ (tmp_path / ".bob" / "skills" / "speckit-plan" ).mkdir (parents = True )
748+ save_init_options (tmp_path , {"ai" : "claude" , "ai_skills" : True })
749+
750+ source_dir = tmp_path / "ext-src"
751+ commands = self ._write_command_ref_ext (source_dir )
752+
753+ registrar = CommandRegistrar ()
754+ registrar .register_commands ("bob" , commands , "ext" , source_dir , tmp_path )
755+
756+ written = list ((tmp_path / ".bob" / "commands" ).glob ("*.md" ))
757+ assert written , "expected a rendered Bob command file"
758+ content = written [0 ].read_text (encoding = "utf-8" )
759+ assert "__SPECKIT_COMMAND_PLAN__" not in content
760+ assert "/speckit.plan" in content , (
761+ "Bob command-layout output must use the dot separator regardless "
762+ "of a coexisting .bob/skills directory"
763+ )
764+ assert "/speckit-plan" not in content
0 commit comments