claude-dev-env 8.0.0 → 8.2.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 (97) hide show
  1. package/.agents/agents/clean-coder.md +4 -4
  2. package/.agents/agents/code-quality-agent.md +4 -4
  3. package/.agents/agents/plan-packet-validator.md +2 -2
  4. package/.agents/agents/session-advisor.md +1 -1
  5. package/.agents/skills/cert-classification-rule/SKILL.md +3 -3
  6. package/.agents/skills/e-code-review/SKILL.md +2 -2
  7. package/.agents/skills/e-code-review/reference/fix.md +1 -1
  8. package/.agents/skills/e-code-review/reference/loop.md +1 -1
  9. package/.agents/skills/e-code-review/reference/medium.md +4 -4
  10. package/.agents/skills/e-code-review/reference/xhigh.md +5 -5
  11. package/.agents/skills/everything-search/SKILL.md +3 -3
  12. package/.agents/skills/issue-tracker/SKILL.md +1 -1
  13. package/.agents/skills/orchestrator/SKILL.md +3 -3
  14. package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +1 -1
  15. package/.agents/skills/orchestrator/reference/executor-consult-block.md +1 -1
  16. package/.agents/skills/orchestrator-refresh/SKILL.md +2 -2
  17. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +96 -0
  18. package/.agents/skills/plugin-eval-standalone-skill/reference/graders-and-commands.md +108 -0
  19. package/.agents/skills/privacy-hygiene/SKILL.md +5 -6
  20. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +5 -6
  21. package/.agents/skills/repairing-hook-boundaries/SKILL.md +1 -1
  22. package/.agents/skills/syncing-submodules/SKILL.md +1 -1
  23. package/.agents/skills/team-advisor/SKILL.md +9 -7
  24. package/.agents/skills/team-advisor/reference/advisor-docs-review.md +28 -1
  25. package/.agents/skills/usage-pause/SKILL.md +3 -3
  26. package/_shared/advisor/advisor-protocol.md +7 -3
  27. package/_shared/advisor/reference/advisor-block.md +8 -1
  28. package/audit-rubrics/audit-categories.json +0 -4
  29. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  30. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +2 -2
  31. package/audit-rubrics/category_rubrics/category-i-concurrency.md +1 -1
  32. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
  33. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +1 -1
  34. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +5 -5
  35. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +5 -5
  36. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +3 -3
  37. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +3 -3
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +3 -3
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
  42. package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +12 -12
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +4 -4
  46. package/audit-rubrics/prompts/category-i-concurrency.md +6 -6
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +6 -29
  48. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
  49. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +7 -7
  50. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  51. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +3 -3
  52. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +2 -2
  53. package/bin/ever-shipped-skills.mjs +1 -0
  54. package/bin/ever-shipped-skills.test.mjs +4 -0
  55. package/docs/BDD_SCENARIO_QUALITY.md +2 -2
  56. package/docs/CODE_RULES.md +7 -7
  57. package/docs/TEST_QUALITY.md +1 -1
  58. package/docs/nas-ssh-invocation.md +1 -1
  59. package/docs/references/advisor-tool.md +2 -2
  60. package/docs/references/prose-style-enforcement.md +1 -1
  61. package/docs/references/team-advisor-skill.md +2 -2
  62. package/docs/references/weak-executor-advisor.md +1 -1
  63. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  64. package/package.json +1 -1
  65. package/rules/anti-corollary-tests.md +3 -1
  66. package/rules/archiving-agent-config.md +67 -0
  67. package/rules/asd-ste100-language.md +1 -1
  68. package/rules/ci-owns-the-gate.md +15 -0
  69. package/rules/cleanup-temp-files.md +1 -1
  70. package/rules/code-standards.md +2 -2
  71. package/rules/docstring-prose-matches-implementation.md +1 -1
  72. package/rules/explore-thoroughly.md +2 -2
  73. package/rules/failure-blast-radius.md +9 -8
  74. package/rules/git-workflow.md +7 -4
  75. package/rules/long-horizon-autonomy.md +3 -1
  76. package/rules/nas-ssh-invocation.md +1 -1
  77. package/rules/research-mode.md +3 -1
  78. package/rules/retired-hook-prose.md +10 -2
  79. package/rules/testing.md +3 -3
  80. package/rules/verify-before-asking.md +3 -0
  81. package/rules/verify-runtime-state.md +2 -2
  82. package/scripts/active_capability_references.py +74 -19
  83. package/scripts/banned_prose_words.py +159 -0
  84. package/scripts/dev_env_scripts_constants/active_capability_constants.py +12 -10
  85. package/scripts/dev_env_scripts_constants/banned_prose_word_constants.py +95 -0
  86. package/scripts/policy_lint/adapter_retired_hook_prose.py +17 -7
  87. package/scripts/policy_lint/adapters.py +6 -6
  88. package/scripts/policy_lint/config/constants.py +8 -0
  89. package/scripts/policy_lint/registry.py +1 -1
  90. package/scripts/sync_to_cursor/rules.py +1 -1
  91. package/scripts/test_active_capability_references.py +100 -33
  92. package/scripts/tests/test_banned_prose_words.py +103 -0
  93. package/scripts/tests/test_codex_compat_materializer.py +3 -3
  94. package/scripts/tests/test_engine.py +1 -1
  95. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +46 -0
  96. package/scripts/tests/test_rules.py +12 -1
  97. package/system-prompts/software-engineer.xml +7 -7
@@ -3,13 +3,15 @@
3
3
 
4
4
  Builds inventories from committed skill manifests, agent markdown files, and
5
5
  commands. Scans active prompt text for slash and backticked capability names.
6
- Fails when an active (non-inert) reference names a banned capability.
6
+ Fails when an active (non-inert) reference names a capability the tree
7
+ dropped, resolved from the ever-shipped registry against what the tree
8
+ holds today.
7
9
 
8
10
  ::
9
11
 
10
12
  unresolved = unresolved_active_capabilities(package_root)
11
13
  ok: unresolved == []
12
- flag: active text mentions /qbug or `stub-detector`
14
+ flag: active text mentions a name the tree dropped
13
15
  """
14
16
 
15
17
  from __future__ import annotations
@@ -19,10 +21,12 @@ from dataclasses import dataclass
19
21
  from pathlib import Path
20
22
 
21
23
  from dev_env_scripts_constants.active_capability_constants import (
22
- ALL_BANNED_ACTIVE_CAPABILITY_NAMES,
23
24
  ALL_INERT_FENCE_LANGUAGES,
24
- BACKTICK_CAPABILITY_PATTERN,
25
- BANNED_REASON_PREFIX,
25
+ ALL_QUALIFIED_CAPABILITY_PATTERNS,
26
+ ALL_UNREGISTERED_RETIRED_CAPABILITY_NAMES,
27
+ BLANKED_SPAN,
28
+ EVER_SHIPPED_NAME_PATTERN,
29
+ EVER_SHIPPED_REGISTRY_RELATIVE_PATH,
26
30
  FENCE_CLOSE_PATTERN,
27
31
  FENCE_OPEN_PATTERN,
28
32
  NEWLINE_JOIN_SEPARATOR,
@@ -31,8 +35,11 @@ from dev_env_scripts_constants.active_capability_constants import (
31
35
  PACKAGE_ROOT_AGENTS_DIRECTORY,
32
36
  PACKAGE_ROOT_SKILLS_DIRECTORY,
33
37
  PACKAGE_SKILLS_DIRECTORY,
38
+ RETIRED_REASON_PREFIX,
34
39
  SKILL_MANIFEST_FILENAME,
40
+ MULTI_SEGMENT_PATH_PATTERN,
35
41
  SLASH_CAPABILITY_PATTERN,
42
+ URL_PATTERN,
36
43
  UTF8_ENCODING,
37
44
  )
38
45
 
@@ -52,7 +59,7 @@ class CapabilityInventory:
52
59
 
53
60
  @dataclass(frozen=True)
54
61
  class UnresolvedCapabilityReference:
55
- """One active reference that names a banned capability."""
62
+ """One active reference that names a capability outside the shipped tree."""
56
63
 
57
64
  file_path: str
58
65
  line_number: int
@@ -148,6 +155,10 @@ def strip_inert_fenced_blocks(markdown_text: str) -> str:
148
155
  def extract_active_capability_names(markdown_text: str) -> list[tuple[int, str]]:
149
156
  """Return (1-based line, capability name) for active references.
150
157
 
158
+ A slash reference counts as an invocation, and a qualified reference
159
+ names a skill directory or manifest. A URL and a multi-segment path
160
+ carry neither, so both are blanked before the scan.
161
+
151
162
  Args:
152
163
  markdown_text: Prompt or skill markdown.
153
164
 
@@ -155,31 +166,72 @@ def extract_active_capability_names(markdown_text: str) -> list[tuple[int, str]]
155
166
  Ordered list of line number and capability name pairs.
156
167
  """
157
168
  slash_pattern = re.compile(SLASH_CAPABILITY_PATTERN)
158
- backtick_pattern = re.compile(BACKTICK_CAPABILITY_PATTERN)
169
+ url_pattern = re.compile(URL_PATTERN)
170
+ path_pattern = re.compile(MULTI_SEGMENT_PATH_PATTERN)
171
+ all_qualified_patterns = [
172
+ re.compile(each_pattern) for each_pattern in ALL_QUALIFIED_CAPABILITY_PATTERNS
173
+ ]
159
174
  active_text = strip_inert_fenced_blocks(markdown_text)
160
175
  all_hits: list[tuple[int, str]] = []
161
176
  for each_line_number, each_line in enumerate(active_text.splitlines(), start=1):
162
- for each_match in slash_pattern.finditer(each_line):
163
- all_hits.append((each_line_number, each_match.group(1)))
164
- for each_match in backtick_pattern.finditer(each_line):
177
+ for each_pattern in all_qualified_patterns:
178
+ for each_match in each_pattern.finditer(each_line):
179
+ all_hits.append((each_line_number, each_match.group(1)))
180
+ invocation_line = path_pattern.sub(
181
+ BLANKED_SPAN, url_pattern.sub(BLANKED_SPAN, each_line)
182
+ )
183
+ for each_match in slash_pattern.finditer(invocation_line):
165
184
  all_hits.append((each_line_number, each_match.group(1)))
166
185
  return all_hits
167
186
 
168
187
 
169
- def classify_capability_reference(capability_name: str) -> str | None:
170
- """Return a failure reason when the capability is banned.
188
+ def retired_capability_names(
189
+ from_package_root: Path, inventory: CapabilityInventory
190
+ ) -> frozenset[str]:
191
+ """Return the capability names the shipped tree dropped.
171
192
 
172
- Extractors also match ordinary prose tokens, so unknown names pass.
173
- Only the ban list fails a reference.
193
+ The ever-shipped registry keeps every skill name the package released.
194
+ Subtracting what the tree holds today leaves the retired set, so no hand
195
+ kept list decides which name fails.
196
+
197
+ Args:
198
+ from_package_root: ``packages/claude-dev-env`` root.
199
+ inventory: Names the tree holds today.
200
+
201
+ Returns:
202
+ Retired capability names.
203
+ """
204
+ registry_path = from_package_root / EVER_SHIPPED_REGISTRY_RELATIVE_PATH
205
+ all_ever_shipped_names: set[str] = set(
206
+ ALL_UNREGISTERED_RETIRED_CAPABILITY_NAMES
207
+ )
208
+ if registry_path.is_file():
209
+ all_ever_shipped_names.update(
210
+ re.findall(
211
+ EVER_SHIPPED_NAME_PATTERN,
212
+ registry_path.read_text(encoding=UTF8_ENCODING),
213
+ )
214
+ )
215
+ return frozenset(all_ever_shipped_names - inventory.all_known_names())
216
+
217
+
218
+ def classify_capability_reference(
219
+ capability_name: str, all_retired_names: frozenset[str]
220
+ ) -> str | None:
221
+ """Return a failure reason when the capability left the tree.
222
+
223
+ Extractors also match ordinary prose tokens, so a name the package never
224
+ shipped passes. A name the registry records and the tree dropped fails.
174
225
 
175
226
  Args:
176
227
  capability_name: Extracted skill/command-like name.
228
+ all_retired_names: Names resolved by ``retired_capability_names``.
177
229
 
178
230
  Returns:
179
231
  Reason string, or None when the reference is allowed.
180
232
  """
181
- if capability_name in ALL_BANNED_ACTIVE_CAPABILITY_NAMES:
182
- return f"{BANNED_REASON_PREFIX}{capability_name}"
233
+ if capability_name in all_retired_names:
234
+ return f"{RETIRED_REASON_PREFIX}{capability_name}"
183
235
  return None
184
236
 
185
237
 
@@ -188,7 +240,7 @@ def unresolved_active_capabilities(
188
240
  *,
189
241
  all_relative_markdown_paths: list[str] | None = None,
190
242
  ) -> list[UnresolvedCapabilityReference]:
191
- """Scan package markdown for banned active capability references.
243
+ """Scan package markdown for references to retired capabilities.
192
244
 
193
245
  Args:
194
246
  from_package_root: ``packages/claude-dev-env`` root.
@@ -196,10 +248,13 @@ def unresolved_active_capabilities(
196
248
  defaults to all skills, agents, and commands markdown.
197
249
 
198
250
  Returns:
199
- Banned references with file, line, name, and reason.
251
+ Retired references with file, line, name, and reason.
200
252
  """
201
253
  if all_relative_markdown_paths is None:
202
254
  all_relative_markdown_paths = _default_markdown_paths(from_package_root)
255
+ all_retired_names = retired_capability_names(
256
+ from_package_root, build_capability_inventory(from_package_root)
257
+ )
203
258
  all_unresolved: list[UnresolvedCapabilityReference] = []
204
259
  for each_relative_path in all_relative_markdown_paths:
205
260
  absolute_path = from_package_root / each_relative_path
@@ -209,7 +264,7 @@ def unresolved_active_capabilities(
209
264
  for each_line_number, each_name in extract_active_capability_names(
210
265
  markdown_text
211
266
  ):
212
- reason = classify_capability_reference(each_name)
267
+ reason = classify_capability_reference(each_name, all_retired_names)
213
268
  if reason is None:
214
269
  continue
215
270
  all_unresolved.append(
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """Find banned emphasis words in authored prose.
3
+
4
+ The package AGENTS.md bans ``real``, ``really``, ``real-world``, ``actual``,
5
+ ``actually``, ``genuine``, and ``true`` from every authored surface. This
6
+ module reports each occurrence that survives after code spans, paths, and
7
+ link targets are removed, so a caller can grade a document or a whole tree.
8
+
9
+ ::
10
+
11
+ all_hits = find_banned_prose_words("The real cause is a timeout.\n")
12
+ ok: all_hits == [(1, "real")]
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path, PurePosixPath
18
+
19
+ from dev_env_scripts_constants.banned_prose_word_constants import (
20
+ ADJECTIVAL_TRUE_PATTERN,
21
+ ALL_EXEMPT_PATH_SEGMENTS,
22
+ ALL_GOVERNED_PATH_SEGMENTS,
23
+ ALL_GOVERNED_SUFFIXES,
24
+ ALL_PREDICATIVE_TRUE_LEADS,
25
+ BANNED_PROSE_WORD_PATTERN,
26
+ BLANKED_SPAN,
27
+ FENCED_CODE_PATTERN,
28
+ INDENTED_CODE_PATTERN,
29
+ INLINE_CODE_PATTERN,
30
+ MARKDOWN_LINK_TARGET_PATTERN,
31
+ PATH_PATTERN,
32
+ URL_PATTERN,
33
+ UTF8_ENCODING,
34
+ XML_TAG_PATTERN,
35
+ )
36
+
37
+
38
+ def governs_path(relative_posix_path: str) -> bool:
39
+ """Return whether the ban applies to one repository path.
40
+
41
+ Args:
42
+ relative_posix_path: Repository-relative path with forward slashes.
43
+
44
+ Returns:
45
+ True for an authored instruction surface the ban covers.
46
+ """
47
+ normalized_path = f"/{relative_posix_path.lower().lstrip('/')}"
48
+ if PurePosixPath(normalized_path).suffix not in ALL_GOVERNED_SUFFIXES:
49
+ return False
50
+ if any(
51
+ each_segment in normalized_path for each_segment in ALL_EXEMPT_PATH_SEGMENTS
52
+ ):
53
+ return False
54
+ return any(
55
+ each_segment in normalized_path for each_segment in ALL_GOVERNED_PATH_SEGMENTS
56
+ )
57
+
58
+
59
+ def prose_lines(document_text: str) -> list[str]:
60
+ """Return one prose line for each source line, with code spans blanked.
61
+
62
+ Args:
63
+ document_text: Full document source.
64
+
65
+ Returns:
66
+ Lines of the same count as the source, holding prose only.
67
+ """
68
+ all_prose_lines: list[str] = []
69
+ is_inside_fence = False
70
+ for each_line in document_text.splitlines():
71
+ if FENCED_CODE_PATTERN.match(each_line):
72
+ is_inside_fence = not is_inside_fence
73
+ all_prose_lines.append("")
74
+ continue
75
+ if is_inside_fence or INDENTED_CODE_PATTERN.match(each_line):
76
+ all_prose_lines.append("")
77
+ continue
78
+ prose_line = INLINE_CODE_PATTERN.sub(BLANKED_SPAN, each_line)
79
+ prose_line = XML_TAG_PATTERN.sub(BLANKED_SPAN, prose_line)
80
+ prose_line = MARKDOWN_LINK_TARGET_PATTERN.sub(BLANKED_SPAN, prose_line)
81
+ prose_line = URL_PATTERN.sub(BLANKED_SPAN, prose_line)
82
+ all_prose_lines.append(PATH_PATTERN.sub(BLANKED_SPAN, prose_line))
83
+ return all_prose_lines
84
+
85
+
86
+ def _adjectival_true_columns(prose_line: str) -> list[int]:
87
+ all_columns: list[int] = []
88
+ for each_match in ADJECTIVAL_TRUE_PATTERN.finditer(prose_line):
89
+ leading_words = prose_line[: each_match.start()].lower().split()
90
+ if leading_words and leading_words[-1].strip(",;:(") in (
91
+ ALL_PREDICATIVE_TRUE_LEADS
92
+ ):
93
+ continue
94
+ all_columns.append(each_match.start() + 1)
95
+ return all_columns
96
+
97
+
98
+ def find_banned_prose_words(document_text: str) -> list[tuple[int, int, str]]:
99
+ """Return each banned word occurrence in authored prose.
100
+
101
+ Args:
102
+ document_text: Full document source.
103
+
104
+ Returns:
105
+ Ordered ``(line number, column, word)`` triples, both 1-based.
106
+ """
107
+ all_hits: list[tuple[int, int, str]] = []
108
+ for each_line_number, each_prose_line in enumerate(prose_lines(document_text), 1):
109
+ for each_match in BANNED_PROSE_WORD_PATTERN.finditer(each_prose_line):
110
+ all_hits.append(
111
+ (each_line_number, each_match.start() + 1, each_match.group(1).lower())
112
+ )
113
+ for each_column in _adjectival_true_columns(each_prose_line):
114
+ all_hits.append((each_line_number, each_column, "true"))
115
+ return sorted(all_hits)
116
+
117
+
118
+ def governed_paths(from_package_root: Path) -> list[str]:
119
+ """Return every governed surface under one package root.
120
+
121
+ Args:
122
+ from_package_root: ``packages/claude-dev-env`` root.
123
+
124
+ Returns:
125
+ Sorted package-relative paths the ban covers.
126
+ """
127
+ all_paths: list[str] = []
128
+ for each_path in from_package_root.rglob("*"):
129
+ if not each_path.is_file():
130
+ continue
131
+ relative_path = each_path.relative_to(from_package_root).as_posix()
132
+ if governs_path(relative_path):
133
+ all_paths.append(relative_path)
134
+ return sorted(all_paths)
135
+
136
+
137
+ def banned_prose_words_in_tree(
138
+ from_package_root: Path,
139
+ ) -> list[tuple[str, int, int, str]]:
140
+ """Return every banned word occurrence across the governed surfaces.
141
+
142
+ Args:
143
+ from_package_root: ``packages/claude-dev-env`` root.
144
+
145
+ Returns:
146
+ Ordered ``(package-relative path, line number, column, word)`` tuples.
147
+ """
148
+ all_hits: list[tuple[str, int, int, str]] = []
149
+ for each_relative_path in governed_paths(from_package_root):
150
+ document_text = (from_package_root / each_relative_path).read_text(
151
+ encoding=UTF8_ENCODING, errors="replace"
152
+ )
153
+ for each_line_number, each_column, each_word in find_banned_prose_words(
154
+ document_text
155
+ ):
156
+ all_hits.append(
157
+ (each_relative_path, each_line_number, each_column, each_word)
158
+ )
159
+ return all_hits
@@ -14,18 +14,16 @@ PACKAGE_ROOT_SKILLS_DIRECTORY: str = "skills"
14
14
  PACKAGE_ROOT_AGENTS_DIRECTORY: str = "agents"
15
15
  SKILL_MANIFEST_FILENAME: str = "SKILL.md"
16
16
 
17
- # Slash-command and skill names that must never appear as active instructions.
18
- ALL_BANNED_ACTIVE_CAPABILITY_NAMES: frozenset[str] = frozenset(
17
+ ALL_UNREGISTERED_RETIRED_CAPABILITY_NAMES: frozenset[str] = frozenset(
19
18
  {
20
19
  "stub-detector",
21
20
  "superpowers",
22
- "qbug",
23
- "findbugs",
24
- "fixbugs",
25
- "monitor-open-prs",
26
21
  }
27
22
  )
28
23
 
24
+ EVER_SHIPPED_REGISTRY_RELATIVE_PATH: str = "bin/ever-shipped-skills.mjs"
25
+ EVER_SHIPPED_NAME_PATTERN: str = r"'([a-z][a-z0-9-]*)'"
26
+
29
27
  # Fence languages treated as inert historical/example content.
30
28
  ALL_INERT_FENCE_LANGUAGES: frozenset[str] = frozenset(
31
29
  {
@@ -37,13 +35,17 @@ ALL_INERT_FENCE_LANGUAGES: frozenset[str] = frozenset(
37
35
  }
38
36
  )
39
37
 
40
- SLASH_CAPABILITY_PATTERN: str = r"(?<![`\w])/([a-z][a-z0-9-]{2,})"
41
- BACKTICK_CAPABILITY_PATTERN: str = (
42
- r"`(?:skills/)?([a-z][a-z0-9-]{2,})(?:/SKILL\.md)?`"
38
+ SLASH_CAPABILITY_PATTERN: str = r"(?<![`\w./-])/([a-z][a-z0-9-]{2,})(?![\w/-])"
39
+ ALL_QUALIFIED_CAPABILITY_PATTERNS: tuple[str, ...] = (
40
+ r"(?<![\w-])([a-z][a-z0-9-]{2,})/SKILL\.md",
41
+ r"`skills/([a-z][a-z0-9-]{2,})`",
43
42
  )
43
+ URL_PATTERN: str = r"\b[a-z][a-z0-9+.-]*://\S+"
44
+ MULTI_SEGMENT_PATH_PATTERN: str = r"(?<![\w-])[\w.{}-]*/[\w.{}/-]*/[\w.{}-]+"
45
+ BLANKED_SPAN: str = " "
44
46
  FENCE_OPEN_PATTERN: str = r"^```([A-Za-z0-9_-]*)\s*$"
45
47
  FENCE_CLOSE_PATTERN: str = r"^```\s*$"
46
- BANNED_REASON_PREFIX: str = "banned_active_capability:"
48
+ RETIRED_REASON_PREFIX: str = "retired_capability:"
47
49
 
48
50
  UTF8_ENCODING: str = "utf-8"
49
51
  NEWLINE_JOIN_SEPARATOR: str = "\n"
@@ -0,0 +1,95 @@
1
+ """Constants for the banned-prose-word detector.
2
+
3
+ The package AGENTS.md bans a family of emphasis words from every authored
4
+ surface. These patterns describe where the ban applies and which spans of a
5
+ document are code rather than prose.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ UTF8_ENCODING: str = "utf-8"
13
+
14
+ ALL_GOVERNED_PATH_SEGMENTS: tuple[str, ...] = (
15
+ "/.agents/agents/",
16
+ "/.agents/skills/",
17
+ "/audit-rubrics/",
18
+ "/commands/",
19
+ "/docs/",
20
+ "/output-styles/",
21
+ "/rules/",
22
+ "/system-prompts/",
23
+ )
24
+
25
+ ALL_EXEMPT_PATH_SEGMENTS: tuple[str, ...] = (
26
+ "-archived/",
27
+ "/fixtures/",
28
+ "/tests/",
29
+ "/changelog.md",
30
+ )
31
+
32
+ ALL_GOVERNED_SUFFIXES: frozenset[str] = frozenset({".md", ".mdx", ".xml"})
33
+
34
+ ALL_BANNED_PROSE_WORDS: tuple[str, ...] = (
35
+ "real",
36
+ "really",
37
+ "real-world",
38
+ "actual",
39
+ "actually",
40
+ "genuine",
41
+ "genuinely",
42
+ "truly",
43
+ )
44
+
45
+ BANNED_PROSE_WORD_PATTERN: re.Pattern[str] = re.compile(
46
+ r"(?<![\w-])(" + "|".join(ALL_BANNED_PROSE_WORDS) + r")(?![\w-])",
47
+ re.IGNORECASE,
48
+ )
49
+
50
+ ADJECTIVAL_TRUE_PATTERN: re.Pattern[str] = re.compile(
51
+ r"(?<![\w-])true(?![\w-])\s+[a-z]", re.IGNORECASE
52
+ )
53
+
54
+ ALL_PREDICATIVE_TRUE_LEADS: frozenset[str] = frozenset(
55
+ {
56
+ "is",
57
+ "are",
58
+ "was",
59
+ "were",
60
+ "be",
61
+ "been",
62
+ "being",
63
+ "always",
64
+ "still",
65
+ "holds",
66
+ "hold",
67
+ "held",
68
+ "stays",
69
+ "stay",
70
+ "remains",
71
+ "remain",
72
+ "returns",
73
+ "return",
74
+ "reads",
75
+ "read",
76
+ "and",
77
+ "or",
78
+ "not",
79
+ "never",
80
+ }
81
+ )
82
+
83
+ FENCED_CODE_PATTERN: re.Pattern[str] = re.compile(r"^\s*```")
84
+ INDENTED_CODE_PATTERN: re.Pattern[str] = re.compile(r"^(?: {4,}|\t)\S")
85
+ INLINE_CODE_PATTERN: re.Pattern[str] = re.compile(r"`[^`]*`")
86
+ URL_PATTERN: re.Pattern[str] = re.compile(r"\b(?:https?|file)://\S+")
87
+ PATH_PATTERN: re.Pattern[str] = re.compile(r"(?<![\w])[\w./-]*/[\w./-]+")
88
+ MARKDOWN_LINK_TARGET_PATTERN: re.Pattern[str] = re.compile(r"\]\([^)]*\)")
89
+ XML_TAG_PATTERN: re.Pattern[str] = re.compile(r"<[^>]+>")
90
+ BLANKED_SPAN: str = " "
91
+
92
+ BANNED_PROSE_WORD_RULE_ID: str = "banned-prose-word"
93
+ BANNED_PROSE_WORD_MESSAGE: str = (
94
+ "Banned word: {word}. Delete it and name the evidence instead."
95
+ )
@@ -15,23 +15,33 @@ from .config import constants
15
15
  from .model import Diagnostic, Document, Location, Severity
16
16
 
17
17
 
18
- def accepts_rules_markdown(document: Document) -> bool:
19
- """Return whether the document is Markdown in a rules directory.
18
+ def accepts_instruction_markdown(document: Document) -> bool:
19
+ """Return whether the document is authored instruction Markdown.
20
+
21
+ Rules, skills, agents, commands, documentation and stored system prompts
22
+ all describe hooks to a reader, so each one carries the same claim.
20
23
 
21
24
  Args:
22
25
  document: Candidate document.
23
26
 
24
27
  Returns:
25
- True for a Markdown file directly under a ``rules`` directory.
28
+ True for Markdown on an instruction surface.
26
29
  """
27
- return (
28
- document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
29
- and document.path.parent.name == constants.RULES_DIRECTORY_NAME
30
+ if document.path.suffix.lower() not in constants.ALL_MARKDOWN_SUFFIXES:
31
+ return False
32
+ normalized_path = f"/{document.path.as_posix().lower()}"
33
+ return any(
34
+ each_segment in normalized_path
35
+ for each_segment in constants.ALL_HOOK_PROSE_SURFACE_SEGMENTS
30
36
  )
31
37
 
32
38
 
33
39
  def _package_root(repository_root: Path, document: Document) -> Path:
34
- return repository_root / document.path.parent.parent.as_posix()
40
+ for each_ancestor in document.path.parents:
41
+ candidate_root = repository_root / each_ancestor.as_posix()
42
+ if (candidate_root / constants.HOOKS_DIRECTORY_NAME).is_dir():
43
+ return candidate_root
44
+ return repository_root / document.path.parent.as_posix()
35
45
 
36
46
 
37
47
  def _read_text(path: Path) -> str:
@@ -427,25 +427,25 @@ def accepts_plans(document: Document) -> bool:
427
427
  )
428
428
 
429
429
 
430
- def accepts_rules_markdown(document: Document) -> bool:
431
- """Return whether the document is Markdown in a rules directory.
430
+ def accepts_instruction_markdown(document: Document) -> bool:
431
+ """Return whether the document is authored instruction Markdown.
432
432
 
433
433
  Args:
434
434
  document: Candidate document.
435
435
 
436
436
  Returns:
437
- True for a Markdown file directly under a ``rules`` directory.
437
+ True for Markdown on an instruction surface.
438
438
  """
439
- return adapter_retired_hook_prose.accepts_rules_markdown(document)
439
+ return adapter_retired_hook_prose.accepts_instruction_markdown(document)
440
440
 
441
441
 
442
442
  def retired_hook_prose_diagnostics(
443
443
  document: Document, repository_root: Path
444
444
  ) -> tuple[Diagnostic, ...]:
445
- """Report rules prose that credits an unregistered hook with a live action.
445
+ """Report instruction prose that credits an unregistered hook with a live action.
446
446
 
447
447
  Args:
448
- document: Current rules Markdown text and path.
448
+ document: Current instruction Markdown text and path.
449
449
  repository_root: Request repository root for package resolution.
450
450
 
451
451
  Returns:
@@ -92,6 +92,14 @@ VIOLATION_SCOPING_MODULE_NAME = "code_rules_gate_parts.violation_scoping"
92
92
 
93
93
  RETIRED_HOOK_PROSE_RULE_ID = "retired-hook-prose"
94
94
  RULES_DIRECTORY_NAME = "rules"
95
+ ALL_HOOK_PROSE_SURFACE_SEGMENTS = (
96
+ "/.agents/",
97
+ "/commands/",
98
+ "/docs/",
99
+ "/output-styles/",
100
+ "/rules/",
101
+ "/system-prompts/",
102
+ )
95
103
  HOOKS_DIRECTORY_NAME = "hooks"
96
104
  HOOKS_CONFIGURATION_FILE_NAME = "hooks.json"
97
105
  HOOK_CONSTANTS_DIRECTORY_NAME = "hooks_constants"
@@ -106,7 +106,7 @@ def _text_document_rules() -> tuple[model.Rule, ...]:
106
106
  ),
107
107
  build_document_rule(
108
108
  "retired-hook-prose",
109
- adapters.accepts_rules_markdown,
109
+ adapters.accepts_instruction_markdown,
110
110
  adapters.retired_hook_prose_diagnostics,
111
111
  ),
112
112
  )
@@ -73,7 +73,7 @@ _merged_mapping_key_order = (
73
73
  _code_standards_section_order = (
74
74
  "COMMENT PRESERVATION",
75
75
  "CORE PRINCIPLES",
76
- "⚡ HOOK-ENFORCED RULES",
76
+ "⚡ LINT-ENFORCED RULES",
77
77
  "3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
78
78
  "5. NO ABBREVIATIONS",
79
79
  "6. COMPLETE TYPE HINTS",