@@ -1388,8 +1388,10 @@ def test_dispatcher_is_self_contained(self, tmp_path):
13881388 {"pre_tool_use" : [{"command" : "speckit.tdd.validate" }]},
13891389 )
13901390 content = (tmp_path / EVENTS_DISPATCHER_REL ).read_text ()
1391- # Delegates to specify_cli when importable.
1392- assert "from specify_cli.events import resolve_and_run_event_command" in content
1391+ # Delegates to specify_cli when importable and confinement is present.
1392+ assert "EVENT_SCRIPT_PATH_CONFINEMENT" in content
1393+ assert "from specify_cli.events import" in content
1394+ assert "resolve_and_run_event_command" in content
13931395 assert "except (ImportError, TypeError):" in content
13941396 # Inline stdlib fallback resolver for one-time/temporary installs.
13951397 assert "_run_inline" in content
@@ -1454,6 +1456,53 @@ def test_dispatcher_inline_fallback_runs_script(self, tmp_path):
14541456 assert out_file .exists (), f"inline fallback did not run script; stderr={ result .stderr !r} rc={ result .returncode } "
14551457 assert out_file .read_text () == '{"tool_name":"x"}'
14561458
1459+ def test_dispatcher_ignores_stale_specify_cli_without_confinement (self , tmp_path ):
1460+ """A generated dispatcher must not delegate to an older specify_cli
1461+ that lacks EVENT_SCRIPT_PATH_CONFINEMENT (uvx-init plus stale
1462+ global install). Absolute script tokens stay rejected."""
1463+ import subprocess as _sp
1464+ import sys as _sys
1465+
1466+ integration = ClaudeIntegration ()
1467+ manifest = MagicMock (spec = IntegrationManifest )
1468+ manifest .files = {}
1469+ manifest .record_file = MagicMock ()
1470+ manifest .record_existing = MagicMock ()
1471+ install_integration_events (
1472+ integration , tmp_path , manifest ,
1473+ {"session_start" : [{"command" : "speckit.boot" }]},
1474+ )
1475+ dispatcher = tmp_path / EVENTS_DISPATCHER_REL
1476+ cmd_dir = tmp_path / ".specify" / "templates" / "commands"
1477+ cmd_dir .mkdir (parents = True )
1478+ ran = tmp_path / "stale-ran"
1479+ (cmd_dir / "boot.md" ).write_text (
1480+ "---\n description: \" Boot\" \n scripts:\n sh: /tmp/outside.sh\n ---\n Body\n " ,
1481+ encoding = "utf-8" ,
1482+ )
1483+
1484+ fake_dir = tmp_path / "_stale_pkg"
1485+ pkg = fake_dir / "specify_cli"
1486+ pkg .mkdir (parents = True )
1487+ (pkg / "__init__.py" ).write_text ("" , encoding = "utf-8" )
1488+ (pkg / "events.py" ).write_text (
1489+ "def resolve_and_run_event_command(*_a, **_k):\n "
1490+ f" open({ str (ran )!r} , 'w').write('delegated')\n "
1491+ " return 0\n " ,
1492+ encoding = "utf-8" ,
1493+ )
1494+ env = dict (os .environ )
1495+ env ["PYTHONPATH" ] = str (fake_dir )
1496+ result = _sp .run (
1497+ [_sys .executable , str (dispatcher ), "speckit.boot" , "session_start" , "60" ],
1498+ input = "{}" ,
1499+ capture_output = True ,
1500+ text = True ,
1501+ env = env ,
1502+ cwd = str (tmp_path ),
1503+ )
1504+ assert not ran .exists (), f"stale package ran; stderr={ result .stderr !r} "
1505+
14571506 def test_dispatcher_threads_per_handler_timeout (self , tmp_path ):
14581507 """S4: the generated dispatcher reads an optional 4th timeout arg and
14591508 uses it for the inner subprocess, instead of a fixed 120s cap that
0 commit comments