claude-dev-env 8.46.5 → 8.46.7

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.
@@ -22,7 +22,7 @@ Before writing a single line — **task-local discovery only** (no project-wide
22
22
  1. **Load scoped repository instructions first.** Starting at the repository root, read every applicable `AGENTS.md` on the path to the task file. Then read the applicable `CLAUDE.md` files. Apply nearer instructions after broader ones; the closest file wins.
23
23
  2. **Read the file you are about to edit** (when editing existing code). Note every existing comment so you can leave each one untouched on lines that remain otherwise unchanged.
24
24
  3. **Discover config only next to the task files.** From each file you will write or edit, walk up to the nearest package or repo root and inspect the target package's existing constants layout — such as `config/` or a sibling `*_constants` package. Keep this task-local constants search. Do **not** force a generic `config/` layout. Do **not** glob the whole tree for every config file. Do **not** glob or open `.env`, `.env.*`, or other secret files.
25
- 4. **Reuse constants from that local table.** Reuse first: exact value match → import the existing name. Semantic match → reuse it. Add a shared constant only when the value is shared policy or has multiple consumers. When no match exists, use the target package's existing constants layout, and keep a one-use value in the scope that reads it rather than making it a new shared constant.
25
+ 4. **Reuse constants from that local table.** Reuse first: exact value match → import the existing name. Semantic match → reuse it. Add a shared constant only when the value is shared policy or has multiple consumers. When no match exists, use the target package's existing constants layout, and keep a one-use value in the scope that reads it.
26
26
  5. **Search callers.** When a symbol, name, or signature changes, search its full caller boundary and update every consumer. This search may be wider than the constants search.
27
27
 
28
28
  ## Generation mindset (9 laws)
@@ -210,7 +210,7 @@ Every Shape A finding cites a file path and a line number. The offending line is
210
210
  Do not infer missing context. If the diff and allowed checks cannot confirm a
211
211
  claim, use an open question or an evidence gap.
212
212
 
213
- When the diff alone lacks the context to confirm a finding, list the item under an "Open questions" section rather than asserting it as a Shape A finding. Each open question names the file and line where uncertainty arose and states what additional context would resolve it.
213
+ When the diff alone lacks the context to confirm a finding, list the item under an "Open questions" section and keep it out of the Shape A findings. Each open question names the file and line where uncertainty arose and states what additional context would resolve it.
214
214
 
215
215
  ```json
216
216
  {
@@ -150,7 +150,7 @@ For each acceptance line from the brief:
150
150
  1. Keep task-specific detail in its own part file so the brief templates stay
151
151
  reusable.
152
152
  2. Absolute paths only in `prompt_parts` — the launcher reads them as given.
153
- 3. One worker, one closed scope. Split large work into more workers rather than
154
- one long brief.
153
+ 3. One worker, one closed scope. Split large work into more workers, each with a
154
+ short brief.
155
155
  4. The lead session fills bracketed fields before launch; workers never see the
156
156
  skill folder unless you copy text into their part files.
@@ -50,11 +50,11 @@ def check_comments_python(content: str) -> list[str]:
50
50
  Hash characters that appear inside string literals (hex color codes,
51
51
  URL fragments, and the hash inside an f-string interpolation pattern)
52
52
  are correctly skipped because the tokenizer recognizes them as parts
53
- of string tokens rather than comment tokens.
53
+ of string tokens.
54
54
 
55
55
  When the tokenizer cannot parse the file (partial content during
56
- Edit, invalid syntax), the check returns no findings rather than
57
- falling back to a line-walker scan — false negatives on
56
+ Edit, invalid syntax), the check returns no findings and does not
57
+ fall back to a line-walker scan — false negatives on
58
58
  syntactically-invalid drafts are preferable to false positives that
59
59
  mis-classify string-interior hash characters as comments.
60
60
  """
@@ -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.7",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {