Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def cmd_ensure_stop_hook(args: list[str]) -> int:


def cmd_stop_hook(_: list[str]) -> int:
sys.stdin.read()
raw = sys.stdin.read()
if os.environ.get("STORY_AUTOMATOR_CHILD", "").lower() == "true":
return 0
marker = active_marker_path()
Expand All @@ -151,6 +151,16 @@ def cmd_stop_hook(_: list[str]) -> int:
payload = json.loads(marker.read_text())
except json.JSONDecodeError:
return 0
owner = (payload.get("ownerSession") or "").strip()
if owner:
sid = ""
try:
if raw.strip():
sid = (json.loads(raw) or {}).get("session_id", "") or ""
except json.JSONDecodeError:
sid = ""
if sid and sid != owner:
return 0
remaining = payload.get("storiesRemaining", 0)
if isinstance(remaining, str) and remaining.isdigit():
remaining = int(remaining)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def _marker(args: list[str]) -> int:
"heartbeat": options["heartbeat"] or iso_now(),
"pid": int(options["pid"] or "0"),
"projectSlug": options["project-slug"],
"ownerSession": os.environ.get("CLAUDE_CODE_SESSION_ID", ""),
}
atomic_write(marker_file, json.dumps(payload, indent=2) + "\n")
print(f"Marker created: {marker_file}")
Expand Down
51 changes: 51 additions & 0 deletions tests/test_stop_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from unittest.mock import patch

from story_automator.commands.basic import cmd_ensure_stop_hook, cmd_stop_hook
from story_automator.commands.orchestrator import cmd_orchestrator_helper


REPO_ROOT = Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -416,6 +417,56 @@ def test_stop_hook_uses_project_root_env_when_invoked_from_nested_directory(self
payload = json.loads(stdout.getvalue())
self.assertEqual(payload["decision"], "block")

def test_stop_hook_blocks_the_owning_session(self) -> None:
self._install_bundle(".agents")
marker = self.project_root / ".agents" / ".story-automator-active"
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text(json.dumps({"storiesRemaining": 2, "ownerSession": "owner-123"}), encoding="utf-8")
stdout = io.StringIO()
with (
patch.dict(os.environ, {"PROJECT_ROOT": str(self.project_root)}, clear=False),
patch("story_automator.commands.basic.sys.stdin", io.StringIO(json.dumps({"session_id": "owner-123"}))),
patch("os.getcwd", return_value=str(self.project_root)),
redirect_stdout(stdout),
):
code = cmd_stop_hook([])
self.assertEqual(code, 0)
self.assertEqual(json.loads(stdout.getvalue())["decision"], "block")

def test_stop_hook_allows_a_different_session_while_run_active(self) -> None:
self._install_bundle(".agents")
marker = self.project_root / ".agents" / ".story-automator-active"
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text(json.dumps({"storiesRemaining": 2, "ownerSession": "owner-123"}), encoding="utf-8")
stdout = io.StringIO()
with (
patch.dict(os.environ, {"PROJECT_ROOT": str(self.project_root)}, clear=False),
patch("story_automator.commands.basic.sys.stdin", io.StringIO(json.dumps({"session_id": "other-session"}))),
patch("os.getcwd", return_value=str(self.project_root)),
redirect_stdout(stdout),
):
code = cmd_stop_hook([])
self.assertEqual(code, 0)
self.assertEqual(stdout.getvalue(), "")

def test_marker_create_records_owner_session_from_env(self) -> None:
self._install_bundle(".agents")
stdout = io.StringIO()
with (
patch.dict(
os.environ,
{"PROJECT_ROOT": str(self.project_root), "CLAUDE_CODE_SESSION_ID": "owner-xyz"},
clear=False,
),
patch("os.getcwd", return_value=str(self.project_root)),
redirect_stdout(stdout),
):
code = cmd_orchestrator_helper(["marker", "create", "--epic", "e1", "--story", "e1-1", "--remaining", "2"])
self.assertEqual(code, 0)
marker = self.project_root / ".agents" / ".story-automator-active"
payload = json.loads(marker.read_text(encoding="utf-8"))
self.assertEqual(payload["ownerSession"], "owner-xyz")

def test_ensure_stop_hook_codex_updates_dotted_features_toml(self) -> None:
self._install_bundle(".agents")
codex_dir = self.project_root / ".codex"
Expand Down