claude-dev-env 2.7.0 → 2.8.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 (88) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/pr-loop/worker-spawn.md +3 -1
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/code-quality-agent.md +6 -5
  13. package/agents/deep-research.md +7 -24
  14. package/agents/docs-agent.md +1 -27
  15. package/agents/issue-tracker.md +1 -7
  16. package/agents/skill-writer-agent.md +1 -2
  17. package/agents/test_agent_frontmatter.py +309 -12
  18. package/hooks/blocking/CLAUDE.md +2 -0
  19. package/hooks/blocking/fable_spawn_gate.py +187 -0
  20. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  21. package/hooks/blocking/plain_language_blocker.py +287 -15
  22. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  23. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  24. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  26. package/hooks/git-hooks/CLAUDE.md +2 -2
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  28. package/hooks/git-hooks/pre_push.py +343 -54
  29. package/hooks/git-hooks/test_pre_push.py +852 -6
  30. package/hooks/hooks.json +9 -19
  31. package/hooks/hooks_constants/CLAUDE.md +2 -0
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  33. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  34. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  35. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  36. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  37. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  40. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  41. package/package.json +1 -1
  42. package/rules/CLAUDE.md +1 -0
  43. package/rules/ask-user-question-required.md +26 -0
  44. package/rules/claims-as-quotes.md +65 -0
  45. package/scripts/CLAUDE.md +4 -4
  46. package/scripts/_code_review_test_support.py +6 -0
  47. package/scripts/check.ps1 +18 -5
  48. package/scripts/claude_chain_runner.py +203 -31
  49. package/scripts/codec_forwarding_test_support.py +2 -0
  50. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  51. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  52. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  53. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
  54. package/scripts/grok_headless_runner.py +148 -18
  55. package/scripts/resolve_worker_spawn.py +56 -10
  56. package/scripts/spawn_grok_batch.py +81 -23
  57. package/scripts/test_claude_chain_runner.py +358 -0
  58. package/scripts/test_grok_headless_runner.py +547 -10
  59. package/scripts/test_invoke_code_review.py +298 -0
  60. package/scripts/test_resolve_worker_spawn.py +185 -15
  61. package/scripts/test_spawn_grok_batch.py +326 -22
  62. package/scripts/tests/CLAUDE.md +1 -0
  63. package/scripts/tests/test_grok_worker_constants.py +59 -0
  64. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
  66. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
  67. package/skills/autoconverge/workflow/converge.mjs +113 -31
  68. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  69. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  70. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  71. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  72. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  73. package/skills/e-code-review/SKILL.md +9 -8
  74. package/skills/e-code-review/reference/fix.md +29 -7
  75. package/skills/e-code-review/reference/loop.md +230 -14
  76. package/skills/e-code-review/reference/low.md +33 -15
  77. package/skills/e-code-review/reference/medium.md +55 -21
  78. package/skills/e-code-review/reference/xhigh.md +30 -12
  79. package/skills/fresh-branch/CLAUDE.md +5 -5
  80. package/skills/fresh-branch/SKILL.md +14 -6
  81. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  82. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  83. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  84. package/skills/grok-spawn/SKILL.md +10 -3
  85. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  86. package/skills/orchestrator/SKILL.md +4 -1
  87. package/skills/orchestrator-refresh/SKILL.md +5 -1
  88. package/skills/team-advisor/SKILL.md +4 -1
@@ -8,7 +8,7 @@ color: red
8
8
 
9
9
  You audit a pull request diff for bugs and CODE_RULES.md compliance issues. You return findings; the orchestrator handles fixes.
10
10
 
11
- **Announce at start:** "Using code-quality-agent — auditing diff against A–P categories with CODE_RULES.md awareness."
11
+ **Announce at start:** "Using code-quality-agent — auditing diff against A–Q categories with CODE_RULES.md awareness."
12
12
 
13
13
  ## Scope
14
14
 
@@ -18,7 +18,7 @@ Audit only added or modified lines in the diff. Pre-existing code on untouched l
18
18
 
19
19
  This agent runs in one of two modes depending on the calling prompt:
20
20
 
21
- - **Unscoped (default):** the prompt names no categories. Walk all of A through P and produce Shape A/B for every category.
21
+ - **Unscoped (default):** the prompt names no categories. Walk all of A through Q and produce Shape A/B for every category.
22
22
  - **Category-restricted:** the prompt names a subset of categories ("audit only category F" or "investigate only H, I, and K"). Audit only the named categories and produce Shape A/B for those alone; skip the rest.
23
23
 
24
24
  Tradeoff for callers picking the category-restricted mode: parallel category invocation loses cross-category reasoning. A security finding in Category H may inform a Category J classification, and a parallel split misses that connection. When categories need to inform each other, prefer the unscoped mode.
@@ -31,9 +31,9 @@ Preserve every existing comment. Findings on production code report only on new
31
31
 
32
32
  Report findings only. Author zero edits. Author zero diffs. Run zero commits or pushes. The orchestrator (and the calling skill) handles fix application, commit creation, and PR posting based on your finding list.
33
33
 
34
- ## Bug Categories A–P
34
+ ## Bug Categories A–Q
35
35
 
36
- Every audit pass walks all sixteen categories. Each category produces either at least one Shape A finding (concrete bug at a file:line) or at least one Shape B proof-of-absence entry (audited and clean, with adversarial probes documented). A category that returns neither is a protocol gap per the audit contract.
36
+ Every audit pass walks all seventeen categories. Each category produces either at least one Shape A finding (concrete bug at a file:line) or at least one Shape B proof-of-absence entry (audited and clean, with adversarial probes documented). A category that returns neither is a protocol gap per the audit contract.
37
37
 
38
38
  For each category's full description, examples, sub-bucket decomposition, and concrete checks, read the matching rubric in `../audit-rubrics/category_rubrics/`:
39
39
 
@@ -55,6 +55,7 @@ For each category's full description, examples, sub-bucket decomposition, and co
55
55
  | N | Test-name scenario verifier | `../audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md` |
56
56
  | O | Docstring / fixture-prose vs implementation drift | `../audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` |
57
57
  | P | Name / regex / word-list vs behavior-contract precision | `../audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md` |
58
+ | Q | Cross-surface claim consistency (terminology, PR-description claims, message-vs-guard) | `../audit-rubrics/category_rubrics/category-q-cross-surface-claims.md` |
58
59
 
59
60
  Test files (`test_*.py`, `*_test.py`, `*.test.*`, `*.spec.*`, `conftest.py`, and any path under `/tests/`) are exempt from category J. The exempt path families documented in the J reference also opt out of the constants-location sub-item.
60
61
 
@@ -114,7 +115,7 @@ A bare verified-clean label is inadequate: every Shape B entry lists the files o
114
115
 
115
116
  ## Per-Category Expectation
116
117
 
117
- Every category A through P is investigated. The output for each category is one of:
118
+ Every category A through Q is investigated. The output for each category is one of:
118
119
  - one or more Shape A findings, or
119
120
  - one Shape B proof-of-absence entry with concrete files, quoted lines, and adversarial probes.
120
121
 
@@ -1,25 +1,6 @@
1
1
  ---
2
2
  name: deep-research
3
- description: Use this agent for iterative, multi-source deep research that produces comprehensive Obsidian reports with full citations. Official-docs-first methodology with anti-hallucination constraints. Examples:
4
-
5
- <example>
6
- Context: User wants thorough research on a technical topic
7
- user: "Research the current state of WebSocket authentication best practices"
8
- assistant: "I'll use the deep-research agent to conduct iterative multi-source research and produce a cited report."
9
- <commentary>
10
- Multi-source research requiring iteration and synthesis — exactly what deep-research handles.
11
- </commentary>
12
- </example>
13
-
14
- <example>
15
- Context: User needs a landscape survey with citations
16
- user: "Compare the major vector database options for production RAG systems in 2026"
17
- assistant: "I'll launch the deep-research agent to survey the landscape across multiple sources."
18
- <commentary>
19
- Broad survey requiring many sources, comparison, and synthesis — deep-research with exhaustive depth.
20
- </commentary>
21
- </example>
22
-
3
+ description: "Use this agent for iterative, multi-source deep research that produces comprehensive Obsidian reports with full citations. Official-docs-first methodology with anti-hallucination constraints. Examples:\n\n <example>\n Context: User wants thorough research on a technical topic\n user: \"Research the current state of WebSocket authentication best practices\"\n assistant: \"I'll use the deep-research agent to conduct iterative multi-source research and produce a cited report.\"\n <commentary>\n Multi-source research requiring iteration and synthesis — exactly what deep-research handles.\n </commentary>\n </example>\n\n <example>\n Context: User needs a landscape survey with citations\n user: \"Compare the major vector database options for production RAG systems in 2026\"\n assistant: \"I'll launch the deep-research agent to survey the landscape across multiple sources.\"\n <commentary>\n Broad survey requiring many sources, comparison, and synthesis — deep-research with exhaustive depth.\n </commentary>\n </example>"
23
4
  color: cyan
24
5
  ---
25
6
 
@@ -31,10 +12,9 @@ You receive a `<research_brief>` from the orchestrating skill. Your job is to ex
31
12
 
32
13
  On receiving the research brief, write the state file:
33
14
 
34
- `.deep-research-state.md`:
15
+ `.deep-research-state.md` opens with a YAML frontmatter block — a three-hyphen fence line, these fields, then a closing three-hyphen fence line:
35
16
 
36
- ```markdown
37
- ---
17
+ ```yaml
38
18
  topic: "[from brief]"
39
19
  brief: "[one-line summary from brief]"
40
20
  iteration: 0
@@ -42,8 +22,11 @@ max_iterations: [from brief]
42
22
  status: researching
43
23
  source_count: 0
44
24
  official_docs_found: false
45
- ---
25
+ ```
26
+
27
+ The rest of the file holds the tracking sections:
46
28
 
29
+ ```markdown
47
30
  ## Sources Found
48
31
 
49
32
  (none yet)
@@ -1,32 +1,6 @@
1
1
  ---
2
2
  name: docs-agent
3
- description: Use this agent for all documentation tasks — managing/consolidating project docs, analyzing existing documentation to prevent code duplication, and writing user-facing guides for non-technical audiences. Trigger with requests like 'update our docs', 'check if we already have this documented', 'write a user guide', or 'consolidate documentation'.
4
-
5
- Examples:
6
- - <example>
7
- Context: User wants comprehensive documentation management
8
- user: "call the doc manager"
9
- assistant: "I'll use the docs-agent to analyze and update the documentation"
10
- <commentary>
11
- Documentation management request — use docs-agent in orchestration mode.
12
- </commentary>
13
- </example>
14
- - <example>
15
- Context: User is implementing a new feature and wants to avoid duplicating existing functionality
16
- user: "I need to add a function that validates user input"
17
- assistant: "Let me consult the docs-agent to check if we already have validation methods documented"
18
- <commentary>
19
- Before implementing new functionality, use docs-agent in analysis mode to check for existing documented methods.
20
- </commentary>
21
- </example>
22
- - <example>
23
- Context: User needs documentation for non-technical staff
24
- user: "Write documentation for the new export feature so our office staff can use it"
25
- assistant: "I'll use the docs-agent to create clear, step-by-step documentation that anyone can follow"
26
- <commentary>
27
- Non-technical audience — use docs-agent in user-docs writing mode.
28
- </commentary>
29
- </example>
3
+ description: "Use this agent for all documentation tasks — managing/consolidating project docs, analyzing existing documentation to prevent code duplication, and writing user-facing guides for non-technical audiences. Trigger with requests like 'update our docs', 'check if we already have this documented', 'write a user guide', or 'consolidate documentation'.\n\nExamples:\n- <example>\n Context: User wants comprehensive documentation management\n user: \"call the doc manager\"\n assistant: \"I'll use the docs-agent to analyze and update the documentation\"\n <commentary>\n Documentation management request — use docs-agent in orchestration mode.\n </commentary>\n</example>\n- <example>\n Context: User is implementing a new feature and wants to avoid duplicating existing functionality\n user: \"I need to add a function that validates user input\"\n assistant: \"Let me consult the docs-agent to check if we already have validation methods documented\"\n <commentary>\n Before implementing new functionality, use docs-agent in analysis mode to check for existing documented methods.\n </commentary>\n</example>\n- <example>\n Context: User needs documentation for non-technical staff\n user: \"Write documentation for the new export feature so our office staff can use it\"\n assistant: \"I'll use the docs-agent to create clear, step-by-step documentation that anyone can follow\"\n <commentary>\n Non-technical audience — use docs-agent in user-docs writing mode.\n </commentary>\n</example>"
30
4
  color: cyan
31
5
  ---
32
6
 
@@ -1,12 +1,6 @@
1
1
  ---
2
2
  name: issue-tracker
3
- description: >-
4
- Primary handler for one GitHub issue action per spawn on a work-stream:
5
- open an epic, file a sub-issue, update status in place, refresh the epic
6
- checklist, or close a sub-issue. Spawn with one action plus the issue-candidate
7
- or issue number it needs; loads the issue-tracker skill; returns affected
8
- issue numbers and URLs. Prefer the same warm agent for follow-ups on the same
9
- issue or the same epic work-stream.
3
+ description: "Primary handler for one GitHub issue action per spawn on a work-stream: open an epic, file a sub-issue, update status in place, refresh the epic checklist, or close a sub-issue. Spawn with one action plus the issue-candidate or issue number it needs; loads the issue-tracker skill; returns affected issue numbers and URLs. Prefer the same warm agent for follow-ups on the same issue or the same epic work-stream."
10
4
  tools: Read, Bash, Skill, mcp__github__search_issues, mcp__github__issue_read, mcp__github__issue_write, mcp__github__sub_issue_write, mcp__github__add_issue_comment
11
5
  color: green
12
6
  ---
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: skill-writer-agent
3
- description: >-
4
- Authors a skill package — SKILL.md plus companion files — to skill-builder conventions: hub layout, trigger-catalog description, deterministic work shipped as scripts and task-seed lists. Triggers: author a skill, write a SKILL.md, skill-writer-agent, delegate skill file authoring, produce skill package files.
3
+ description: "Authors a skill package — SKILL.md plus companion files — to skill-builder conventions: hub layout, trigger-catalog description, deterministic work shipped as scripts and task-seed lists. Triggers: author a skill, write a SKILL.md, skill-writer-agent, delegate skill file authoring, produce skill package files."
5
4
  tools: Read, Write, Edit, Grep, Glob
6
5
  color: green
7
6
  ---
@@ -16,6 +16,53 @@ model on every spawn, so no agent definition names one, concrete or
16
16
  flag: model: inherit <- caller can no longer choose the model
17
17
  flag: model: opus <- pinned concrete model, caller can't override
18
18
 
19
+ Every block must also load through `yaml.safe_load`. An unquoted colon inside
20
+ a plain scalar reads as a mapping key and makes the whole block unloadable::
21
+
22
+ ok: description: ... constraints. Examples are below.
23
+ flag: description: ... constraints. Examples: <- block no longer loads
24
+
25
+ Two parsers read these files, and a block that satisfies one can still be
26
+ unreadable by the other. `scripts/codex_compat_materializer.py` reads the
27
+ block line by line, so every field fits on the line that names it::
28
+
29
+ ok: description: "Use this agent ... Examples:\\n\\n <example> ..."
30
+ flag: description: | <- the line scan cannot follow the block scalar
31
+
32
+ That reader also counts fence lines across the whole file, so a bare `---`
33
+ anywhere in the body reads as a second frontmatter fence::
34
+
35
+ ok: ```yaml ... ``` example blocks in the body
36
+ flag: a `---` line inside a body example <- counted as a fence
37
+
38
+ That reader also cannot express an empty `tools: []` list, which is a defect
39
+ in the reader rather than in the one definition that declares it, so that
40
+ definition is named in `_codex_materializable_paths` and covered by every
41
+ other check here.
42
+
43
+ Each definition also has to carry a `name` and a `description` bound to a
44
+ non-empty string, and a `name` equal to its file stem — a mapping that loads
45
+ but binds `description` to nothing, or names an agent the file does not,
46
+ registers a subagent the caller cannot spawn::
47
+
48
+ ok: docs-agent.md -> name: docs-agent
49
+ flag: docs-agent.md -> name: doc-manager <- wrong spawn id
50
+ flag: description: <- loads as None, loader needs text
51
+
52
+ Every check above is parametrized over the definitions that yield a
53
+ frontmatter block, so a file that yields none would drop out of all of them
54
+ and leave the suite green while unreadable. The block is what the fence lines
55
+ delimit, so the file that opens no fence or never closes one is exactly the
56
+ broken file these checks exist to catch::
57
+
58
+ ok: docs-agent.md -> --- name/description --- <- block found
59
+ flag: docs-agent.md -> --- name/description <- no closing fence,
60
+ silently uncovered
61
+
62
+ `test_every_agent_definition_yields_a_frontmatter_block` holds that floor: it
63
+ walks the same directory the parametrized checks draw from and fails on any
64
+ definition missing from them.
65
+
19
66
  Frontmatter parsing is self-contained here (stdlib only): the block is the
20
67
  text between the file's opening and closing `---` fence lines, and top-level
21
68
  keys are read with a line scan so an agent whose `description` embeds
@@ -24,18 +71,27 @@ informal `<example>` prose is not mistaken for one carrying extra keys.
24
71
 
25
72
  from __future__ import annotations
26
73
 
74
+ import importlib.util
27
75
  import re
76
+ import sys
28
77
  from functools import cache
29
78
  from pathlib import Path
79
+ from types import ModuleType
30
80
 
31
81
  import pytest
32
82
  import yaml
33
83
 
34
84
  ACCEPTED_FRONTMATTER_KEYS = frozenset({"name", "description", "tools", "color"})
35
- CODE_VERIFIER_AGENT_NAME = "code-verifier"
85
+ EMPTY_TOOLS_LIST_FILENAME = "code-advisor.md"
36
86
  EXEMPT_MARKDOWN_FILENAME = "CLAUDE.md"
37
87
  FRONTMATTER_FENCE_LINE = "---"
88
+ MATERIALIZER_MODULE_NAME = "codex_compat_materializer"
89
+ MATERIALIZER_MODULE_PATH = (
90
+ Path(__file__).resolve().parent.parent / "scripts" / f"{MATERIALIZER_MODULE_NAME}.py"
91
+ )
38
92
  MODEL_KEY_NAME = "model"
93
+ RENAMED_AGENT_NAME = "an-agent-name-no-file-carries"
94
+ REQUIRED_FRONTMATTER_FIELDS = ("name", "description")
39
95
  TOP_LEVEL_KEY_PATTERN = re.compile(r"^([a-z][a-z0-9_]*):", re.MULTILINE)
40
96
 
41
97
 
@@ -60,14 +116,30 @@ def _extract_frontmatter_block(markdown_text: str) -> str | None:
60
116
 
61
117
 
62
118
  @cache
63
- def _agent_definition_paths() -> tuple[Path, ...]:
119
+ def _agent_definition_candidate_paths() -> tuple[Path, ...]:
120
+ """Return every markdown file in this directory that must be a definition.
121
+
122
+ This is the floor the parametrized checks are measured against: each of
123
+ these files is expected to yield a frontmatter block, so one that does not
124
+ is a broken definition rather than a file to pass over.
125
+
126
+ Returns:
127
+ Every `*.md` path in this directory except the exempt one, sorted.
128
+ """
64
129
  agents_directory = Path(__file__).parent
65
- all_markdown_files = sorted(agents_directory.glob("*.md"))
66
130
  return tuple(
67
131
  each_markdown_file
68
- for each_markdown_file in all_markdown_files
132
+ for each_markdown_file in sorted(agents_directory.glob("*.md"))
69
133
  if each_markdown_file.name != EXEMPT_MARKDOWN_FILENAME
70
- and _extract_frontmatter_block(each_markdown_file.read_text(encoding="utf-8"))
134
+ )
135
+
136
+
137
+ @cache
138
+ def _agent_definition_paths() -> tuple[Path, ...]:
139
+ return tuple(
140
+ each_markdown_file
141
+ for each_markdown_file in _agent_definition_candidate_paths()
142
+ if _extract_frontmatter_block(each_markdown_file.read_text(encoding="utf-8"))
71
143
  is not None
72
144
  )
73
145
 
@@ -84,6 +156,124 @@ def _top_level_keys(frontmatter_block: str) -> set[str]:
84
156
  return set(TOP_LEVEL_KEY_PATTERN.findall(frontmatter_block))
85
157
 
86
158
 
159
+ @cache
160
+ def _codex_materializable_paths() -> tuple[Path, ...]:
161
+ """Return the definitions the Codex materializer is expected to read.
162
+
163
+ `code-advisor.md` declares `tools: []`, and its body states the agent has
164
+ zero tools, so the empty list is the field saying what the author meant.
165
+ The materializer's list parser raises on an empty list, so it cannot
166
+ express a correct declaration::
167
+
168
+ tools: Read, Bash -> ok: parsed
169
+ tools: [] -> flag: MaterializerError, though the file is right
170
+
171
+ The gap belongs to that parser, so the definition stays as written and only
172
+ this one check steps around it. The YAML-load, required-field, accepted-key,
173
+ model-ban, and name checks all still cover the file.
174
+
175
+ Returns:
176
+ Every agent definition path except the one whose correct frontmatter
177
+ the materializer's list parser cannot express.
178
+ """
179
+ return tuple(
180
+ each_path
181
+ for each_path in _agent_definition_paths()
182
+ if each_path.name != EMPTY_TOOLS_LIST_FILENAME
183
+ )
184
+
185
+
186
+ @cache
187
+ def _codex_materializer_module() -> ModuleType:
188
+ """Load the package's own line-oriented frontmatter parser from disk.
189
+
190
+ Returns:
191
+ The imported `codex_compat_materializer` module.
192
+ """
193
+ module_specification = importlib.util.spec_from_file_location(
194
+ MATERIALIZER_MODULE_NAME, MATERIALIZER_MODULE_PATH
195
+ )
196
+ assert module_specification is not None, (
197
+ f"no import specification for {MATERIALIZER_MODULE_PATH}"
198
+ )
199
+ assert module_specification.loader is not None, (
200
+ f"no import loader for {MATERIALIZER_MODULE_PATH}"
201
+ )
202
+ materializer_module = importlib.util.module_from_spec(module_specification)
203
+ sys.modules.setdefault(MATERIALIZER_MODULE_NAME, materializer_module)
204
+ module_specification.loader.exec_module(materializer_module)
205
+ return materializer_module
206
+
207
+
208
+ def _required_field_problem(parsed_frontmatter: object) -> str | None:
209
+ """Describe what stops the loader using this frontmatter, or None.
210
+
211
+ The loader needs text for every required field, so a field that loads as
212
+ nothing or as a nested mapping is a problem even though the block itself
213
+ is valid YAML::
214
+
215
+ description: Use this agent ... -> ok: None
216
+ description: -> flag: bound to NoneType
217
+ description: -> flag: bound to dict
218
+ nested: value
219
+
220
+ Args:
221
+ parsed_frontmatter: Value `yaml.safe_load` produced for the block.
222
+
223
+ Returns:
224
+ A sentence naming the problem, or None when every required field is a
225
+ non-empty string.
226
+ """
227
+ if not isinstance(parsed_frontmatter, dict):
228
+ return (
229
+ f"frontmatter loads as {type(parsed_frontmatter).__name__} rather "
230
+ "than a key/value mapping"
231
+ )
232
+ for each_field in REQUIRED_FRONTMATTER_FIELDS:
233
+ if each_field not in parsed_frontmatter:
234
+ return f"frontmatter carries no {each_field} field"
235
+ field_value = parsed_frontmatter[each_field]
236
+ if not isinstance(field_value, str):
237
+ return (
238
+ f"frontmatter binds {each_field} to "
239
+ f"{type(field_value).__name__} rather than a string"
240
+ )
241
+ if not field_value.strip():
242
+ return f"frontmatter binds {each_field} to an empty string"
243
+ return None
244
+
245
+
246
+ def _agent_name_problem(parsed_frontmatter: object, expected_name: str) -> str | None:
247
+ """Describe a name that disagrees with the file stem, or None.
248
+
249
+ A subagent registers under the name in its frontmatter, so a name that is
250
+ not the file stem is spawned by an id no caller uses::
251
+
252
+ docs-agent.md -> name: docs-agent -> ok: None
253
+ docs-agent.md -> name: doc-manager -> flag: wrong spawn id
254
+
255
+ Args:
256
+ parsed_frontmatter: Value `yaml.safe_load` produced for the block.
257
+ expected_name: The definition file's stem.
258
+
259
+ Returns:
260
+ A sentence naming the problem, or None when the name matches the stem.
261
+ """
262
+ if not isinstance(parsed_frontmatter, dict):
263
+ return (
264
+ f"frontmatter loads as {type(parsed_frontmatter).__name__} rather "
265
+ "than a key/value mapping"
266
+ )
267
+ declared_name = parsed_frontmatter.get("name")
268
+ if declared_name == expected_name:
269
+ return None
270
+ return (
271
+ f"frontmatter declares name {declared_name!r} rather than "
272
+ f"{expected_name!r}, so the agent registers under a spawn id that does "
273
+ "not match its file"
274
+ )
275
+
276
+
87
277
  @pytest.mark.parametrize(
88
278
  "agent_definition_path",
89
279
  _agent_definition_paths(),
@@ -100,14 +290,121 @@ def test_agent_frontmatter_uses_only_accepted_keys(
100
290
  )
101
291
 
102
292
 
103
- def test_code_verifier_frontmatter_parses_and_names_the_agent() -> None:
104
- agents_directory = Path(__file__).parent
105
- code_verifier_block = _frontmatter_block(
106
- agents_directory / f"{CODE_VERIFIER_AGENT_NAME}.md"
293
+ @pytest.mark.parametrize(
294
+ "agent_definition_path",
295
+ _agent_definition_paths(),
296
+ ids=lambda each_path: each_path.name,
297
+ )
298
+ def test_agent_frontmatter_loads_as_a_yaml_mapping(
299
+ agent_definition_path: Path,
300
+ ) -> None:
301
+ frontmatter_block = _frontmatter_block(agent_definition_path)
302
+ try:
303
+ parsed_frontmatter = yaml.safe_load(frontmatter_block)
304
+ except yaml.YAMLError as yaml_error:
305
+ pytest.fail(
306
+ f"{agent_definition_path.name} frontmatter is not loadable YAML, so "
307
+ f"the subagent loader cannot read the definition: {yaml_error}"
308
+ )
309
+ assert isinstance(parsed_frontmatter, dict), (
310
+ f"{agent_definition_path.name} frontmatter loads as "
311
+ f"{type(parsed_frontmatter).__name__} rather than a key/value mapping"
312
+ )
313
+ unaccepted_keys = set(parsed_frontmatter) - ACCEPTED_FRONTMATTER_KEYS
314
+ assert not unaccepted_keys, (
315
+ f"{agent_definition_path.name} loads frontmatter keys the subagent "
316
+ f"loader does not accept: {sorted(unaccepted_keys)}"
317
+ )
318
+
319
+
320
+ def test_every_agent_definition_yields_a_frontmatter_block() -> None:
321
+ covered_names = {each_path.name for each_path in _agent_definition_paths()}
322
+ uncovered_names = sorted(
323
+ each_path.name
324
+ for each_path in _agent_definition_candidate_paths()
325
+ if each_path.name not in covered_names
326
+ )
327
+ assert not uncovered_names, (
328
+ f"{', '.join(uncovered_names)} yield no frontmatter block, so every "
329
+ "parametrized check in this module passes over them and the suite "
330
+ "stays green while the definitions are unreadable"
331
+ )
332
+
333
+
334
+ @pytest.mark.parametrize(
335
+ "agent_definition_path",
336
+ _codex_materializable_paths(),
337
+ ids=lambda each_path: each_path.name,
338
+ )
339
+ def test_agent_frontmatter_parses_with_the_codex_materializer(
340
+ agent_definition_path: Path,
341
+ ) -> None:
342
+ materializer_module = _codex_materializer_module()
343
+ try:
344
+ materializer_module.parse_frontmatter(
345
+ agent_definition_path,
346
+ agent_definition_path.read_text(encoding="utf-8"),
347
+ agent_definition_path.name,
348
+ )
349
+ except materializer_module.MaterializerError as materializer_error:
350
+ pytest.fail(
351
+ f"{agent_definition_path.name} frontmatter is unreadable by "
352
+ f"{MATERIALIZER_MODULE_NAME}, so the agent cannot be materialized "
353
+ f"for Codex: {materializer_error}"
354
+ )
355
+
356
+
357
+ @pytest.mark.parametrize(
358
+ "agent_definition_path",
359
+ _agent_definition_paths(),
360
+ ids=lambda each_path: each_path.name,
361
+ )
362
+ def test_agent_frontmatter_binds_required_fields_to_non_empty_strings(
363
+ agent_definition_path: Path,
364
+ ) -> None:
365
+ parsed_frontmatter = yaml.safe_load(_frontmatter_block(agent_definition_path))
366
+ field_problem = _required_field_problem(parsed_frontmatter)
367
+ assert field_problem is None, f"{agent_definition_path.name} {field_problem}"
368
+
369
+
370
+ def test_required_field_check_rejects_a_description_bound_to_nothing() -> None:
371
+ parsed_frontmatter = yaml.safe_load("name: docs-agent\ndescription:\n")
372
+ assert _required_field_problem(parsed_frontmatter) == (
373
+ "frontmatter binds description to NoneType rather than a string"
374
+ )
375
+
376
+
377
+ def test_required_field_check_rejects_a_description_bound_to_a_mapping() -> None:
378
+ parsed_frontmatter = yaml.safe_load(
379
+ "name: docs-agent\ndescription:\n nested: value\n"
380
+ )
381
+ assert _required_field_problem(parsed_frontmatter) == (
382
+ "frontmatter binds description to dict rather than a string"
383
+ )
384
+
385
+
386
+ @pytest.mark.parametrize(
387
+ "agent_definition_path",
388
+ _agent_definition_paths(),
389
+ ids=lambda each_path: each_path.name,
390
+ )
391
+ def test_agent_frontmatter_names_the_agent_after_its_file(
392
+ agent_definition_path: Path,
393
+ ) -> None:
394
+ parsed_frontmatter = yaml.safe_load(_frontmatter_block(agent_definition_path))
395
+ name_problem = _agent_name_problem(parsed_frontmatter, agent_definition_path.stem)
396
+ assert name_problem is None, f"{agent_definition_path.name} {name_problem}"
397
+
398
+
399
+ def test_name_check_rejects_an_agent_renamed_away_from_its_file_stem() -> None:
400
+ agent_definition_path = _agent_definition_paths()[0]
401
+ renamed_block = _frontmatter_block(agent_definition_path).replace(
402
+ f"name: {agent_definition_path.stem}", f"name: {RENAMED_AGENT_NAME}", 1
403
+ )
404
+ name_problem = _agent_name_problem(
405
+ yaml.safe_load(renamed_block), agent_definition_path.stem
107
406
  )
108
- parsed_frontmatter = yaml.safe_load(code_verifier_block)
109
- assert parsed_frontmatter["name"] == CODE_VERIFIER_AGENT_NAME
110
- assert set(parsed_frontmatter) <= ACCEPTED_FRONTMATTER_KEYS
407
+ assert name_problem is not None and RENAMED_AGENT_NAME in name_problem
111
408
 
112
409
 
113
410
  @pytest.mark.parametrize(
@@ -85,6 +85,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
85
85
  | `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 120 reader-visible words, more than 6 bullet lines, more than 2 lines carrying over 20 words each, or instruction lines telling the user to act with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged, replies under 60 words always pass, and a reply opening with `Long form:` opts out entirely |
86
86
  | `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
87
87
  | `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
88
+ | `fable_spawn_gate.py` | PreToolUse (Agent/Task) | An `Agent` or `Task` spawn whose prompt carries no `FABLE-SPAWN-AUTHORIZED` token and whose model field reads `fable` in any letter case — the bare alias, or a delimiter segment of a full model id, so `claude-fable-5` is denied too |
88
89
  | `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
89
90
  | `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
90
91
  | `gh_pr_author_restore.py` | PostToolUse | Restores PR author after a tool call |
@@ -98,6 +99,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
98
99
  | `pii_payload_scan.py` | library | Write/Edit and durable post-body PII evaluation reused by `pii_prevention_blocker.py` |
99
100
  | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
100
101
  | `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
102
+ | `piped_pytest_blocker.py` | PreToolUse (Bash) | A pytest run whose output feeds a pipe, where the pipeline reports the exit code of the command on the right |
101
103
  | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
102
104
  | `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
103
105
  | `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit`/`comment` bodies that fail the Anthropic claude-code style audit, proof-shaped `gh pr comment` bodies missing proof-of-work parts, and `gh pr ready` while the PR carries no passing proof comment |