claude-dev-env 2.7.1 → 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.
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +1 -1
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/spawn_grok_batch.py +38 -1
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +101 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- 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–
|
|
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
|
|
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–
|
|
34
|
+
## Bug Categories A–Q
|
|
35
35
|
|
|
36
|
-
Every audit pass walks all
|
|
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
|
|
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
|
|
package/agents/deep-research.md
CHANGED
|
@@ -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
|
-
```
|
|
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)
|
package/agents/docs-agent.md
CHANGED
|
@@ -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
|
|
package/agents/issue-tracker.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
132
|
+
for each_markdown_file in sorted(agents_directory.glob("*.md"))
|
|
69
133
|
if each_markdown_file.name != EXEMPT_MARKDOWN_FILENAME
|
|
70
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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(
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -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 |
|