claude-dev-env 8.72.0 → 8.72.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/install.mjs +1 -0
  2. package/bin/install.test.mjs +1 -0
  3. package/docs/rule-guides/asd-ste100-language.md +10 -0
  4. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +9 -1
  5. package/hooks/blocking/headless_claude_broker_gate.py +14 -1
  6. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +2 -0
  7. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +46 -0
  8. package/hooks/blocking/test_headless_claude_broker_gate.py +12 -0
  9. package/hooks/conftest.py +18 -0
  10. package/hooks/features/session-context.md +1 -3
  11. package/hooks/hooks.json +0 -5
  12. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  13. package/hooks/hooks_constants/headless_claude_broker_gate_constants.py +7 -1
  14. package/hooks/hooks_constants/mod_handoff.py +91 -0
  15. package/hooks/hooks_constants/mod_handoff_constants.py +38 -0
  16. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -1
  17. package/hooks/hooks_constants/test_mod_handoff.py +100 -0
  18. package/hooks/session/advisor_rules_prompt.py +5 -1
  19. package/hooks/session/orchestrator_auto_starter.py +5 -1
  20. package/hooks/session/task_tool_prompt.py +5 -1
  21. package/hooks/session/test_advisor_rules_prompt.py +11 -0
  22. package/hooks/session/test_orchestrator_auto_starter.py +22 -0
  23. package/hooks/session/test_task_tool_prompt.py +15 -0
  24. package/hooks/session/test_working_style_prompt.py +15 -0
  25. package/hooks/session/working_style_prompt.py +5 -1
  26. package/package.json +1 -1
  27. package/rules/asd-ste100-language.md +1 -1
  28. package/scripts/dev_env_scripts_constants/review_closure_constants.py +24 -0
  29. package/scripts/review_closure_model.py +48 -12
  30. package/scripts/test_files/review_closure/codex_comments.json +1266 -0
  31. package/scripts/tests/test_review_closure_model.py +130 -0
  32. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +0 -22
  33. package/hooks/session/issue_tracker_session_starter.py +0 -136
  34. package/hooks/session/test_issue_tracker_session_starter.py +0 -106
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
4
+ import json
3
5
  import sys
4
6
  from datetime import datetime
5
7
  from pathlib import Path
@@ -10,6 +12,7 @@ SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
10
12
  if str(SCRIPTS_DIRECTORY) not in sys.path:
11
13
  sys.path.insert(0, str(SCRIPTS_DIRECTORY))
12
14
 
15
+ import review_closure as command
13
16
  import review_closure_model as model
14
17
  from dev_env_scripts_constants.review_closure_constants import (
15
18
  APPROVALS_CHECK_NAME,
@@ -567,3 +570,130 @@ def should_read_a_person_carrying_a_repository_marker_as_a_comment() -> None:
567
570
  )
568
571
 
569
572
  assert not comment.is_notice
573
+
574
+
575
+ CASES = json.loads(
576
+ (Path(__file__).resolve().parents[1] / "test_files/review_closure/codex_comments.json")
577
+ .read_text(encoding="utf-8")
578
+ )
579
+ DRIVERS = frozenset({CASES[0]["driver_login"]})
580
+ SLUG = "jl-cmd-projects/claude-dev-env"
581
+
582
+
583
+ def _comments(case: dict) -> tuple[model.TopLevelComment, ...]:
584
+ return tuple(model.parse_top_level_comment(record) for record in case["comments"])
585
+
586
+
587
+ @pytest.mark.parametrize("case", CASES, ids=lambda case: str(case["number"]))
588
+ def should_replay_clean_queue_comments_through_the_command(
589
+ case: dict, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
590
+ ) -> None:
591
+ pr = {"number": case["number"], "head": {"sha": case["head"]},
592
+ "user": {"login": next(iter(DRIVERS))}}
593
+ monkeypatch.setattr(command, "github_token", lambda: "fixture-token")
594
+ monkeypatch.setattr(command, "read_pull_request", lambda *_: pr)
595
+ monkeypatch.setattr(command, "read_review_threads", lambda *_: ())
596
+ monkeypatch.setattr(command, "read_top_level_comments", lambda *_: _comments(case))
597
+ monkeypatch.setattr(command, "read_approvals_conclusion", lambda *_: None)
598
+
599
+ assert command.main([SLUG, str(case["number"])]) == 0
600
+ assert capsys.readouterr().out.startswith(f"CLOSED {SLUG}#{case['number']}")
601
+
602
+
603
+ @pytest.mark.parametrize("body", ["@codex review", "@codex security review"])
604
+ def should_treat_a_command_only_request_as_a_notice(body: str) -> None:
605
+ record = copy.deepcopy(CASES[0]["comments"][0])
606
+ record["body"] = body
607
+
608
+ assert model.parse_top_level_comment(record).is_notice
609
+
610
+
611
+ def should_not_credit_a_driver_review_request_as_a_bug_reply() -> None:
612
+ request = copy.deepcopy(CASES[0]["comments"][0])
613
+ request["user"]["login"] = next(iter(DRIVERS))
614
+ bug = copy.deepcopy(request)
615
+ bug["user"]["login"] = "maintainer"
616
+ bug["body"] = "The rollback loses the saved profile."
617
+ bug["created_at"] = bug["updated_at"] = "2026-10-06T11:00:00Z"
618
+ comments = tuple(model.parse_top_level_comment(record) for record in (bug, request))
619
+
620
+ assert len(model.top_level_findings(comments, DRIVERS)) == 1
621
+ assert model.latest_driver_comment_time(comments, DRIVERS) is None
622
+
623
+
624
+ @pytest.mark.parametrize("user_type,login", [
625
+ ("User", "chatgpt-codex-connector[bot]"),
626
+ ("Bot", "untrusted[bot]"),
627
+ ("User", "maintainer"),
628
+ ])
629
+ @pytest.mark.parametrize("record_index", [1, 2])
630
+ @pytest.mark.parametrize("case", CASES, ids=lambda case: str(case["number"]))
631
+ def should_retain_a_notice_pasted_by_an_untrusted_author(
632
+ user_type: str, login: str, record_index: int, case: dict
633
+ ) -> None:
634
+ record = copy.deepcopy(case["comments"][record_index])
635
+ record["user"] = {"type": user_type, "login": login}
636
+ comment = model.parse_top_level_comment(record)
637
+
638
+ assert not comment.is_notice
639
+ assert len(model.top_level_findings((comment,), DRIVERS)) == 1
640
+
641
+
642
+ @pytest.mark.parametrize("record_index", [0, 1, 2])
643
+ @pytest.mark.parametrize("case", CASES, ids=lambda case: str(case["number"]))
644
+ def should_retain_a_command_or_notice_with_an_appended_finding(record_index: int, case: dict) -> None:
645
+ record = copy.deepcopy(case["comments"][record_index])
646
+ record["body"] += "\n\n[P1] The rollback loses the saved profile."
647
+ comment = model.parse_top_level_comment(record)
648
+
649
+ assert not comment.is_notice
650
+ assert len(model.top_level_findings((comment,), DRIVERS)) == 1
651
+
652
+
653
+ @pytest.mark.parametrize("case", CASES, ids=lambda case: str(case["number"]))
654
+ def should_retain_a_finding_inside_the_clean_review_sentence(case: dict) -> None:
655
+ record = copy.deepcopy(case["comments"][2])
656
+ record["body"] = record["body"].replace(
657
+ "Didn't find any major issues.", "Didn't find any major issues. [P1] The rollback loses a profile."
658
+ )
659
+
660
+ assert not model.parse_top_level_comment(record).is_notice
661
+
662
+
663
+ def should_retain_a_finding_inside_pasted_notice_metadata() -> None:
664
+ record = copy.deepcopy(CASES[0]["comments"][2])
665
+ record["body"] = record["body"].replace(
666
+ "</details>", "[P1] The rollback loses the saved profile.\n</details>"
667
+ )
668
+
669
+ assert not model.parse_top_level_comment(record).is_notice
670
+
671
+
672
+ def should_keep_an_inline_finding_open_beside_clean_notices() -> None:
673
+ thread = model.ReviewThread(
674
+ subject="bin/install.mjs", is_resolved=False, is_outdated=False,
675
+ all_comments=(model.ReviewComment("reviewer", "[P1] The rollback loses a profile."),),
676
+ )
677
+
678
+ findings = model.all_open_findings((thread,), _comments(CASES[0]), DRIVERS, None)
679
+
680
+ assert len(findings) == 1
681
+ assert findings[0].subject == "bin/install.mjs"
682
+
683
+
684
+ def should_keep_a_top_level_bug_open_after_a_stale_clean_verdict() -> None:
685
+ bug = copy.deepcopy(CASES[0]["comments"][0])
686
+ bug["user"]["login"] = "maintainer"
687
+ bug["body"] = "[P1] The rollback loses the saved profile."
688
+ clean = copy.deepcopy(CASES[0]["comments"][2])
689
+ clean["body"] = clean["body"].replace("e123a99375", "aaaaaaaaaa")
690
+ comments = tuple(model.parse_top_level_comment(record) for record in (bug, clean))
691
+
692
+ assert len(model.top_level_findings(comments, DRIVERS)) == 1
693
+
694
+
695
+ def should_keep_a_blocking_approval_open_beside_clean_notices() -> None:
696
+ findings = model.all_open_findings((), _comments(CASES[0]), DRIVERS, "failure")
697
+
698
+ assert len(findings) == 1
699
+ assert findings[0].subject == "Claude Approvals"
@@ -1,22 +0,0 @@
1
- """Constants for the repository-gated issue-tracker SessionStart starter.
2
-
3
- Default off. Set CLAUDE_ISSUE_TRACKER_SESSION_STARTER_ENABLED to 1/true/yes/on.
4
- Even when enabled, injects only when the session cwd's git root is registered
5
- in ~/.claude/project-paths.json. Missing registry fails closed (no inject).
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR: str = (
11
- "CLAUDE_ISSUE_TRACKER_SESSION_STARTER_ENABLED"
12
- )
13
- ALL_ISSUE_TRACKER_STARTER_ENABLED_ENV_VALUES: frozenset[str] = frozenset(
14
- {"1", "true", "yes", "on"}
15
- )
16
- ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS: int = 50
17
-
18
- ISSUE_TRACKER_SESSION_START_DIRECTIVE: str = (
19
- "SessionStart issue-tracker opt-in is active for this registered repository. "
20
- "Load the issue-tracker skill when the session needs one GitHub issue action "
21
- "(open epic, file sub-issue, update status, refresh checklist, close sub-issue)."
22
- )
@@ -1,136 +0,0 @@
1
- #!/usr/bin/env python3
2
- """SessionStart hook — repository-gated issue-tracker context via shared injector.
3
-
4
- Default off. When CLAUDE_ISSUE_TRACKER_SESSION_STARTER_ENABLED is set and the
5
- session cwd's git root appears in ~/.claude/project-paths.json, emit an
6
- additionalContext directive for the issue-tracker skill. Missing registry or
7
- unregistered repos fail closed with no output.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import json
13
- import os
14
- import sys
15
- from pathlib import Path
16
-
17
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
18
- if _hooks_dir not in sys.path:
19
- sys.path.insert(0, _hooks_dir)
20
-
21
- from hooks_constants.issue_tracker_session_starter_constants import ( # noqa: E402
22
- ALL_ISSUE_TRACKER_STARTER_ENABLED_ENV_VALUES,
23
- ISSUE_TRACKER_SESSION_START_DIRECTIVE,
24
- ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR,
25
- ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS,
26
- )
27
- from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
28
- read_hook_input_dictionary_from_stdin,
29
- )
30
- from hooks_constants.project_paths_reader import ( # noqa: E402
31
- find_git_root,
32
- load_registry,
33
- registry_contains_path,
34
- )
35
- from hooks_constants.session_start_injector import ( # noqa: E402
36
- InjectorConfiguration,
37
- build_additional_context_payload,
38
- inject_session_start_context,
39
- )
40
- from hooks_constants.session_start_injector_constants import ( # noqa: E402
41
- ALL_KNOWN_SESSION_START_SOURCES,
42
- )
43
-
44
-
45
- def issue_tracker_session_starter_enabled_in_environment() -> bool:
46
- """Return True only when the opt-in env var holds an enabled value."""
47
- raw_setting = os.environ.get(ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR, "")
48
- return raw_setting.strip().lower() in ALL_ISSUE_TRACKER_STARTER_ENABLED_ENV_VALUES
49
-
50
-
51
- def repository_is_registered(
52
- working_directory: str,
53
- registry_by_name: dict[str, str],
54
- ) -> bool:
55
- """Return True when cwd git root is a value in the project-paths registry.
56
-
57
- Missing registry, missing git root, or unregistered root fail closed.
58
-
59
- Args:
60
- working_directory: Session cwd used to locate the git root.
61
- registry_by_name: Name-to-path map; empty map fails closed.
62
- """
63
- if not registry_by_name:
64
- return False
65
- git_root = find_git_root(working_directory)
66
- if git_root is None:
67
- return False
68
- return registry_contains_path(registry_by_name, git_root)
69
-
70
-
71
- def build_issue_tracker_injector_configuration(
72
- timeout_milliseconds: int,
73
- ) -> InjectorConfiguration:
74
- """Build injector config with the issue-tracker directive per known source.
75
-
76
- Args:
77
- timeout_milliseconds: Injector budget; zero or less yields timeout status.
78
- """
79
- context_by_source = {
80
- each_source: ISSUE_TRACKER_SESSION_START_DIRECTIVE
81
- for each_source in ALL_KNOWN_SESSION_START_SOURCES
82
- }
83
- return InjectorConfiguration(
84
- is_enabled=True,
85
- timeout_milliseconds=timeout_milliseconds,
86
- context_by_source=context_by_source,
87
- default_context_for_unknown="",
88
- )
89
-
90
-
91
- def run_issue_tracker_session_starter(
92
- payload_by_key: dict[str, object],
93
- is_enabled: bool,
94
- is_repository_eligible: bool,
95
- timeout_milliseconds: int,
96
- ) -> dict[str, object]:
97
- """Return additionalContext when opt-in and repo gate both pass, else empty.
98
-
99
- Args:
100
- payload_by_key: Parsed SessionStart payload.
101
- is_enabled: When False, return empty without calling the injector.
102
- is_repository_eligible: When False, return empty without calling the injector.
103
- timeout_milliseconds: Injector timeout budget.
104
-
105
- Returns:
106
- ``{"hookSpecificOutput": {"hookEventName": "SessionStart", ...}}``
107
- when injected, else ``{}``.
108
- """
109
- if not is_enabled or not is_repository_eligible:
110
- return {}
111
- configuration = build_issue_tracker_injector_configuration(timeout_milliseconds)
112
- injection_result = inject_session_start_context(payload_by_key, configuration)
113
- return build_additional_context_payload(injection_result)
114
-
115
-
116
- def main() -> None:
117
- """Emit issue-tracker additionalContext when opt-in and registry gate pass."""
118
- payload_by_key = read_hook_input_dictionary_from_stdin()
119
- if payload_by_key is None:
120
- return
121
- is_enabled = issue_tracker_session_starter_enabled_in_environment()
122
- is_repository_eligible = (
123
- repository_is_registered(os.getcwd(), load_registry()) if is_enabled else False
124
- )
125
- payload = run_issue_tracker_session_starter(
126
- payload_by_key,
127
- is_enabled,
128
- is_repository_eligible,
129
- ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS,
130
- )
131
- if payload:
132
- print(json.dumps(payload))
133
-
134
-
135
- if __name__ == "__main__":
136
- main()
@@ -1,106 +0,0 @@
1
- """Tests for the repository-gated issue-tracker SessionStart starter."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import subprocess
8
- import sys
9
- from pathlib import Path
10
-
11
- import pytest
12
-
13
- _HOOKS_ROOT = Path(__file__).resolve().parent.parent
14
- if str(_HOOKS_ROOT) not in sys.path:
15
- sys.path.insert(0, str(_HOOKS_ROOT))
16
-
17
- from hooks_constants.issue_tracker_session_starter_constants import (
18
- ISSUE_TRACKER_SESSION_START_DIRECTIVE,
19
- ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR,
20
- ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS,
21
- )
22
- from session.issue_tracker_session_starter import (
23
- issue_tracker_session_starter_enabled_in_environment,
24
- repository_is_registered,
25
- run_issue_tracker_session_starter,
26
- )
27
-
28
- STARTER_SCRIPT = Path(__file__).resolve().parent / "issue_tracker_session_starter.py"
29
- DEFAULT_TIMEOUT = ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS
30
-
31
-
32
- def test_default_environment_is_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
33
- monkeypatch.delenv(ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR, raising=False)
34
- assert issue_tracker_session_starter_enabled_in_environment() is False
35
-
36
-
37
- def test_enabled_env_values(monkeypatch: pytest.MonkeyPatch) -> None:
38
- monkeypatch.setenv(ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR, "1")
39
- assert issue_tracker_session_starter_enabled_in_environment() is True
40
- monkeypatch.setenv(ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR, "true")
41
- assert issue_tracker_session_starter_enabled_in_environment() is True
42
-
43
-
44
- def test_disabled_or_ineligible_emits_nothing() -> None:
45
- assert (
46
- run_issue_tracker_session_starter(
47
- {"source": "startup"}, False, True, DEFAULT_TIMEOUT
48
- )
49
- == {}
50
- )
51
- assert (
52
- run_issue_tracker_session_starter(
53
- {"source": "startup"}, True, False, DEFAULT_TIMEOUT
54
- )
55
- == {}
56
- )
57
-
58
-
59
- def test_enabled_eligible_startup_emits_directive() -> None:
60
- payload = run_issue_tracker_session_starter(
61
- {"source": "startup"}, True, True, DEFAULT_TIMEOUT
62
- )
63
- hook_output = payload["hookSpecificOutput"]
64
- assert hook_output["hookEventName"] == "SessionStart"
65
- assert hook_output["additionalContext"] == ISSUE_TRACKER_SESSION_START_DIRECTIVE
66
-
67
-
68
- def test_timeout_zero_emits_nothing() -> None:
69
- assert (
70
- run_issue_tracker_session_starter({"source": "startup"}, True, True, 0) == {}
71
- )
72
-
73
-
74
- def test_unknown_source_emits_nothing() -> None:
75
- assert (
76
- run_issue_tracker_session_starter({"source": "weird"}, True, True, DEFAULT_TIMEOUT)
77
- == {}
78
- )
79
-
80
-
81
- def test_repository_is_registered_fail_closed_empty_registry() -> None:
82
- assert repository_is_registered("/any/path", {}) is False # empty registry fails closed
83
-
84
-
85
- def test_repository_is_registered_matches_git_root(tmp_path: Path) -> None:
86
- (tmp_path / ".git").mkdir()
87
- child = tmp_path / "pkg"
88
- child.mkdir()
89
- registry = {"demo": str(tmp_path)}
90
- assert repository_is_registered(str(child), registry) is True
91
- assert repository_is_registered(str(tmp_path / "other"), {"demo": str(tmp_path / "nope")}) is False
92
-
93
-
94
- def test_main_disabled_prints_nothing() -> None:
95
- environment_by_key = os.environ.copy()
96
- environment_by_key.pop(ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR, None)
97
- completed = subprocess.run(
98
- [sys.executable, str(STARTER_SCRIPT)],
99
- input=json.dumps({"source": "startup"}),
100
- capture_output=True,
101
- text=True,
102
- check=False,
103
- env=environment_by_key,
104
- )
105
- assert completed.returncode == 0
106
- assert completed.stdout.strip() == ""