claude-dev-env 2.0.1 → 2.0.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 (42) hide show
  1. package/agents/clean-coder.md +31 -1
  2. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  3. package/bin/install.test.mjs +2 -2
  4. package/docs/CLAUDE.md +6 -3
  5. package/docs/CODE_RULES.md +5 -1
  6. package/docs/agent-spawn-protocol.md +39 -0
  7. package/docs/nas-ssh-invocation.md +23 -0
  8. package/docs/worker-completion-gate.md +33 -0
  9. package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
  10. package/hooks/blocking/config/verified_commit_constants.py +8 -1
  11. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
  12. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
  13. package/hooks/blocking/test_verification_verdict_store.py +12 -0
  14. package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
  15. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
  16. package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
  17. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  18. package/package.json +1 -1
  19. package/rules/CLAUDE.md +2 -3
  20. package/rules/agent-spawn-protocol.md +5 -43
  21. package/rules/code-standards.md +1 -36
  22. package/rules/env-var-table-code-drift.md +2 -21
  23. package/rules/hook-prose-matches-detector.md +5 -16
  24. package/rules/nas-ssh-invocation.md +3 -15
  25. package/rules/no-historical-clutter.md +7 -49
  26. package/rules/no-inline-destructive-literals.md +3 -5
  27. package/rules/package-inventory-stale-entry.md +7 -32
  28. package/rules/re-stage-before-commit.md +6 -23
  29. package/rules/shell-invocation-policy.md +1 -1
  30. package/rules/vault-context.md +3 -3
  31. package/rules/workers-done-before-complete.md +2 -30
  32. package/scripts/claude_chain_runner.py +39 -3
  33. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
  34. package/scripts/test_claude_chain_runner.py +112 -0
  35. package/scripts/test_grok_headless_runner.py +0 -1
  36. package/skills/auditing-claude-config/SKILL.md +1 -1
  37. package/skills/everything-search/SKILL.md +7 -1
  38. package/skills/orchestrator/SKILL.md +161 -147
  39. package/system-prompts/CLAUDE.md +3 -3
  40. package/docs/agents-md-alignment-plan.md +0 -123
  41. package/docs/emotion-informed-prompt-design.md +0 -362
  42. package/rules/es-exe-file-search.md +0 -17
@@ -1,31 +1,14 @@
1
1
  # Re-Stage Session Edits Before Commit
2
2
 
3
- **When this applies:** Any `git commit` run through the Bash tool in a git repository.
3
+ Stage the files you edited this session right before you commit them. A plain `git commit` records only the staged snapshot; a tracked file this session changed but left unstaged stays behind in the working tree.
4
4
 
5
- ## Rule
5
+ `session_edit_stage_gate` (PreToolUse on Bash `git commit`) denies a commit that would drop tracked session edits and names the fix: `git add <paths>`, `git commit -a`, or a `# partial-commit` marker.
6
6
 
7
- Stage the files you edited this session right before you commit them. A file this session changed but left unstaged is dropped by a plain `git commit` — the commit records the staged snapshot and the edit stays behind in the working tree.
7
+ ## Escapes the denial does not restate
8
8
 
9
- The `session_edit_stage_gate` hook holds you to this. It reads the per-session tracker that records every file the session edited, checks which of those files are tracked yet still unstaged at commit time, and denies the commit when any are left out. The denial names each file and gives the exact fix: `git add <paths>`, `git commit -a`, or a `# partial-commit` marker.
10
-
11
- ## What the gate allows
12
-
13
- The gate steps aside for a commit that skips staged files on purpose:
14
-
15
- - **`-a` / `--all`** — the commit already takes every tracked change, so nothing is dropped.
16
- - **A pathspec** — `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose.
17
- - **A preceding `git add` / `git stage`** — `git add <paths> && git commit …` stages the files in its own segment before the commit runs, so they are staged by the time the commit records the index.
18
- - **`# partial-commit`** — add this marker to the command to commit the staged set on purpose and leave the rest.
9
+ - **A pathspec** `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose and steps the gate aside.
10
+ - **A preceding `git add` / `git stage`** — `git add <paths> && git commit …` stages the files in its own segment before the commit runs.
19
11
 
20
12
  A `--amend` does not step the gate aside: an amend records the staged snapshot too, so an unstaged session edit is dropped the same way a plain commit drops it.
21
13
 
22
- A missing tracker file or any git failure allows the commit, so the gate never blocks on a tooling problem.
23
-
24
- ## Companion hooks
25
-
26
- - `session_file_edit_tracker` (PostToolUse) records the resolved absolute path of each Write, Edit, and MultiEdit into the per-session tracker file.
27
- - `session_edit_tracker_cleanup` (SessionStart, SessionEnd) deletes the running session's own tracker file: at a fresh SessionStart so a new session begins with an empty record, and at SessionEnd so a clean exit leaves nothing behind. A tracker is read only by the session that wrote it, so a file a crashed session leaves behind is inert and no peer session touches it.
28
-
29
- ## Why
30
-
31
- A stale git index is a quiet failure: the commit succeeds, the branch looks right, and one file you meant to include never lands. Catching it at commit time keeps the staged set and the session's edits in step.
14
+ `session_file_edit_tracker` (PostToolUse) records each Write/Edit/MultiEdit path; `session_edit_tracker_cleanup` (SessionStart, SessionEnd) clears the session's tracker.
@@ -2,4 +2,4 @@
2
2
 
3
3
  Every Bash-tool shell command on Windows uses `pwsh`: `pwsh -NoProfile -File '<script>.ps1' <args>` for scripts, `pwsh -NoProfile -Command "..."` (or a literal `@'...'@` here-string) for inline work, or the built-in `PowerShell` tool for pure-PowerShell workflows (it supports `run_in_background`). Never wrap a script path in `-Command "& '...'"` — `-File` keeps `permissions.allow` matching. The `&` call operator is fine for invoking an executable at a path (`& '<venv>\Scripts\python.exe' script.py`).
4
4
 
5
- `powershell`, `powershell.exe`, `cmd /c`, and `bash -c` are blocked by `permissions.deny` and the `pwsh_enforcer.py` PreToolUse hook, which returns the corrective pattern. Audit and migration scripts (`Audit-ShellPolicy.ps1`, `Migrate-ShellPolicy.ps1`) live in `packages/claude-dev-env/scripts/`.
5
+ Keep `powershell`, `powershell.exe`, `cmd /c`, and `bash -c` out of the `settings.json` permission rules. `Audit-ShellPolicy.ps1` reports those forms and `Migrate-ShellPolicy.ps1` rewrites them to `pwsh`, both in `packages/claude-dev-env/scripts/` and run on demand, not as a live gate.
@@ -1,7 +1,7 @@
1
1
  # Obsidian Vault Context
2
2
 
3
- An Obsidian vault stores session reports (`sessions/`), decisions (`decisions/`), and research (`Research/`) across projects. Resolve its location via the obsidian MCP tools — `mcp__obsidian__search_notes` (supports `searchFrontmatter: true`), `mcp__obsidian__read_note`, `mcp__obsidian__read_multiple_notes` — never assume an OS path.
3
+ An Obsidian vault holds session reports (`sessions/`), decisions (`decisions/`), and research (`Research/`) across projects, reached through the obsidian MCP tools.
4
4
 
5
- IMPORTANT: Before substantive project work, search the vault for prior sessions and decisions for the current project — by `project` frontmatter first, then keywords ("blocked", "superseded", "decision", "gotcha"). Also search when touching a component with known history or when a task might repeat or reverse a prior decision.
5
+ Before substantive project work, search the vault for prior sessions and decisions on the current project through the `recall` skill — by `project` frontmatter first, then by keyword. Search again when touching a component with known history, or when a task might repeat or reverse a prior decision.
6
6
 
7
- Session logging runs through `/session-log` (same full flow for short and long sessions); offer it at the end of substantive sessions. Reports include `vault_context_retrieved: true|false` and `session_id` (from `CLAUDE_CODE_SESSION_ID`; literal `unknown` when unset) in frontmatter, and every session log ends with a `/rename [Project] - [Primary Outcome]` command — mandatory output, never optional.
7
+ At the end of a substantive session, offer the `session-log` skill; it owns the report format, the `vault_context_retrieved` and `session_id` frontmatter, and the closing `/rename` hand-off.
@@ -1,33 +1,5 @@
1
1
  # Workers Done Before Complete
2
2
 
3
- **When this applies:** Before marking any task `completed` when the task spawned workers — subagents, workflow agents, or background shells.
3
+ Before marking a task `completed` that spawned workers — subagents, workflow agents, or background shells — confirm both: every worker has finished, and each worker's result is merged into run state (`state.json`, `pr-converge-state.json`, the task list, or whatever record the task keeps). A worker still running, or one whose output never landed in run state, keeps the task `in_progress`: list the live workers, report any dead or hung one as a finding rather than dropping it in silence, and schedule a wakeup so the run picks the workers back up before the task closes.
4
4
 
5
- ## Rule
6
-
7
- A task reaches `completed` only when two things hold: every worker it spawned has finished, and each worker's result is merged into run state — `state.json`, `pr-converge-state.json`, the task list, or whatever record the task keeps. A worker that still runs, or one that finished but whose output never landed in run state, leaves the task open.
8
-
9
- List the live workers before you mark the task complete. When a worker is dead or hung, that is a finding to record and report, not a result to drop in silence. A step that waits on workers ends its turn `in_progress` with a wakeup scheduled, so the run picks the workers back up rather than closing the task without them.
10
-
11
- ## Checklist before marking complete
12
-
13
- | Check | Action |
14
- |---|---|
15
- | Are any spawned workers still running? | List them; if yes, stay `in_progress` and schedule a wakeup. |
16
- | Did every finished worker return a result? | Read each result; a dead or hung worker is a finding to report. |
17
- | Is each result merged into run state? | Write it to `state.json` / the task list before closing. |
18
- | Does the task's own goal now hold? | Confirm against the merged state, not a worker's self-report. |
19
-
20
- Mark `completed` only when every row passes.
21
-
22
- ## Examples
23
-
24
- **Wrong:** Marking the audit task complete while two bugteam workers still run in the background.
25
- **Right:** List the workers, see two still running, keep the task `in_progress`, and schedule a wakeup to collect them.
26
-
27
- **Wrong:** A worker crashes; the task closes as complete because the other workers finished.
28
- **Right:** Record the crashed worker as a finding, report it, and hold the task open until its work is covered.
29
-
30
- ## Relationship to other rules
31
-
32
- - **`long-horizon-autonomy`** covers acting on what you have and not ending a turn on a promise. This rule names the specific completion gate: workers finished and their results merged.
33
- - **`skills/pr-converge/reference/state-schema.md`** defines the run-state records this rule requires a worker's result to land in before the task closes.
5
+ Checklist, examples, and run-state detail: `@~/.claude/docs/worker-completion-gate.md`.
@@ -19,6 +19,12 @@ A usage-limited primary falls over to the second binary::
19
19
  primary claude -> exit 1, "usage limit reached" (falls over)
20
20
  fallback claude-ev -> exit 0 (served)
21
21
 
22
+ When stdin is piped (not a TTY), the runner reads it once and forwards the
23
+ same text to every chain attempt so a piped ``-p`` charter body reaches each
24
+ binary in the walk::
25
+
26
+ cat charter.md | python claude_chain_runner.py -- -p --strict-mcp-config
27
+
22
28
  Import ``run_claude`` for the outcome object, or run the module as a CLI::
23
29
 
24
30
  python claude_chain_runner.py [--timeout-seconds N] -- <claude args...>
@@ -27,11 +33,13 @@ Import ``run_claude`` for the outcome object, or run the module as a CLI::
27
33
  from __future__ import annotations
28
34
 
29
35
  import argparse
36
+ import io
30
37
  import json
31
38
  import subprocess
32
39
  import sys
33
40
  from dataclasses import dataclass
34
41
  from pathlib import Path
42
+ from typing import TextIO
35
43
 
36
44
  from dev_env_scripts_constants.claude_chain_constants import (
37
45
  ALL_USAGE_LIMIT_SIGNATURES,
@@ -48,6 +56,7 @@ from dev_env_scripts_constants.claude_chain_constants import (
48
56
  CLAUDE_HOME_SUBDIRECTORY,
49
57
  CLI_ARGUMENTS_SEPARATOR,
50
58
  CLI_TIMEOUT_FLAG,
59
+ CODEC_ERROR_STRATEGY,
51
60
  CONFIG_CHAIN_EMPTY_REASON,
52
61
  CONFIG_CHAIN_KEY,
53
62
  CONFIG_CHAIN_NOT_LIST_REASON,
@@ -283,7 +292,10 @@ def _classify_completion(
283
292
 
284
293
 
285
294
  def run_claude(
286
- all_claude_arguments: list[str], *, timeout_seconds: int
295
+ all_claude_arguments: list[str],
296
+ *,
297
+ timeout_seconds: int,
298
+ stdin_text: str | None = None,
287
299
  ) -> ChainInvocationOutcome:
288
300
  """Run *all_claude_arguments* through the configured fallback chain.
289
301
 
@@ -291,12 +303,16 @@ def run_claude(
291
303
  exit whose output carries a usage-limit signature) falls over to the next
292
304
  binary. A missing fallback binary is skipped and the walk continues. A
293
305
  timeout, a missing primary binary, or a non-zero exit without a usage-limit
294
- signature stops the walk and returns that outcome unchanged.
306
+ signature stops the walk and returns that outcome unchanged. When
307
+ *stdin_text* is set, that same text is supplied as stdin on every chain
308
+ attempt.
295
309
 
296
310
  Args:
297
311
  all_claude_arguments: Arguments passed after the binary name, such as
298
312
  ``["-p", prompt, "--strict-mcp-config"]``.
299
313
  timeout_seconds: Timeout applied to each binary invocation.
314
+ stdin_text: Optional UTF-8 text forwarded as stdin to every binary.
315
+ ``None`` leaves the subprocess without a piped stdin body.
300
316
 
301
317
  Returns:
302
318
  The outcome of the walk, naming the serving binary and the full
@@ -315,8 +331,11 @@ def run_claude(
315
331
  _build_invocation(each_entry, all_claude_arguments),
316
332
  capture_output=True,
317
333
  text=True,
334
+ encoding=UTF8_ENCODING,
335
+ errors=CODEC_ERROR_STRATEGY,
318
336
  timeout=timeout_seconds,
319
337
  check=False,
338
+ input=stdin_text,
320
339
  )
321
340
  except subprocess.TimeoutExpired as timeout_error:
322
341
  all_attempts.append(
@@ -368,6 +387,12 @@ def _exhausted_message(all_attempts: tuple[ChainAttempt, ...]) -> str:
368
387
  return CHAIN_EXHAUSTED_MESSAGE_TEMPLATE.format(attempt_summary=attempt_summary)
369
388
 
370
389
 
390
+ def _read_piped_stdin_text() -> str | None:
391
+ if sys.stdin.isatty():
392
+ return None
393
+ return sys.stdin.read()
394
+
395
+
371
396
  def main(all_command_arguments: list[str]) -> int:
372
397
  """Walk the chain for CLI arguments and return the process exit code.
373
398
 
@@ -381,9 +406,12 @@ def main(all_command_arguments: list[str]) -> int:
381
406
  parser = _build_argument_parser()
382
407
  parsed_arguments = parser.parse_args(all_command_arguments)
383
408
  all_claude_arguments = _strip_leading_separator(parsed_arguments.passthrough)
409
+ maybe_stdin_text = _read_piped_stdin_text()
384
410
  try:
385
411
  chain_outcome = run_claude(
386
- all_claude_arguments, timeout_seconds=parsed_arguments.timeout_seconds
412
+ all_claude_arguments,
413
+ timeout_seconds=parsed_arguments.timeout_seconds,
414
+ stdin_text=maybe_stdin_text,
387
415
  )
388
416
  except ChainConfigurationError as configuration_error:
389
417
  print(str(configuration_error), file=sys.stderr)
@@ -396,5 +424,13 @@ def main(all_command_arguments: list[str]) -> int:
396
424
  return chain_outcome.returncode
397
425
 
398
426
 
427
+ def _reconfigure_stream_to_utf8(stream: TextIO) -> None:
428
+ """Reconfigure *stream* to emit UTF-8, replacing any unmappable character."""
429
+ if isinstance(stream, io.TextIOWrapper):
430
+ stream.reconfigure(encoding=UTF8_ENCODING, errors=CODEC_ERROR_STRATEGY)
431
+
432
+
399
433
  if __name__ == "__main__":
434
+ _reconfigure_stream_to_utf8(sys.stdout)
435
+ _reconfigure_stream_to_utf8(sys.stderr)
400
436
  sys.exit(main(sys.argv[1:]))
@@ -9,6 +9,9 @@ from __future__ import annotations
9
9
  UTF8_ENCODING: str = "utf-8"
10
10
  """Encoding used to read the chain configuration file."""
11
11
 
12
+ CODEC_ERROR_STRATEGY: str = "replace"
13
+ """Codec error handler that maps any unencodable or undecodable value to a marker."""
14
+
12
15
  CLAUDE_HOME_SUBDIRECTORY: str = ".claude"
13
16
  """Per-user directory under the home directory that holds the chain config."""
14
17
 
@@ -1,6 +1,7 @@
1
1
  """Behavioral tests for the claude fallback-chain runner."""
2
2
 
3
3
  import json
4
+ import os
4
5
  import subprocess
5
6
  import sys
6
7
  from pathlib import Path
@@ -78,20 +79,32 @@ class _Recorder:
78
79
  self.behavior_by_command = behavior_by_command
79
80
  self.invocations = []
80
81
  self.timeouts = []
82
+ self.all_keyword_arguments = []
81
83
 
82
84
  def __call__(self, invocation, **keyword_arguments):
83
85
  self.invocations.append(invocation)
84
86
  self.timeouts.append(keyword_arguments.get("timeout"))
87
+ self.all_keyword_arguments.append(keyword_arguments)
85
88
  behavior = self.behavior_by_command[invocation[0]]
86
89
  if isinstance(behavior, BaseException):
87
90
  raise behavior
88
91
  return behavior
89
92
 
90
93
 
94
+ class _TtyStdin:
95
+ def isatty(self) -> bool:
96
+ return True
97
+
98
+
99
+ def _install_tty_stdin(monkeypatch):
100
+ monkeypatch.setattr(runner.sys, "stdin", _TtyStdin())
101
+
102
+
91
103
  def _install(monkeypatch, config_file, behavior_by_command):
92
104
  recorder = _Recorder(behavior_by_command)
93
105
  monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
94
106
  monkeypatch.setattr(runner, "chain_subprocess_runner", recorder)
107
+ _install_tty_stdin(monkeypatch)
95
108
  return recorder
96
109
 
97
110
 
@@ -276,6 +289,45 @@ def test_extra_args_are_appended_to_invocation(
276
289
  assert recorder.invocations[0] == ["claude", "-p", "hello", "--account", "ev"]
277
290
 
278
291
 
292
+ def test_run_claude_forwards_stdin_text_to_subprocess(
293
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
294
+ ) -> None:
295
+ config_file = _write_chain_config(tmp_path, [_entry("claude")])
296
+ recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
297
+ runner.run_claude(
298
+ _PROMPT_ARGUMENTS, timeout_seconds=5, stdin_text="charter body"
299
+ )
300
+ assert recorder.all_keyword_arguments[0]["input"] == "charter body"
301
+
302
+
303
+ def test_run_claude_passes_none_input_when_stdin_text_omitted(
304
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
305
+ ) -> None:
306
+ config_file = _write_chain_config(tmp_path, [_entry("claude")])
307
+ recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
308
+ runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
309
+ assert recorder.all_keyword_arguments[0].get("input") is None
310
+
311
+
312
+ def test_cli_forwards_piped_stdin_to_invocation(
313
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
314
+ ) -> None:
315
+ config_file = _write_chain_config(tmp_path, [_entry("claude")])
316
+ recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
317
+
318
+ class _PipedStdin:
319
+ def isatty(self) -> bool:
320
+ return False
321
+
322
+ def read(self) -> str:
323
+ return "charter body"
324
+
325
+ monkeypatch.setattr(runner.sys, "stdin", _PipedStdin())
326
+ exit_code = runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
327
+ assert exit_code == 0
328
+ assert recorder.all_keyword_arguments[0]["input"] == "charter body"
329
+
330
+
279
331
  def test_signature_matching_is_case_insensitive(
280
332
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
281
333
  ) -> None:
@@ -362,6 +414,7 @@ def test_cli_missing_config_exits_config_error(
362
414
  ) -> None:
363
415
  missing_config = tmp_path / CONFIG_FILENAME
364
416
  monkeypatch.setattr(runner, "chain_config_path", lambda: missing_config)
417
+ _install_tty_stdin(monkeypatch)
365
418
  exit_code = runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
366
419
  captured = capsys.readouterr()
367
420
  assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
@@ -470,3 +523,62 @@ def test_chain_config_path_points_at_home_config() -> None:
470
523
  config_path = runner.chain_config_path()
471
524
  assert config_path.name == CONFIG_FILENAME
472
525
  assert CLAUDE_HOME_SUBDIRECTORY in config_path.parts
526
+
527
+
528
+ def test_real_subprocess_capture_replaces_undecodable_bytes(
529
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
530
+ ) -> None:
531
+ config_file = _write_chain_config(tmp_path, [_entry(sys.executable)])
532
+ monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
533
+ child_code = 'import sys; sys.stdout.buffer.write(b"ok \\x90 end")'
534
+ chain_result = runner.run_claude(["-c", child_code], timeout_seconds=60)
535
+ assert chain_result.served_command == sys.executable
536
+ assert chain_result.returncode == 0
537
+ assert chain_result.stdout == "ok � end"
538
+
539
+
540
+ def test_real_subprocess_capture_preserves_utf8_text(
541
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
542
+ ) -> None:
543
+ config_file = _write_chain_config(tmp_path, [_entry(sys.executable)])
544
+ monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
545
+ child_code = 'import sys; sys.stdout.buffer.write("report ✅ done".encode("utf-8"))'
546
+ chain_result = runner.run_claude(["-c", child_code], timeout_seconds=60)
547
+ assert chain_result.served_command == sys.executable
548
+ assert chain_result.returncode == 0
549
+ assert chain_result.stdout == "report ✅ done"
550
+
551
+
552
+ def test_cli_emits_utf8_when_console_encoding_is_legacy(tmp_path: Path) -> None:
553
+ tmp_home = tmp_path / "home"
554
+ claude_directory = tmp_home / CLAUDE_HOME_SUBDIRECTORY
555
+ claude_directory.mkdir(parents=True)
556
+ config_file = claude_directory / CONFIG_FILENAME
557
+ config_file.write_text(
558
+ json.dumps({CONFIG_CHAIN_KEY: [_entry(sys.executable)]}),
559
+ encoding=UTF8_ENCODING,
560
+ )
561
+ runner_script = _SCRIPTS_DIR / "claude_chain_runner.py"
562
+ child_code = 'import sys; sys.stdout.buffer.write("report ✅".encode("utf-8"))'
563
+ legacy_console_encoding = "cp1252"
564
+ child_environment = dict(os.environ)
565
+ child_environment["USERPROFILE"] = str(tmp_home)
566
+ child_environment["HOME"] = str(tmp_home)
567
+ child_environment["PYTHONIOENCODING"] = legacy_console_encoding
568
+ child_environment.pop("PYTHONUTF8", None)
569
+ runner_command = [
570
+ sys.executable,
571
+ str(runner_script),
572
+ CLI_ARGUMENTS_SEPARATOR,
573
+ "-c",
574
+ child_code,
575
+ ]
576
+ completed = subprocess.run(
577
+ runner_command,
578
+ capture_output=True,
579
+ env=child_environment,
580
+ timeout=60,
581
+ )
582
+ assert completed.returncode == 0
583
+ assert b"Traceback" not in completed.stderr
584
+ assert "report ✅" in completed.stdout.decode(UTF8_ENCODING)
@@ -418,7 +418,6 @@ def test_tightened_auth_phrases_still_classify_auth_failure(
418
418
  assert outcome.classification == CLASSIFICATION_AUTH_FAILURE, each_stderr
419
419
 
420
420
 
421
- def test_dual_match_prefers_auth_failure_over_usage_limit(
422
421
  def test_missing_binary_returns_dedicated_error(
423
422
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
424
423
  ) -> None:
@@ -31,7 +31,7 @@ These come from the official Claude Code documentation; do not re-derive them.
31
31
  | Files in `.claude/rules/` without `paths:` frontmatter load **every** session | "Rules without `paths` frontmatter are loaded unconditionally and apply to all files" |
32
32
  | Path-scoped rules load **lazily** when matching files are accessed | "Rules can be scoped to specific files using YAML frontmatter with the `paths` field. These conditional rules only apply when Claude is working with files matching the specified patterns" |
33
33
  | Skills preload **metadata only** | "At startup, only the metadata (name and description) from all Skills is pre-loaded. Claude reads SKILL.md only when the Skill becomes relevant, and reads additional files only as needed" |
34
- | `@`-imports inside fenced/inline code blocks do not trigger imports | Empirical (verified in this skill's source session) referenced files alongside backtick-wrapped `@` paths do not appear in session-start context |
34
+ | `@`-imports inside fenced/inline code blocks do not trigger imports | Empirical (`InstructionsLoaded` hook): a bare `@`-import fires an `include` load event; the same path wrapped in backticks fires none, so the referenced file stays out of session-start context |
35
35
 
36
36
  ## Audit workflow
37
37
 
@@ -13,7 +13,13 @@ Search files instantly on Windows using the Everything command-line interface (e
13
13
 
14
14
  ## Hard limits
15
15
 
16
- Scope and fallback policy: `packages/claude-dev-env/rules/es-exe-file-search.md`.
16
+ Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds — narrow the search to what you need.
17
+
18
+ When `es.exe` fails or returns nothing, self-heal first: fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents), and report the outage so the reader knows the index was unavailable. When self-healing also fails, ask the user through `AskUserQuestion` with a short analysis and next-step options.
19
+
20
+ ## Registry tokens
21
+
22
+ The `es_exe_path_rewriter` hook resolves scope tokens before the command runs. A `{project-name}` placeholder or a bare registry key from `~/.claude/project-paths.json` becomes its quoted absolute path in the command. The hook allows and rewrites — it never blocks — so a search scoped to a registered project names the project token and lets the hook fill in the path.
17
23
 
18
24
  ## Instructions
19
25