claude-dev-env 8.46.5 → 8.46.6

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.
@@ -13,6 +13,8 @@ if hooks_directory not in sys.path:
13
13
  sys.path.insert(0, hooks_directory)
14
14
 
15
15
  from hooks_constants.pr_lifecycle_skill_gate_constants import (
16
+ AGENT_ID_FIELD,
17
+ AGENT_ID_PATTERN,
16
18
  ALL_API_ACTION_NAMES,
17
19
  ALL_COMMAND_PREFIX_WORDS,
18
20
  ALL_GITHUB_MCP_TOOL_SUFFIXES,
@@ -40,8 +42,11 @@ from hooks_constants.pr_lifecycle_skill_gate_constants import (
40
42
  PERMISSION_DECISION_REASON_KEY,
41
43
  PULL_REQUEST_SCRIPT_NAME,
42
44
  PYTHON_OPTIONS_WITH_VALUE,
45
+ SESSION_TRANSCRIPT_PATH_FIELD,
43
46
  SHELL_TOOL_NAMES,
44
47
  SKILL_NAME,
48
+ SUBAGENT_TRANSCRIPT_DIRECTORY_NAME,
49
+ SUBAGENT_TRANSCRIPT_FILE_TEMPLATE,
45
50
  )
46
51
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
47
52
  from hooks_constants.setup_project_paths_constants import DECODE_ERRORS_POLICY, UTF8_ENCODING
@@ -174,6 +179,21 @@ def _transcript_load_status(path: str) -> bool | None:
174
179
  return None
175
180
 
176
181
 
182
+ def _subagent_transcript_path(all_payload_fields: dict[str, object]) -> str | None:
183
+ session_path = all_payload_fields.get(SESSION_TRANSCRIPT_PATH_FIELD)
184
+ agent_id = all_payload_fields.get(AGENT_ID_FIELD)
185
+ if not isinstance(session_path, str) or not session_path:
186
+ return None
187
+ if not isinstance(agent_id, str) or not re.fullmatch(AGENT_ID_PATTERN, agent_id):
188
+ return None
189
+ subagent_path = (
190
+ Path(session_path).with_suffix("")
191
+ / SUBAGENT_TRANSCRIPT_DIRECTORY_NAME
192
+ / SUBAGENT_TRANSCRIPT_FILE_TEMPLATE.format(agent_id=agent_id)
193
+ )
194
+ return str(subagent_path) if subagent_path.is_file() else None
195
+
196
+
177
197
  def decision_for(all_payload_fields: dict[str, object]) -> dict[str, object] | None:
178
198
  """Return a deny for a governed action with readable, unloaded transcripts.
179
199
 
@@ -183,7 +203,8 @@ def decision_for(all_payload_fields: dict[str, object]) -> dict[str, object] | N
183
203
  if not _is_governed_action(all_payload_fields):
184
204
  return None
185
205
  paths = [all_payload_fields.get(field) for field in ALL_TRANSCRIPT_PATH_FIELDS]
186
- readable_paths = [path for path in paths if isinstance(path, str) and path]
206
+ paths.append(_subagent_transcript_path(all_payload_fields))
207
+ readable_paths = list(dict.fromkeys(path for path in paths if isinstance(path, str) and path))
187
208
  if not readable_paths:
188
209
  return None
189
210
  statuses = [_transcript_load_status(path) for path in readable_paths]
@@ -82,6 +82,49 @@ def test_agent_transcript_can_supply_invocation(tmp_path: Path) -> None:
82
82
  assert gate.decision_for(payload) is None
83
83
 
84
84
 
85
+ def _subagent_transcript(tmp_path: Path, agent_id: str, skill_name: str | None) -> Path:
86
+ subagent_directory = tmp_path / "session" / "subagents"
87
+ subagent_directory.mkdir(parents=True)
88
+ subagent_path = subagent_directory / f"agent-{agent_id}.jsonl"
89
+ lines = [json.dumps({"type": "user", "message": {"content": "Task."}})]
90
+ if skill_name:
91
+ lines.append(json.dumps({"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Skill", "input": {"skill": skill_name}}]}}))
92
+ subagent_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
93
+ return subagent_path
94
+
95
+
96
+ def test_subagent_transcript_located_by_agent_id_can_supply_invocation(tmp_path: Path) -> None:
97
+ session_path = _transcript(tmp_path)
98
+ _subagent_transcript(tmp_path, "a8865a3f3c71595ce", "pr-lifecycle")
99
+ payload = _payload("git commit", session_path)
100
+ payload["agent_id"] = "a8865a3f3c71595ce"
101
+ assert _run_main(payload) == (0, "")
102
+
103
+
104
+ def test_subagent_without_invocation_is_denied(tmp_path: Path) -> None:
105
+ session_path = _transcript(tmp_path)
106
+ _subagent_transcript(tmp_path, "a8865a3f3c71595ce", None)
107
+ payload = _payload("git push", session_path)
108
+ payload["agent_id"] = "a8865a3f3c71595ce"
109
+ _assert_denied(payload)
110
+
111
+
112
+ def test_missing_subagent_transcript_keeps_session_deny(tmp_path: Path) -> None:
113
+ payload = _payload("git push", _transcript(tmp_path))
114
+ payload["agent_id"] = "a8865a3f3c71595ce"
115
+ _assert_denied(payload)
116
+
117
+
118
+ def test_agent_id_with_path_separator_is_not_followed(tmp_path: Path) -> None:
119
+ session_path = _transcript(tmp_path)
120
+ escaped_path = tmp_path / "session" / "agent-x.jsonl"
121
+ escaped_path.parent.mkdir(parents=True)
122
+ escaped_path.write_text(json.dumps({"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Skill", "input": {"skill": "pr-lifecycle"}}]}}) + "\n", encoding="utf-8")
123
+ payload = _payload("git push", session_path)
124
+ payload["agent_id"] = "../agent-x"
125
+ _assert_denied(payload)
126
+
127
+
85
128
  def test_unreadable_or_missing_transcript_allows_silently(tmp_path: Path) -> None:
86
129
  payload = _payload("gh pr create", tmp_path / "missing.jsonl")
87
130
  assert _run_main(payload) == (0, "")
@@ -18,6 +18,11 @@ ALL_GITHUB_MCP_TOOL_SUFFIXES = (
18
18
  "__update_pull_request",
19
19
  )
20
20
  ALL_TRANSCRIPT_PATH_FIELDS = ("transcript_path", "agent_transcript_path")
21
+ SESSION_TRANSCRIPT_PATH_FIELD = "transcript_path"
22
+ AGENT_ID_FIELD = "agent_id"
23
+ AGENT_ID_PATTERN = r"[A-Za-z0-9_-]+"
24
+ SUBAGENT_TRANSCRIPT_DIRECTORY_NAME = "subagents"
25
+ SUBAGENT_TRANSCRIPT_FILE_TEMPLATE = "agent-{agent_id}.jsonl"
21
26
  COMMAND_SEPARATORS = ";|&\n()`"
22
27
  COMMAND_WHITESPACE = " \t\r"
23
28
  OPTION_AND_VALUE_WORD_COUNT = 2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.46.5",
3
+ "version": "8.46.6",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {