claude-dev-env 8.69.0 → 8.70.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 (99) hide show
  1. package/.agents/skills/correction/SKILL.md +28 -6
  2. package/.agents/skills/correction/references/filing.md +33 -0
  3. package/.agents/skills/correction/scripts/correction_filing.py +327 -0
  4. package/.agents/skills/correction/scripts/correction_filing_constants/__init__.py +0 -0
  5. package/.agents/skills/correction/scripts/correction_filing_constants/config/__init__.py +0 -0
  6. package/.agents/skills/correction/scripts/correction_filing_constants/config/constants.py +56 -0
  7. package/.agents/skills/correction/scripts/pytest.ini +4 -0
  8. package/.agents/skills/correction/scripts/test_correction_filing.py +155 -0
  9. package/.agents/skills/pr-lifecycle/SKILL.md +3 -1
  10. package/.agents/skills/second-account-workers/SKILL.md +21 -3
  11. package/docs/rule-guides/features-start-with-an-eval.md +40 -0
  12. package/hooks/blocking/followup_pr_dedupe.py +55 -5
  13. package/hooks/blocking/image_read_size_gate.py +167 -0
  14. package/hooks/blocking/pr_lifecycle_skill_gate.py +62 -11
  15. package/hooks/blocking/pull_request_proof.py +86 -20
  16. package/hooks/blocking/test_artifact_look_gate.py +1 -0
  17. package/hooks/blocking/test_followup_pr_dedupe.py +33 -0
  18. package/hooks/blocking/test_image_read_size_gate.py +79 -0
  19. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +117 -3
  20. package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -5
  21. package/hooks/blocking/test_pull_request_proof.py +124 -1
  22. package/hooks/features/README.md +1 -0
  23. package/hooks/features/conduct-gates.md +1 -1
  24. package/hooks/features/read-gates.md +20 -0
  25. package/hooks/features/write-edit-blocking.md +3 -5
  26. package/hooks/hooks.json +11 -1
  27. package/hooks/hooks_constants/artifact_look_gate_constants.py +1 -2
  28. package/hooks/hooks_constants/followup_pr_dedupe_constants.py +4 -0
  29. package/hooks/hooks_constants/image_read_size_gate_constants.py +41 -0
  30. package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +3 -0
  31. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -5
  32. package/hooks/hooks_constants/pull_request_proof_constants.py +20 -1
  33. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +4 -11
  34. package/hooks/hooks_constants/transcript_skill_scan_constants.py +1 -0
  35. package/hooks/test_transcript_skill_scan.py +75 -1
  36. package/hooks/transcript_skill_scan.py +87 -15
  37. package/package.json +1 -1
  38. package/rules/anti-corollary-tests.md +2 -2
  39. package/rules/archiving-agent-config.md +2 -2
  40. package/rules/asd-ste100-language.md +2 -2
  41. package/rules/bdd.md +1 -1
  42. package/rules/build-needs-a-user.md +1 -1
  43. package/rules/cleanup-temp-files.md +3 -3
  44. package/rules/code-standards.md +1 -1
  45. package/rules/correction-lens.md +3 -3
  46. package/rules/destructive-commands.md +3 -3
  47. package/rules/doc-inventory-integrity.md +1 -1
  48. package/rules/docstring-prose-matches-implementation.md +1 -1
  49. package/rules/explore-thoroughly.md +1 -1
  50. package/rules/failure-blast-radius.md +1 -1
  51. package/rules/falsify-before-green.md +2 -2
  52. package/rules/features-start-with-an-eval.md +9 -0
  53. package/rules/filesystem-search.md +3 -3
  54. package/rules/flag-non-breaking-findings.md +2 -2
  55. package/rules/long-horizon-autonomy.md +2 -2
  56. package/rules/memory-stores-durable-facts.md +2 -2
  57. package/rules/no-contrast-framing.md +1 -1
  58. package/rules/no-cross-skill-duplicate-helpers.md +1 -1
  59. package/rules/orphan-css-class.md +1 -1
  60. package/rules/paired-test-coverage.md +1 -1
  61. package/rules/plain-illustrative-docstrings.md +1 -1
  62. package/rules/prefer-existing-tools.md +2 -2
  63. package/rules/proof-before-pull-request.md +1 -1
  64. package/rules/request-scope.md +1 -1
  65. package/rules/research-mode.md +2 -2
  66. package/rules/retired-hook-prose.md +1 -1
  67. package/rules/session-title.md +1 -1
  68. package/rules/shell-invocation.md +2 -2
  69. package/rules/testing.md +1 -1
  70. package/rules/verify-before-asking.md +3 -3
  71. package/rules/verify-runtime-state.md +2 -2
  72. package/rules/windows-filesystem-safe.md +1 -1
  73. package/rules/workers-done-before-complete.md +2 -2
  74. package/rules/workflow-substitution-slots.md +1 -1
  75. package/scripts/account_broker.py +10 -3
  76. package/scripts/account_broker_support.py +7 -3
  77. package/scripts/agent_image_copy.py +155 -0
  78. package/scripts/claude_account_worker.py +24 -5
  79. package/scripts/claude_account_worker_process.py +5 -0
  80. package/scripts/claude_account_worker_report.py +16 -1
  81. package/scripts/dev_env_scripts_constants/agent_image_copy_constants.py +11 -0
  82. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +12 -0
  83. package/scripts/policy_lint/config/constants.py +1 -0
  84. package/scripts/test_account_broker.py +38 -0
  85. package/scripts/test_account_broker_support.py +24 -0
  86. package/scripts/test_agent_image_copy.py +76 -0
  87. package/scripts/test_claude_account_worker.py +47 -0
  88. package/scripts/test_claude_account_worker_process.py +20 -0
  89. package/scripts/test_claude_account_worker_report.py +15 -0
  90. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +4 -4
  91. package/scripts/tests/test_rule_load_scopes.py +1 -0
  92. package/hooks/advisory/conftest.py +0 -10
  93. package/hooks/advisory/refactor_guard.py +0 -385
  94. package/hooks/advisory/refactor_guard_test_support.py +0 -46
  95. package/hooks/advisory/test_refactor_guard.py +0 -148
  96. package/hooks/advisory/test_refactor_guard_advisory.py +0 -242
  97. package/hooks/advisory/test_refactor_guard_eligibility.py +0 -166
  98. package/hooks/hooks_constants/refactor_guard_constants.py +0 -77
  99. package/hooks/hooks_constants/test_refactor_guard_constants.py +0 -21
@@ -1,18 +1,17 @@
1
1
  # Write and Edit blocking
2
2
 
3
- This family decides whether an agent's file mutation can run. It also warns about broad refactors and unsafe migration edits before the agent changes a file.
3
+ This family decides whether an agent's file mutation can run. It also warns about unsafe migration edits before the agent changes a file.
4
4
 
5
5
  ## Checks
6
6
 
7
7
  - `blocking/pre_tool_use_dispatcher.py` combines hosted advisory output with its native description check and returns one permission decision.
8
- - `advisory/refactor_guard.py` warns when an Edit or MultiEdit appears to rename code beyond the current change.
9
8
  - `advisory/migration_safety_advisor.py` warns when an Edit or MultiEdit adds an unsafe Django migration operation.
10
9
  - `blocking/state_description_blocker.py` rejects historical or comparative prose in supported comments, docstrings, and Markdown.
11
10
 
12
11
  ## When it fires
13
12
 
14
13
  - `blocking/pre_tool_use_dispatcher.py` runs on `PreToolUse`, matcher `Write|Edit|MultiEdit|apply_patch`, timeout `60` seconds in `hooks.json`.
15
- - `advisory/refactor_guard.py` and `advisory/migration_safety_advisor.py` run inside that dispatcher on `PreToolUse`, matcher `Write|Edit|MultiEdit|apply_patch`, timeout `60` seconds. `ALL_HOSTED_HOOK_ENTRIES` selects `Edit` and `MultiEdit` for each.
14
+ - `advisory/migration_safety_advisor.py` runs inside that dispatcher on `PreToolUse`, matcher `Write|Edit|MultiEdit|apply_patch`, timeout `60` seconds. `ALL_HOSTED_HOOK_ENTRIES` selects `Edit` and `MultiEdit` for it.
16
15
  - `blocking/state_description_blocker.py` runs through the dispatcher's native evaluator on `PreToolUse`, matcher `Write|Edit|MultiEdit|apply_patch`, timeout `60` seconds. Its evaluator checks `Write`, `Edit`, and `MultiEdit` payloads and skips `apply_patch`.
17
16
 
18
17
  ## Proving it
@@ -22,13 +21,12 @@ Preconditions:
22
21
  - Run each command from the repository root with Python and pytest available. The tests use disposable paths where they need files.
23
22
 
24
23
  - **Dispatcher decision.** Input is a clean Write payload. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_pre_tool_use_dispatcher.py -q`. The adjacent test checks that the dispatcher allows clean writes and carries denials from its checks.
25
- - **Refactor warning.** Input is a MultiEdit whose second edit renames a function. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_refactor_guard.py -q`. The adjacent test reports both function names in the warning.
26
24
  - **Migration warning.** Input is an Edit containing `migrations.RemoveField`. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_migration_safety_advisor.py -q`. The adjacent test observes an allow decision with a migration warning.
27
25
  - **Description gate.** Input is a Write containing historical prose in a Python comment. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_state_description_blocker.py -q`. The adjacent test observes a denial reason.
28
26
 
29
27
  ## Gotchas
30
28
 
31
- - `ALL_HOSTED_HOOK_ENTRIES` lists only the two nonblocking advisors. The dispatcher calls the description evaluator through its native hook table.
29
+ - `ALL_HOSTED_HOOK_ENTRIES` lists only the nonblocking migration advisor. The dispatcher calls the description evaluator through its native hook table.
32
30
  - The dispatcher runs hosted entries in roster order. A hosted advisory crash stays silent; a native blocking check can deny the write.
33
31
  - `scripts/policy_lint/adapter_detectors.py` also loads `blocking/state_description_blocker.py`. Check that adapter when changing its evaluator.
34
32
  - `bin/install.mjs` names folded and retired script paths so reinstall can remove stale registrations.
package/hooks/hooks.json CHANGED
@@ -88,7 +88,7 @@
88
88
  ]
89
89
  },
90
90
  {
91
- "matcher": "Bash|PowerShell|mcp__.*__(create_pull_request|merge_pull_request|enable_pr_auto_merge|update_pull_request)",
91
+ "matcher": "Bash|PowerShell|mcp__.*__(create_pull_request|merge_pull_request|enable_pr_auto_merge|update_pull_request|actions_run_trigger)",
92
92
  "hooks": [
93
93
  {
94
94
  "type": "command",
@@ -97,6 +97,16 @@
97
97
  }
98
98
  ]
99
99
  },
100
+ {
101
+ "matcher": "Read",
102
+ "hooks": [
103
+ {
104
+ "type": "command",
105
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/image_read_size_gate.py",
106
+ "timeout": 10
107
+ }
108
+ ]
109
+ },
100
110
  {
101
111
  "matcher": "EnterWorktree",
102
112
  "hooks": [
@@ -28,6 +28,5 @@ UNSEEN_PAGE_REASON_TEMPLATE = (
28
28
  "headless browser, for example `chromium --headless --screenshot=page.png "
29
29
  "--window-size=800,900 file://{page_path}`, then open page.png with the "
30
30
  "Read tool and check it against the ask. Fix what the picture shows, take "
31
- "a new screenshot, and publish again. Attach that screenshot when you "
32
- "report the page, so the person who asked sees the same proof."
31
+ "a new screenshot, and publish again."
33
32
  )
@@ -17,3 +17,7 @@ DUPLICATE_FOLLOWUP_REASON_TEMPLATE = (
17
17
  " ({open_url}). Add this finding to the open follow-up pull request #{open_number}."
18
18
  " One combined follow-up pull request per parent pull request."
19
19
  )
20
+ ALL_TITLE_OPTIONS = frozenset({"-t", "--title"})
21
+ WORKFLOW_DISPATCH_TOOL_SUFFIX = "__actions_run_trigger"
22
+ RUN_WORKFLOW_METHOD = "run_workflow"
23
+ ALL_PULL_REQUEST_DISPATCH_INPUT_KEYS = ("head", "title")
@@ -0,0 +1,41 @@
1
+ """Names, limits and refusal text for the image_read_size_gate PreToolUse hook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ READ_TOOL_NAME = "Read"
8
+ TOOL_NAME_KEY = "tool_name"
9
+ TOOL_INPUT_KEY = "tool_input"
10
+ FILE_PATH_INPUT_KEY = "file_path"
11
+ PRE_TOOL_USE_EVENT_NAME = "PreToolUse"
12
+ DENY_DECISION = "deny"
13
+ PERMISSION_DECISION_REASON_KEY = "permissionDecisionReason"
14
+ MAXIMUM_LONG_EDGE_PIXELS = 512
15
+ HEADER_BYTE_COUNT = 64 * 1024
16
+ PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
17
+ PNG_SIZE_START = 16
18
+ PNG_SIZE_END = 24
19
+ GIF_SIZE_START = 6
20
+ GIF_SIZE_END = 10
21
+ ALL_GIF_SIGNATURES = (b"GIF87a", b"GIF89a")
22
+ JPEG_SIGNATURE = b"\xff\xd8"
23
+ JPEG_MARKER_PREFIX = 0xFF
24
+ JPEG_MARKER_LENGTH = 2
25
+ JPEG_FRAME_SIZE_START = 5
26
+ JPEG_FRAME_SIZE_END = 9
27
+ ALL_JPEG_FRAME_MARKERS = frozenset(
28
+ {0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF}
29
+ )
30
+ ALL_JPEG_STANDALONE_MARKERS = frozenset(
31
+ {0x01, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8}
32
+ )
33
+ AGENT_COPY_SCRIPT_PATH = Path(__file__).resolve().parents[2] / "scripts" / "agent_image_copy.py"
34
+ OVERSIZED_IMAGE_REASON_TEMPLATE = (
35
+ "This image is {width}x{height} pixels. Agents read images at {maximum_edge} pixels or less"
36
+ " on the long edge, because a full-size image fills the context: a 1440x2560 screenshot costs"
37
+ " about 4,784 tokens, and a 288x512 copy costs about 209. Write a capped copy and read that"
38
+ ' copy: `python3 "{script_path}" "{image_path}"` prints the path of the copy, which keeps'
39
+ " the aspect ratio. Crop the region you need with `--crop LEFT,TOP,RIGHT,BOTTOM` when a"
40
+ " detail is too small at that size. Send the full-size file to a person, never to an agent."
41
+ )
@@ -39,3 +39,6 @@ ALL_PYTHON_EXECUTABLES = frozenset({"python", "python3", "python.exe", "python3.
39
39
  PULL_REQUEST_SCRIPT_NAME = "pull_request.py"
40
40
  PATH_SEPARATOR_PATTERN = r"[\\/]"
41
41
  PYTHON_OPTIONS_WITH_VALUE = frozenset({"-X", "-W"})
42
+ ALL_BUILD_EVAL_SKILL_NAMES = ("claude-api",)
43
+ BUILD_EVAL_ARGUMENT_WORD = "build-eval"
44
+ BUILD_EVAL_COMMAND_MARKER = "<command-name>/claude-api</command-name>"
@@ -83,11 +83,6 @@ class HostedHookEntry:
83
83
 
84
84
 
85
85
  ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
86
- HostedHookEntry(
87
- script_relative_path="advisory/refactor_guard.py",
88
- applicable_tool_names=frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}),
89
- is_blocking=False,
90
- ),
91
86
  HostedHookEntry(
92
87
  script_relative_path="advisory/migration_safety_advisor.py",
93
88
  applicable_tool_names=frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}),
@@ -8,7 +8,6 @@ ALL_GH_CREATE_WORDS = ["pr", "create"]
8
8
  PULL_REQUEST_SCRIPT_NAME = "pull_request.py"
9
9
  PULL_REQUEST_SCRIPT_CREATE_WORD = "create"
10
10
  ALL_PYTHON_PROGRAM_NAMES = frozenset({"python", "python3", "py"})
11
- CREATE_PULL_REQUEST_TOOL_SUFFIX = "__create_pull_request"
12
11
  PROOF_GUIDE_PATH = "~/.claude/docs/rule-guides/proof-before-pull-request.md"
13
12
  MISSING_BODY_REASON = (
14
13
  "Pass the pull request body with --body or --body-file so this gate can read its"
@@ -64,3 +63,23 @@ UNREADABLE_CHANGES_REASON = (
64
63
  " from a checkout of the head branch with origin fetched, or put a screenshot"
65
64
  " link in the 'Proof in practice' section. Read " + PROOF_GUIDE_PATH + " for the full rule."
66
65
  )
66
+ FEATURE_TITLE_PATTERN = r"^\s*feat(\([^)]*\))?!?:"
67
+ EVAL_HEADING_PATTERN = r"^(#{1,6})[ \t]+evals?[ \t]*#*[ \t]*$"
68
+ EVAL_GUIDE_PATH = "~/.claude/docs/rule-guides/features-start-with-an-eval.md"
69
+ MISSING_EVAL_SECTION_REASON = (
70
+ "This feature pull request has no 'Eval' section naming its eval. A new feature is"
71
+ " built against an eval. Invoke the Skill tool with skill 'claude-api' and args"
72
+ " 'build-eval', build or reuse the eval it leads you to, and run it. Then add an"
73
+ " 'Eval' heading to the body that names the eval's cases and grader, gives the"
74
+ " command that runs it in backticks, and quotes its result. Read "
75
+ + EVAL_GUIDE_PATH
76
+ + " for the full rule."
77
+ )
78
+ MISSING_BUILD_EVAL_REASON = (
79
+ "This feature pull request comes from a session that never invoked /claude-api"
80
+ " build-eval. A new feature starts with that command. Invoke the Skill tool with"
81
+ " skill 'claude-api' and args 'build-eval', check the eval named in the 'Eval'"
82
+ " section with it, then run the same command again. Read "
83
+ + EVAL_GUIDE_PATH
84
+ + " for the full rule."
85
+ )
@@ -37,21 +37,14 @@ def test_roster_keeps_only_nonblocking_edit_advisors() -> None:
37
37
  all_script_paths = tuple(
38
38
  each_entry.script_relative_path for each_entry in ALL_HOSTED_HOOK_ENTRIES
39
39
  )
40
- assert all_script_paths == (
41
- "advisory/refactor_guard.py",
42
- "advisory/migration_safety_advisor.py",
43
- )
40
+ assert all_script_paths == ("advisory/migration_safety_advisor.py",)
44
41
  assert all(not each_entry.is_blocking for each_entry in ALL_HOSTED_HOOK_ENTRIES)
45
42
 
46
43
 
47
44
  def test_advisors_apply_to_edit_and_multi_edit() -> None:
48
- for each_script_path in (
49
- "advisory/refactor_guard.py",
50
- "advisory/migration_safety_advisor.py",
51
- ):
52
- entry = _entry_for(each_script_path)
53
- assert entry is not None
54
- assert entry.applicable_tool_names == frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME})
45
+ entry = _entry_for("advisory/migration_safety_advisor.py")
46
+ assert entry is not None
47
+ assert entry.applicable_tool_names == frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME})
55
48
 
56
49
 
57
50
  def test_every_hosted_script_path_exists_under_the_hooks_root() -> None:
@@ -5,3 +5,4 @@ COMPACT_BOUNDARY_SUBTYPE = "compact_boundary"
5
5
  SKILL_TOOL_NAME = "Skill"
6
6
  TOOL_USE_BLOCK_TYPE = "tool_use"
7
7
  USER_ENTRY_TYPE = "user"
8
+ COMMAND_ARGUMENTS_PATTERN = r"<command-args>(.*?)</command-args>"
@@ -8,7 +8,11 @@ HOOKS_DIRECTORY = Path(__file__).resolve().parent
8
8
  if str(HOOKS_DIRECTORY) not in sys.path:
9
9
  sys.path.insert(0, str(HOOKS_DIRECTORY))
10
10
 
11
- from transcript_skill_scan import is_skill_loaded_after_last_compaction, skill_invocation_status
11
+ from transcript_skill_scan import (
12
+ invokes_skill_with_argument,
13
+ is_skill_loaded_after_last_compaction,
14
+ skill_invocation_status,
15
+ )
12
16
 
13
17
 
14
18
  def _skill_entry(name: str) -> str:
@@ -56,3 +60,73 @@ def test_invocation_status_separates_ever_invoked_from_loaded_now() -> None:
56
60
  assert skill_invocation_status([_skill_entry("pr-lifecycle")], ("pr-lifecycle",), ()) == (True, True)
57
61
  assert skill_invocation_status([_skill_entry("pr-lifecycle"), compaction], ("pr-lifecycle",), ()) == (True, False)
58
62
  assert skill_invocation_status([compaction, _skill_entry("other")], ("pr-lifecycle",), ()) == (False, False)
63
+
64
+
65
+ BUILD_EVAL_MARKER = "<command-name>/claude-api</command-name>"
66
+
67
+
68
+ def _skill_entry_with_arguments(name: str, arguments: object) -> str:
69
+ return json.dumps(
70
+ {
71
+ "type": "assistant",
72
+ "message": {
73
+ "content": [{"type": "tool_use", "name": "Skill", "input": {"skill": name, "args": arguments}}]
74
+ },
75
+ }
76
+ )
77
+
78
+
79
+ def _invokes_build_eval(lines: list[str]) -> bool:
80
+ return invokes_skill_with_argument(lines, ("claude-api",), "build-eval", BUILD_EVAL_MARKER)
81
+
82
+
83
+ def test_claude_api_skill_with_build_eval_argument_should_count() -> None:
84
+ assert _invokes_build_eval([_skill_entry_with_arguments("claude-api", "build-eval")])
85
+
86
+
87
+ def test_build_eval_argument_followed_by_more_words_should_count() -> None:
88
+ assert _invokes_build_eval([_skill_entry_with_arguments("claude-api", "build-eval extra words")])
89
+
90
+
91
+ def test_plugin_prefixed_claude_api_with_build_eval_should_count() -> None:
92
+ assert _invokes_build_eval([_skill_entry_with_arguments("plugin:claude-api", "build-eval")])
93
+
94
+
95
+ def test_claude_api_with_another_argument_should_not_count() -> None:
96
+ assert not _invokes_build_eval([_skill_entry_with_arguments("claude-api", "migrate")])
97
+ assert not _invokes_build_eval([_skill_entry_with_arguments("claude-api", "migrate build-eval")])
98
+ assert not _invokes_build_eval([_skill_entry_with_arguments("claude-api", None)])
99
+ assert not _invokes_build_eval([_skill_entry("claude-api")])
100
+
101
+
102
+ def test_build_eval_skill_alone_should_not_count() -> None:
103
+ assert not _invokes_build_eval([_skill_entry("build-eval")])
104
+ assert not _invokes_build_eval([_skill_entry_with_arguments("build-eval", "build-eval")])
105
+
106
+
107
+ def test_user_claude_api_build_eval_command_should_count() -> None:
108
+ command_text = BUILD_EVAL_MARKER + "<command-args>build-eval for the gate</command-args>"
109
+ assert _invokes_build_eval([json.dumps({"type": "user", "message": {"content": command_text}})])
110
+
111
+
112
+ def test_user_claude_api_command_with_another_argument_should_not_count() -> None:
113
+ command_text = BUILD_EVAL_MARKER + "<command-args>migrate</command-args>"
114
+ assert not _invokes_build_eval([json.dumps({"type": "user", "message": {"content": command_text}})])
115
+ assert not _invokes_build_eval([json.dumps({"type": "user", "message": {"content": BUILD_EVAL_MARKER}})])
116
+
117
+
118
+ def test_quoted_build_eval_command_in_a_tool_result_should_not_count() -> None:
119
+ command_text = BUILD_EVAL_MARKER + "<command-args>build-eval</command-args>"
120
+ quoted_result = json.dumps(
121
+ {"type": "user", "message": {"content": [{"type": "tool_result", "content": command_text}]}}
122
+ )
123
+ assert not _invokes_build_eval([quoted_result])
124
+
125
+
126
+ def test_build_eval_invocation_before_a_compact_boundary_should_still_count() -> None:
127
+ lines = [
128
+ _skill_entry_with_arguments("claude-api", "build-eval"),
129
+ json.dumps({"type": "system", "subtype": "compact_boundary"}),
130
+ _skill_entry("other"),
131
+ ]
132
+ assert _invokes_build_eval(lines)
@@ -1,12 +1,14 @@
1
- """Find a skill invocation after the last transcript compaction."""
1
+ """Find a skill invocation after the last transcript compaction, or one with a given first argument anywhere."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from collections.abc import Collection, Iterable
6
+ import re
7
+ from collections.abc import Collection, Iterable, Iterator
7
8
 
8
9
  from hooks_constants.transcript_skill_scan_constants import (
9
10
  ASSISTANT_ENTRY_TYPE,
11
+ COMMAND_ARGUMENTS_PATTERN,
10
12
  COMPACT_BOUNDARY_SUBTYPE,
11
13
  SKILL_TOOL_NAME,
12
14
  TOOL_USE_BLOCK_TYPE,
@@ -14,19 +16,23 @@ from hooks_constants.transcript_skill_scan_constants import (
14
16
  )
15
17
 
16
18
 
17
- def _invokes_skill(
18
- all_entry_fields: dict[str, object],
19
- skill_names: Collection[str],
20
- slash_command_markers: Collection[str],
21
- ) -> bool:
19
+ def _user_command_text(all_entry_fields: dict[str, object]) -> str | None:
22
20
  message = all_entry_fields.get("message")
23
- if not isinstance(message, dict):
24
- return False
21
+ if all_entry_fields.get("type") != USER_ENTRY_TYPE or not isinstance(message, dict):
22
+ return None
25
23
  content = message.get("content")
26
- if all_entry_fields.get("type") == USER_ENTRY_TYPE and isinstance(content, str):
27
- return any(marker in content for marker in slash_command_markers)
28
- if all_entry_fields.get("type") != ASSISTANT_ENTRY_TYPE or not isinstance(content, list):
29
- return False
24
+ return content if isinstance(content, str) else None
25
+
26
+
27
+ def _named_skill_inputs(
28
+ all_entry_fields: dict[str, object], skill_names: Collection[str]
29
+ ) -> Iterator[dict[str, object]]:
30
+ message = all_entry_fields.get("message")
31
+ if all_entry_fields.get("type") != ASSISTANT_ENTRY_TYPE or not isinstance(message, dict):
32
+ return
33
+ content = message.get("content")
34
+ if not isinstance(content, list):
35
+ return
30
36
  for each_block in content:
31
37
  if not isinstance(each_block, dict) or each_block.get("type") != TOOL_USE_BLOCK_TYPE:
32
38
  continue
@@ -36,8 +42,42 @@ def _invokes_skill(
36
42
  if isinstance(invoked_name, str) and any(
37
43
  invoked_name == name or invoked_name.endswith(":" + name) for name in skill_names
38
44
  ):
39
- return True
40
- return False
45
+ yield each_block["input"]
46
+
47
+
48
+ def _invokes_skill(
49
+ all_entry_fields: dict[str, object],
50
+ skill_names: Collection[str],
51
+ slash_command_markers: Collection[str],
52
+ ) -> bool:
53
+ command_text = _user_command_text(all_entry_fields)
54
+ if command_text is not None:
55
+ return any(marker in command_text for marker in slash_command_markers)
56
+ return next(_named_skill_inputs(all_entry_fields, skill_names), None) is not None
57
+
58
+
59
+ def _first_word_is(text: object, argument_word: str) -> bool:
60
+ return isinstance(text, str) and text.split()[:1] == [argument_word]
61
+
62
+
63
+ def _invokes_skill_with_argument(
64
+ all_entry_fields: dict[str, object],
65
+ skill_names: Collection[str],
66
+ argument_word: str,
67
+ command_marker: str,
68
+ ) -> bool:
69
+ command_text = _user_command_text(all_entry_fields)
70
+ if command_text is not None:
71
+ command_arguments = re.search(COMMAND_ARGUMENTS_PATTERN, command_text, re.DOTALL)
72
+ return (
73
+ command_marker in command_text
74
+ and command_arguments is not None
75
+ and _first_word_is(command_arguments.group(1), argument_word)
76
+ )
77
+ return any(
78
+ _first_word_is(each_input.get("args"), argument_word)
79
+ for each_input in _named_skill_inputs(all_entry_fields, skill_names)
80
+ )
41
81
 
42
82
 
43
83
  def _relevant_entry(each_line: str, skill_names: Collection[str]) -> dict[str, object] | None:
@@ -91,3 +131,35 @@ def is_skill_loaded_after_last_compaction(
91
131
  slash_command_markers: Accepted user command tags.
92
132
  """
93
133
  return skill_invocation_status(all_transcript_lines, skill_names, slash_command_markers)[1]
134
+
135
+
136
+ def invokes_skill_with_argument(
137
+ all_transcript_lines: Iterable[str],
138
+ skill_names: Collection[str],
139
+ argument_word: str,
140
+ command_marker: str,
141
+ ) -> bool:
142
+ """Return whether any entry invoked the skill with ``argument_word`` as its first argument.
143
+
144
+ ::
145
+
146
+ Skill claude-api, args "build-eval extra" -> True
147
+ Skill plugin:claude-api, args "build-eval" -> True
148
+ Skill claude-api, args "migrate" -> False
149
+ user "/claude-api" command, command-args "build-eval" -> True
150
+
151
+ An invocation stays counted after a later compact boundary.
152
+
153
+ Args:
154
+ all_transcript_lines: JSON transcript entries, one per line.
155
+ skill_names: Accepted skill names, with plugin prefixes accepted.
156
+ argument_word: The first whitespace-separated word the arguments must carry.
157
+ command_marker: The user command tag that names the skill.
158
+ """
159
+ for each_line in all_transcript_lines:
160
+ if not any(name in each_line for name in skill_names):
161
+ continue
162
+ all_entry_fields = _relevant_entry(each_line, skill_names)
163
+ if all_entry_fields and _invokes_skill_with_argument(all_entry_fields, skill_names, argument_word, command_marker):
164
+ return True
165
+ return False
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.69.0",
3
+ "version": "8.70.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,6 @@ paths:
14
14
 
15
15
  Before keeping a test, name one code change it would catch; replace a case that catches none. Skip spelling cross products after proving canonicalization once; compare a few discriminating cases. Exercise the production path with a non-default expected result so a dead implementation cannot pass. Test policy compliance on governed surfaces; in an audit, name a mutation and record how many tests it fails.
16
16
 
17
- **Enforcement:** none, the agent applies it.
17
+ **Enforcement:** none.
18
18
 
19
- **Full text:** [`docs/rule-guides/anti-corollary-tests.md`](../docs/rule-guides/anti-corollary-tests.md). Read it when choosing cases or auditing mutation evidence.
19
+ **Full text:** [guide](../docs/rule-guides/anti-corollary-tests.md). Read it when choosing cases or auditing mutation evidence.
@@ -15,6 +15,6 @@ paths:
15
15
 
16
16
  Trace checks and callers; `CHANGELOG.md`, `README.md`, shipped-skills registry, and host paths prove neither use nor disuse. Read `rules-archived/ARCHIVE-MANIFEST.md`; skip and report exemptions, then continue. Move unused files with `git mv` into sibling archives; record the reason, restore command, and companion edits. Retain skill names in `bin/ever-shipped-skills.mjs` and check tests lost when a skill directory moves.
17
17
 
18
- **Enforcement:** none, the agent applies it.
18
+ **Enforcement:** none.
19
19
 
20
- **Full text:** [`docs/rule-guides/archiving-agent-config.md`](../docs/rule-guides/archiving-agent-config.md). Read it before moving or restoring.
20
+ **Full text:** [guide](../docs/rule-guides/archiving-agent-config.md). Read it before moving or restoring.
@@ -4,6 +4,6 @@
4
4
 
5
5
  Use the sole general language rule. Write short, complete sentences on one topic. Use active voice; lead with conditions; give one action per step. Use plain, precise words and stable terms. Expand abbreviations and contractions first; name unclear pronouns. Be inclusive; punctuate clearly. Preserve exact labels, identifiers, formulas, titles, and interface text. Send a result, blocker, or question. Mark injury or death `WARNING`, equipment damage `CAUTION`; state condition then result. Aim for 20 words per step, 25 per description. Treat checks as aids; have a human verify accuracy, terms, safety, confidentiality, and meaning.
6
6
 
7
- **Enforcement:** none, the agent applies it.
7
+ **Enforcement:** none.
8
8
 
9
- **Full text:** [`docs/rule-guides/asd-ste100-language.md`](../docs/rule-guides/asd-ste100-language.md). Read it for sources.
9
+ **Full text:** [guide](../docs/rule-guides/asd-ste100-language.md). Read it for sources.
package/rules/bdd.md CHANGED
@@ -13,4 +13,4 @@ paths:
13
13
 
14
14
  For every non-trivial feature, discover what you do not know, illustrate it with concrete examples in chat, formulate each behavior as a narrow "should ..." specification, then build each one through the TDD loop (CODE_RULES §8). Name developer-facing specs and tests as should sentences.
15
15
 
16
- **Full text:** [`docs/rule-guides/bdd.md`](../docs/rule-guides/bdd.md), with the long-form BDD references.
16
+ **Full text:** [guide](../docs/rule-guides/bdd.md), with the long-form BDD references.
@@ -17,4 +17,4 @@ Name its caller in this change, first-use date or event, and last manual use wit
17
17
 
18
18
  **Enforcement:** `uncalled-new-file` checks new code under `scripts/`, `hooks/`, `bin/`, `ci/`, and `tools/` for a caller.
19
19
 
20
- **Full text:** [`docs/rule-guides/build-needs-a-user.md`](../docs/rule-guides/build-needs-a-user.md). Read it when sizing a new check.
20
+ **Full text:** [guide](../docs/rule-guides/build-needs-a-user.md). Read it when sizing a new check.
@@ -2,8 +2,8 @@
2
2
 
3
3
  **When:** Creating scratch files, debug dumps, or one-off helpers during a task.
4
4
 
5
- Prefer memory to scratch files and track any temporary files you create. At completion, remove those files and leave user-requested files in place. Files under the OS temporary root or `$CLAUDE_JOB_DIR` need no explicit removal; a parent handles child-agent scratch. Use the permitted removal form.
5
+ Prefer memory to scratch files and track any temporary files you create. At completion, remove those files and leave user-requested files in place. Files under the OS temporary root or `$CLAUDE_JOB_DIR` need no explicit removal; a parent handles child-agent scratch.
6
6
 
7
- **Enforcement:** none, the agent applies it.
7
+ **Enforcement:** none.
8
8
 
9
- **Full text:** [`docs/rule-guides/cleanup-temp-files.md`](../docs/rule-guides/cleanup-temp-files.md). Read it to classify a file or choose cleanup.
9
+ **Full text:** [guide](../docs/rule-guides/cleanup-temp-files.md). Read it to classify a file or choose cleanup.
@@ -16,4 +16,4 @@ paths:
16
16
 
17
17
  **Enforcement:** the staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file, and CI runs it against the merge base.
18
18
 
19
- **Full text:** [`docs/rule-guides/code-standards.md`](../docs/rule-guides/code-standards.md), with the policy surface map.
19
+ **Full text:** [guide](../docs/rule-guides/code-standards.md), with the policy surface map.
@@ -5,7 +5,7 @@
5
5
  Open a control in the guarded repository, in the same run, at the highest capable layer. State the layer, why higher layers fail, and the change opened. Pair layers when both help. Move repeated corrections up a layer. Memory records the decision.
6
6
 
7
7
  | Priority | Layer | Control |
8
- |---|---|---|
8
+ |-|-|-|
9
9
  | 1 | Codebase | Prevent the mistake. |
10
10
  | 2 | Static analysis | Programmatic check. |
11
11
  | 3 | Review tooling | Reviewer criterion. |
@@ -16,6 +16,6 @@ Open a control in the guarded repository, in the same run, at the highest capabl
16
16
 
17
17
  Keep this file in `rules/`. A prune, archive, consolidation, or rewrite that would move it stops here. Edits that sharpen it are welcome.
18
18
 
19
- **Enforcement:** none, the agent applies it.
19
+ **Enforcement:** none.
20
20
 
21
- **Full text:** [`docs/rule-guides/correction-lens.md`](../docs/rule-guides/correction-lens.md). Read it when choosing a layer or locating a control.
21
+ **Full text:** [guide](../docs/rule-guides/correction-lens.md). Read it when choosing a layer or locating a control.
@@ -2,10 +2,10 @@
2
2
 
3
3
  **When:** Removing files or writing a destructive command string.
4
4
 
5
- No hook watches Bash commands; a permission prompt can stall unattended work. Keep destructive literals out of command strings, including data. Use literal absolute targets; never target a bare temporary root. Use `git rm` for tracked files. Pass bodies by file and test hooks through the test suite. Copy this line into every subagent prompt:
5
+ Keep destructive literals out of command strings, including data. Use literal absolute targets; never target a bare temporary root. Use `git rm` for tracked files. Pass bodies by file and test hooks through the test suite. Copy this line into every subagent prompt:
6
6
 
7
7
  > Never use bash rm in any form. Delete scratch/probe files with the PowerShell tool (Remove-Item -Recurse -Force -Confirm:$false <absolute path>), or leave them in the OS temp dir; remove worktrees only via git worktree remove --force.
8
8
 
9
- **Enforcement:** none, the agent applies it; the harness may prompt.
9
+ **Enforcement:** none; a harness permission prompt can stall unattended work.
10
10
 
11
- **Full text:** [`docs/rule-guides/destructive-commands.md`](../docs/rule-guides/destructive-commands.md). Read it before any removal.
11
+ **Full text:** [guide](../docs/rule-guides/destructive-commands.md). Read it before any removal.
@@ -21,4 +21,4 @@ A doc that inventories code stays in step with the code, in the same change:
21
21
 
22
22
  **Enforcement:** `repository_checks/claude_md.py`, `repository_checks/package_inventory.py`, and `repository_checks/env_var_documentation.py`. Run `python packages/claude-dev-env/scripts/repository_policy.py` before you commit. CI runs the same command.
23
23
 
24
- **Full text:** [`docs/rule-guides/doc-inventory-integrity.md`](../docs/rule-guides/doc-inventory-integrity.md).
24
+ **Full text:** [guide](../docs/rule-guides/doc-inventory-integrity.md).
@@ -12,4 +12,4 @@ The list covers every behavior the body applies, and the body accepts only what
12
12
 
13
13
  **Enforcement:** `code_rules_docstrings.py` and the JS slices in `code_rules_imports_logging.py`, which the staged policy lint runs through `code_rules_enforcer.py`, plus its `hook-prose-consistency` rule. CI runs it against the merge base.
14
14
 
15
- **Full text:** [`docs/rule-guides/docstring-prose-matches-implementation.md`](../docs/rule-guides/docstring-prose-matches-implementation.md), with the write-time checklist. The Category O audit rubric carries the full standard.
15
+ **Full text:** [guide](../docs/rule-guides/docstring-prose-matches-implementation.md), with the write-time checklist. The Category O audit rubric carries the full standard.
@@ -6,4 +6,4 @@ Search for what already does the ask, in full or in part. When something exists,
6
6
 
7
7
  **Enforcement:** the pull request gate denies a body with no "Existing work" section.
8
8
 
9
- **Full text:** [`docs/rule-guides/explore-thoroughly.md`](../docs/rule-guides/explore-thoroughly.md). Read it for search scope and depth.
9
+ **Full text:** [guide](../docs/rule-guides/explore-thoroughly.md). Read it for search scope and depth.
@@ -17,4 +17,4 @@ Four failures end a run: the source bytes changed, a provenance or digest mismat
17
17
 
18
18
  **Enforcement:** `code_rules_blast_radius.py`, which the staged policy lint runs through `code_rules_enforcer.py` under its `code-rules` rule. CI runs it against the merge base.
19
19
 
20
- **Full text:** [`docs/rule-guides/failure-blast-radius.md`](../docs/rule-guides/failure-blast-radius.md), with the boundary example and the Codex excerpt.
20
+ **Full text:** [guide](../docs/rule-guides/failure-blast-radius.md), with the boundary example and the Codex excerpt.
@@ -15,6 +15,6 @@ paths:
15
15
 
16
16
  Apply a named break that the same check must catch, and run a passing control beside it on the same command. Record the break, its failing output, and the control before counting the restored green as evidence. If the check stays green under its break, fix its reach; a probe needs a trip input, a sweep needs a planted violation in its claimed file set, and a writer assertion needs a disabled production writer.
17
17
 
18
- **Enforcement:** none, the agent applies it.
18
+ **Enforcement:** none.
19
19
 
20
- **Full text:** [`docs/rule-guides/falsify-before-green.md`](../docs/rule-guides/falsify-before-green.md). Read it when designing a break or reviewing a red record.
20
+ **Full text:** [guide](../docs/rule-guides/falsify-before-green.md). Read it when designing a break or reviewing a red record.
@@ -0,0 +1,9 @@
1
+ # Features start with an eval
2
+
3
+ **When:** A user asks for a new feature or a change in behavior.
4
+
5
+ Your first action is the Skill tool with skill `claude-api` and args `build-eval`. Build the feature against that eval. When the work touches an existing feature with no eval, start a separate session to build that eval in parallel. An Agent-tool subagent does not count. You own its result.
6
+
7
+ **Enforcement:** `hooks/blocking/pr_lifecycle_skill_gate.py` denies a `feat` pull request with no "Eval" section or no build-eval call.
8
+
9
+ **Full text:** [guide](../docs/rule-guides/features-start-with-an-eval.md).
@@ -2,8 +2,8 @@
2
2
 
3
3
  **When:** Searching for files by name, path, extension, size, or date.
4
4
 
5
- Scope every search to a project, worktree, package, or narrowing filter. Never start at a filesystem root, drive root, bare home, or share root. Read a known path directly; use scoped `es.exe`, Glob, or Grep for discovery. After `es.exe` Error 8, retry twice before falling back within the same scope. Run one large shell walk at a time. Ask the user only after available search tools fail.
5
+ Scope every search to a project, worktree, package, or narrowing filter. Never start at a filesystem root, drive root, bare home, or share root. Read a known path directly; use scoped `es.exe`, Glob, or Grep for discovery. After `es.exe` Error 8, retry twice before falling back within the same scope. Run one large shell walk at a time.
6
6
 
7
- **Enforcement:** none, the agent applies it.
7
+ **Enforcement:** none.
8
8
 
9
- **Full text:** [`docs/rule-guides/filesystem-search.md`](../docs/rule-guides/filesystem-search.md). Read it for tool selection and search examples.
9
+ **Full text:** [guide](../docs/rule-guides/filesystem-search.md). Read it for tool selection and search examples.
@@ -15,6 +15,6 @@ paths:
15
15
 
16
16
  Give every check ID a breaking or smell severity row; style and structure findings are smells. Fail the gate and stop for a bug, secret, broken test, syntax error, or instruction file that cannot load. Record a smell in `.claude/followups/smells.jsonl`, proceed, and fix it later in one combined follow-up pull request per parent pull request, whose body names the parent as `Follow-up to #N`; ledger failure leaves the gate decision unchanged. Treat an unclassified check as breaking.
17
17
 
18
- **Enforcement:** none, the agent applies it.
18
+ **Enforcement:** none.
19
19
 
20
- **Full text:** [`docs/rule-guides/flag-non-breaking-findings.md`](../docs/rule-guides/flag-non-breaking-findings.md). Read it when adding a check.
20
+ **Full text:** [guide](../docs/rule-guides/flag-non-breaking-findings.md). Read it when adding a check.
@@ -10,6 +10,6 @@ paths:
10
10
 
11
11
  Act on settled facts and authority, and recommend after researching unclear intent. When protected or irreversible actions, scope changes, or user-only input block the task, ask for one answer in the user-facing channel, name what resumes, and keep other work moving. Finish owed work without ending a turn on a promise, milestone, or context limit. Delegate independent tracks while working, ground claims in tool output, name skipped checks, make the first update one sentence, reserve later updates for discoveries, and close with the outcome and input.
12
12
 
13
- **Enforcement:** none, the agent applies it.
13
+ **Enforcement:** none.
14
14
 
15
- **Full text:** [`docs/rule-guides/long-horizon-autonomy.md`](../docs/rule-guides/long-horizon-autonomy.md). Read it when preparing the run.
15
+ **Full text:** [guide](../docs/rule-guides/long-horizon-autonomy.md). Read it when preparing the run.