claude-dev-env 1.40.0 → 1.42.0

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 (66) hide show
  1. package/CLAUDE.md +9 -1
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
  3. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
  4. package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +173 -6
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
  7. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +135 -14
  8. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
  10. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  11. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +4 -2
  12. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  13. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  14. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  15. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  16. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  17. package/hooks/blocking/pr_description_enforcer.py +1 -3
  18. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  19. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  20. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  21. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  22. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  23. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  24. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  25. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  26. package/hooks/config/pr_description_enforcer_constants.py +5 -0
  27. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  28. package/hooks/hooks.json +40 -0
  29. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  30. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  31. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  32. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  33. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  34. package/package.json +1 -1
  35. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  36. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  37. package/skills/bugteam/reference/audit-contract.md +22 -0
  38. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  39. package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
  40. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  41. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
  42. package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
  43. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
  44. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  45. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  46. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  47. package/skills/implement/SKILL.md +66 -0
  48. package/skills/implement/scripts/append_note.py +133 -0
  49. package/skills/implement/scripts/config/__init__.py +0 -0
  50. package/skills/implement/scripts/config/notes_constants.py +12 -0
  51. package/skills/implement/scripts/test_append_note.py +191 -0
  52. package/skills/pr-converge/SKILL.md +8 -2
  53. package/skills/pr-converge/config/constants.py +7 -1
  54. package/skills/pr-converge/reference/state-schema.md +36 -8
  55. package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
  56. package/skills/pr-converge/scripts/check_convergence.py +167 -28
  57. package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
  58. package/skills/pr-converge/scripts/conftest.py +60 -0
  59. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
  60. package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
  61. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  62. package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
  63. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
  64. package/skills/refine/SKILL.md +257 -0
  65. package/skills/refine/templates/implementation-notes-template.html +56 -0
  66. package/skills/refine/templates/plan-template.md +60 -0
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: enforce formal bugteam Skill at Step 5 BUGTEAM ticks.
3
+
4
+ The pr-converge loop's Step 5 BUGTEAM contract requires the formal
5
+ ``Skill({skill: "bugteam", args: "<PR URL>"})`` invocation per tick.
6
+ Substituting an ad-hoc ``Agent({subagent_type: "clean-coder", ...})`` audit
7
+ call returns a "converged" verdict without writing the artifact that
8
+ ``check_convergence.py``'s ``bugteam_clean_at`` gate reads, so the loop later
9
+ hits ``gh pr ready`` and fails structurally with no formal review on the PR.
10
+
11
+ The companion tracker hook
12
+ (``pr_converge_bugteam_skill_tracker.py``) records every formal Skill
13
+ invocation. This enforcer reads the recorded HEAD and tick and denies any
14
+ clean-coder audit-shaped Agent call that has not first been preceded by the
15
+ formal Skill at the same HEAD and tick.
16
+
17
+ ``qbug`` is NOT an accepted substitute; only the ``bugteam`` skill records
18
+ the gate artifact, and the tracker deliberately ignores ``qbug`` invocations.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import sys
25
+ from pathlib import Path
26
+ from typing import TextIO
27
+
28
+
29
+ def _insert_hooks_tree_for_imports() -> None:
30
+ hooks_tree = Path(__file__).resolve().parent.parent
31
+ hooks_tree_string = str(hooks_tree)
32
+ if hooks_tree_string not in sys.path:
33
+ sys.path.insert(0, hooks_tree_string)
34
+
35
+
36
+ _insert_hooks_tree_for_imports()
37
+
38
+ from config.pr_converge_bugteam_enforcer_constants import (
39
+ AGENT_TOOL_NAME,
40
+ ALL_AUDIT_PROMPT_SUBSTRINGS,
41
+ BUGTEAM_PHASE,
42
+ CLEAN_CODER_SUBAGENT_TYPE,
43
+ ENFORCER_CORRECTIVE_MESSAGE,
44
+ STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD,
45
+ STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK,
46
+ STATE_FIELD_CURRENT_HEAD,
47
+ STATE_FIELD_PHASE,
48
+ STATE_FIELD_TICK_COUNT,
49
+ )
50
+ from config.pr_converge_bugteam_enforcer_state import (
51
+ load_state_dictionary,
52
+ resolve_state_path,
53
+ )
54
+
55
+
56
+ def _prompt_is_audit_shaped(agent_prompt: str) -> bool:
57
+ """Return True when the Agent prompt looks like an audit substitute.
58
+
59
+ Args:
60
+ agent_prompt: The ``prompt`` field of the Agent tool_input.
61
+
62
+ Returns:
63
+ True when any audit-shaped substring appears in the prompt
64
+ (case-insensitive); False for fix-only or unrelated prompts.
65
+ """
66
+ lowercased_prompt = agent_prompt.lower()
67
+ return any(
68
+ each_substring in lowercased_prompt for each_substring in ALL_AUDIT_PROMPT_SUBSTRINGS
69
+ )
70
+
71
+
72
+ def _has_formal_skill_fired_this_tick(state_by_field: dict[str, object]) -> bool:
73
+ """Return True when the bugteam Skill registered at current HEAD and tick.
74
+
75
+ Args:
76
+ state_by_field: Parsed pr-converge state.json mapping each field name
77
+ to its recorded value.
78
+
79
+ Returns:
80
+ True when both ``bugteam_skill_invoked_at_head`` matches
81
+ ``current_head`` and ``bugteam_skill_invoked_at_tick`` matches
82
+ ``tick_count``; False when either is missing, stale, or carries a
83
+ type that violates state-schema.md (head must be ``str``, tick must
84
+ be a non-bool ``int``) — type-invalid values fail closed so the
85
+ enforcer rejects corrupted state.
86
+ """
87
+ invoked_head = state_by_field.get(STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD)
88
+ current_head = state_by_field.get(STATE_FIELD_CURRENT_HEAD)
89
+ invoked_tick = state_by_field.get(STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK)
90
+ current_tick = state_by_field.get(STATE_FIELD_TICK_COUNT)
91
+ if not isinstance(invoked_head, str) or not isinstance(current_head, str):
92
+ return False
93
+ if invoked_head != current_head:
94
+ return False
95
+ if isinstance(invoked_tick, bool) or isinstance(current_tick, bool):
96
+ return False
97
+ if not isinstance(invoked_tick, int) or not isinstance(current_tick, int):
98
+ return False
99
+ return invoked_tick == current_tick
100
+
101
+
102
+ def _should_block(payload_by_field: dict[str, object]) -> bool:
103
+ """Return True when the Agent call is a BUGTEAM-phase Skill substitution.
104
+
105
+ Args:
106
+ payload_by_field: The full PreToolUse hook payload (already JSON-parsed),
107
+ keyed by top-level field name.
108
+
109
+ Returns:
110
+ True when every gating condition holds: tool is Agent, subagent_type
111
+ is clean-coder, prompt is audit-shaped, pr-converge state.json exists
112
+ with phase BUGTEAM, and the formal bugteam Skill has NOT been
113
+ recorded at the current HEAD and tick. False otherwise.
114
+ """
115
+ if payload_by_field.get("tool_name", "") != AGENT_TOOL_NAME:
116
+ return False
117
+ tool_input = payload_by_field.get("tool_input", {})
118
+ if not isinstance(tool_input, dict):
119
+ return False
120
+ if tool_input.get("subagent_type", "") != CLEAN_CODER_SUBAGENT_TYPE:
121
+ return False
122
+ agent_prompt = tool_input.get("prompt", "")
123
+ if not isinstance(agent_prompt, str):
124
+ return False
125
+ if not _prompt_is_audit_shaped(agent_prompt):
126
+ return False
127
+ state_path = resolve_state_path()
128
+ if state_path is None:
129
+ return False
130
+ parsed_state = load_state_dictionary(state_path)
131
+ if parsed_state is None:
132
+ return False
133
+ if parsed_state.get(STATE_FIELD_PHASE) != BUGTEAM_PHASE:
134
+ return False
135
+ return not _has_formal_skill_fired_this_tick(parsed_state)
136
+
137
+
138
+ def _emit_deny_payload(output_stream: TextIO) -> None:
139
+ """Write the PreToolUse deny payload to the provided stream.
140
+
141
+ Args:
142
+ output_stream: Writable text stream — production code passes
143
+ ``sys.stdout``; tests pass a ``StringIO`` to capture the JSON.
144
+ """
145
+ deny_payload = {
146
+ "hookSpecificOutput": {
147
+ "hookEventName": "PreToolUse",
148
+ "permissionDecision": "deny",
149
+ "permissionDecisionReason": ENFORCER_CORRECTIVE_MESSAGE,
150
+ }
151
+ }
152
+ output_stream.write(json.dumps(deny_payload) + "\n")
153
+ output_stream.flush()
154
+
155
+
156
+ def main() -> None:
157
+ try:
158
+ hook_payload = json.load(sys.stdin)
159
+ except json.JSONDecodeError:
160
+ sys.exit(0)
161
+ if not isinstance(hook_payload, dict):
162
+ sys.exit(0)
163
+ if not _should_block(hook_payload):
164
+ sys.exit(0)
165
+ _emit_deny_payload(sys.stdout)
166
+ sys.exit(0)
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -39,12 +39,10 @@ from config.pr_description_enforcer_constants import (
39
39
  INLINE_CODE_PATTERN,
40
40
  LINK_TEXT_PATTERN,
41
41
  MINIMUM_SUBSTANTIVE_PROSE_CHARS,
42
+ PR_GUIDE_PATH,
42
43
  WHITESPACE_RUN_PATTERN,
43
44
  )
44
45
 
45
- PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
46
- PR_GUIDE_PATH = os.path.join(PLUGIN_ROOT, "docs", "PR_DESCRIPTION_GUIDE.md")
47
-
48
46
  VAGUE_LANGUAGE_PATTERN = re.compile(
49
47
  r'\b(fix(?:ed)? (?:bug|issue|it)|update(?:d)? code|minor changes|various (?:fixes|updates|improvements))\b',
50
48
  re.IGNORECASE,
@@ -401,10 +401,15 @@ def test_has_backtick_bash_continuation_stripped() -> None:
401
401
  assert not _has_backtick(command)
402
402
 
403
403
 
404
- def test_has_backtick_powershell_continuation_stripped() -> None:
405
- """PowerShell backtick line continuations are stripped before checking."""
404
+ def test_has_backtick_powershell_continuation_not_stripped_in_bash_context() -> None:
405
+ """PowerShell backtick continuations are real backticks under the Bash tool.
406
+
407
+ The hook runs on Bash tool invocations where PowerShell-style backtick
408
+ continuations are not continuation markers. Any backtick character in
409
+ the command is a literal backtick that must be detected.
410
+ """
406
411
  command = 'gh pr create `\n --title "T" `\n --body "bugbot run"\n'
407
- assert not _has_backtick(command)
412
+ assert _has_backtick(command)
408
413
 
409
414
 
410
415
  def test_has_backtick_content_backtick_at_line_end() -> None:
@@ -418,3 +423,20 @@ def test_has_backtick_multi_line_body() -> None:
418
423
  command = 'gh pr create --title "T" --body "First line.\nSecond with `code`."'
419
424
  assert _has_backtick(command)
420
425
 
426
+
427
+ def test_has_backtick_inside_unclosed_double_quoted_body_value() -> None:
428
+ """Trailing backtick inside an unclosed --body "..." opening line counts as body content.
429
+
430
+ The first line carries an opening double-quote for --body that is not
431
+ closed on that line (odd quote count = 3). The trailing backtick is body
432
+ content (an inline-code opening), not a PowerShell continuation marker.
433
+ The unclosed-quote check preserves the original line including its
434
+ trailing backtick, so _has_backtick sees the backtick and returns True.
435
+ """
436
+ command = (
437
+ 'gh pr create --title "T" --body "Thanks `\n'
438
+ ' this continues the body\n'
439
+ ' with more text"\n'
440
+ )
441
+ assert _has_backtick(command)
442
+