claude-dev-env 1.75.0 → 1.77.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/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -0
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +8 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_docstrings.py +378 -0
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +48 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +174 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +176 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +267 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/blocking_check_limits.py +56 -0
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +76 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +5 -1
- package/rules/package-inventory-stale-entry.md +16 -0
- package/rules/plain-illustrative-docstrings.md +56 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/workflow/converge.contract.test.mjs +12 -19
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +71 -0
- package/skills/autoconverge/workflow/converge.mjs +86 -110
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -13,7 +13,7 @@ ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
ALL_PYTHON_EXTENSIONS = {".py"}
|
|
16
|
-
ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
|
|
16
|
+
ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".cts"}
|
|
17
17
|
ALL_CODE_EXTENSIONS = ALL_PYTHON_EXTENSIONS | ALL_JAVASCRIPT_EXTENSIONS
|
|
18
18
|
|
|
19
19
|
ALL_TEST_PATH_PATTERNS = {"test_", "_test.", ".test.", ".spec.", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
|
|
@@ -47,6 +47,41 @@ DOCSTRING_PLURAL_FAMILY_STOP_PATTERN: re.Pattern[str] = re.compile(
|
|
|
47
47
|
)
|
|
48
48
|
INLINE_CODE_TOKEN_PATTERN: re.Pattern[str] = re.compile(r"``?(\.?[A-Za-z_][A-Za-z0-9_.]*)``?")
|
|
49
49
|
IDENTIFIER_SHAPED_TUPLE_MEMBER_PATTERN: re.Pattern[str] = re.compile(r"^\.?[A-Za-z_][A-Za-z0-9_]*$")
|
|
50
|
+
ALL_CARDINAL_NUMBER_WORD_VALUES: dict[str, int] = {
|
|
51
|
+
"one": 1,
|
|
52
|
+
"two": 2,
|
|
53
|
+
"three": 3,
|
|
54
|
+
"four": 4,
|
|
55
|
+
"five": 5,
|
|
56
|
+
"six": 6,
|
|
57
|
+
"seven": 7,
|
|
58
|
+
"eight": 8,
|
|
59
|
+
"nine": 9,
|
|
60
|
+
"ten": 10,
|
|
61
|
+
"eleven": 11,
|
|
62
|
+
"twelve": 12,
|
|
63
|
+
}
|
|
64
|
+
ALL_DOCSTRING_OUTCOME_ENUMERATION_NOUNS: tuple[str, ...] = (
|
|
65
|
+
"outcome",
|
|
66
|
+
"branch",
|
|
67
|
+
"case",
|
|
68
|
+
"status",
|
|
69
|
+
"state",
|
|
70
|
+
"code",
|
|
71
|
+
"kind",
|
|
72
|
+
"variant",
|
|
73
|
+
"path",
|
|
74
|
+
"scenario",
|
|
75
|
+
)
|
|
76
|
+
DOCSTRING_CARDINAL_OUTCOME_PHRASE_PATTERN: re.Pattern[str] = re.compile(
|
|
77
|
+
r"\b(" + "|".join(ALL_CARDINAL_NUMBER_WORD_VALUES) + r")\b"
|
|
78
|
+
r"\s+(?:[A-Za-z]+\s+){0,2}"
|
|
79
|
+
r"(?:" + "|".join(ALL_DOCSTRING_OUTCOME_ENUMERATION_NOUNS) + r")(?:e?s)?\b",
|
|
80
|
+
re.IGNORECASE,
|
|
81
|
+
)
|
|
82
|
+
DOCSTRING_MULTI_SEGMENT_SNAKE_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
83
|
+
r"\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b"
|
|
84
|
+
)
|
|
50
85
|
ALL_DOCSTRING_ARGS_SECTION_HEADERS: tuple[str, ...] = ("Args:", "Arguments:")
|
|
51
86
|
ALL_DOCSTRING_TERMINATING_SECTION_HEADERS: frozenset[str] = frozenset({
|
|
52
87
|
"Returns:",
|
|
@@ -119,6 +154,46 @@ LOGGING_FSTRING_PATTERN = re.compile(
|
|
|
119
154
|
r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
|
|
120
155
|
r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
|
|
121
156
|
)
|
|
157
|
+
LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
158
|
+
r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
|
|
159
|
+
)
|
|
160
|
+
MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
|
|
161
|
+
SPAWN_AGENT_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
|
|
162
|
+
r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
|
|
163
|
+
r"(?:async\s+)?function\s+spawn(?P<role>\w+?)Agent\s*\(",
|
|
164
|
+
re.DOTALL,
|
|
165
|
+
)
|
|
166
|
+
RESUME_TASK_ENUMERATION_PATTERN: re.Pattern[str] = re.compile(
|
|
167
|
+
r"(?<![A-Za-z])resume\s*\((?P<enumeration>[^)]*?)\)",
|
|
168
|
+
re.DOTALL,
|
|
169
|
+
)
|
|
170
|
+
TASK_DISPATCH_NAME_PATTERN: re.Pattern[str] = re.compile(
|
|
171
|
+
r"""(?<![A-Za-z0-9_])task\s*===\s*['"](?P<task>[a-z0-9-]+)['"]"""
|
|
172
|
+
)
|
|
173
|
+
ENUMERATION_LIST_ITEM_SEPARATOR_PATTERN: re.Pattern[str] = re.compile(
|
|
174
|
+
r"\s*,\s*|\s+and\s+"
|
|
175
|
+
)
|
|
176
|
+
ENUMERATION_LEADING_CONJUNCTION_PATTERN: re.Pattern[str] = re.compile(
|
|
177
|
+
r"^and\s+"
|
|
178
|
+
)
|
|
179
|
+
ALL_JAVASCRIPT_STRING_DELIMITERS: frozenset[str] = frozenset({"'", '"', "`"})
|
|
180
|
+
JAVASCRIPT_STRING_ESCAPE_CHARACTER: str = "\\"
|
|
181
|
+
JAVASCRIPT_LINE_COMMENT_OPENER: str = "//"
|
|
182
|
+
JAVASCRIPT_BLOCK_COMMENT_OPENER: str = "/*"
|
|
183
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER: str = "*/"
|
|
184
|
+
JAVASCRIPT_REGEX_DELIMITER: str = "/"
|
|
185
|
+
ALL_JAVASCRIPT_REGEX_PRECEDING_CHARACTERS: frozenset[str] = frozenset(
|
|
186
|
+
{"(", ",", "=", ":", "[", "{", "}", ";", "!", "&", "|", "?", "+", "-", "*", "%", "<", ">", "~", "^", "\n"}
|
|
187
|
+
)
|
|
188
|
+
ALL_JAVASCRIPT_REGEX_PRECEDING_KEYWORDS: frozenset[str] = frozenset(
|
|
189
|
+
{"return", "typeof", "case", "in", "of", "do", "else", "void", "delete", "instanceof", "new", "yield", "await", "throw"}
|
|
190
|
+
)
|
|
191
|
+
ENUMERATION_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
192
|
+
r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
193
|
+
)
|
|
194
|
+
HYPHENATED_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
195
|
+
r"^[a-z0-9]+(?:-[a-z0-9]+)+$"
|
|
196
|
+
)
|
|
122
197
|
ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
|
|
123
198
|
"get", "items", "keys", "values", "update", "pop",
|
|
124
199
|
"setdefault", "copy", "clear",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
"""Constants for the duplicate-function-body
|
|
1
|
+
"""Constants for the duplicate-function-body scans in ``code_rules_enforcer``.
|
|
2
2
|
|
|
3
|
-
The blocking scan flags a top-level function whose body is
|
|
4
|
-
to a top-level function already defined in a sibling
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The cross-file blocking scan flags a top-level function whose body is
|
|
4
|
+
structurally identical to a top-level function already defined in a sibling
|
|
5
|
+
``.py`` module in the same directory. The same-file blocking scan flags a
|
|
6
|
+
top-level function whose body appears verbatim as a contiguous statement block
|
|
7
|
+
inside another function in the same module. Both catch the Reuse-before-create /
|
|
8
|
+
DRY violation where a block of logic is copied instead of called from one shared
|
|
9
|
+
home, so a fix that lands in one copy leaves the other carrying the bug.
|
|
7
10
|
|
|
8
11
|
The ``CROSS_SKILL_*`` and ``SKILL*`` constants feed the non-blocking companion
|
|
9
12
|
advisory: a helper copied between two skills' ``scripts`` directories, where a
|
|
@@ -12,6 +15,7 @@ skill on stderr rather than denying the write.
|
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
MINIMUM_DUPLICATE_BODY_STATEMENTS: int = 3
|
|
18
|
+
MINIMUM_INLINE_DUPLICATE_BODY_STATEMENTS: int = 1
|
|
15
19
|
MAX_DUPLICATE_BODY_ISSUES: int = 25
|
|
16
20
|
DUNDER_INIT_FILENAME: str = "__init__.py"
|
|
17
21
|
PYTHON_SOURCE_SUFFIX: str = ".py"
|
|
@@ -20,6 +24,18 @@ DUPLICATE_BODY_GUIDANCE: str = (
|
|
|
20
24
|
"extract a single shared helper (for example in hooks_constants/) and "
|
|
21
25
|
"import it from both modules instead of copying it (Reuse before create / DRY)"
|
|
22
26
|
)
|
|
27
|
+
SAME_FILE_INLINE_DUPLICATE_GUIDANCE: str = (
|
|
28
|
+
"this function body is also present inline as a contiguous statement block "
|
|
29
|
+
"inside another function in the same module; call this helper from that "
|
|
30
|
+
"function instead of repeating the block, so a single helper backs both call "
|
|
31
|
+
"sites and a fix cannot land in one copy while the other keeps the bug "
|
|
32
|
+
"(Reuse before create / DRY)"
|
|
33
|
+
)
|
|
34
|
+
SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE: str = (
|
|
35
|
+
"(inline duplicate body spans: helper at line {helper_start} spanning "
|
|
36
|
+
"{helper_length} lines, enclosing at line {enclosing_start} spanning "
|
|
37
|
+
"{enclosing_length} lines)"
|
|
38
|
+
)
|
|
23
39
|
|
|
24
40
|
SKILLS_DIRECTORY_NAME: str = "skills"
|
|
25
41
|
SKILL_SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
package/package.json
CHANGED
package/rules/CLAUDE.md
CHANGED
|
@@ -30,6 +30,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
|
|
|
30
30
|
| `orphan-css-class.md` | Every `class="..."` attribute in Python-generated markup has a matching selector in the `<style>` block |
|
|
31
31
|
| `package-inventory-stale-entry.md` | A new production code file added to a directory carries an entry in that directory's `README.md`/`CLAUDE.md` file inventory |
|
|
32
32
|
| `parallel-tools.md` | Make all independent tool calls in a single response |
|
|
33
|
+
| `plain-illustrative-docstrings.md` | Public docstring narrative reads plainly and paints a concrete scene a general developer follows on first read; a run-on backstop hook plus Category O9 audit enforce it |
|
|
33
34
|
| `plain-language.md` | Everyday words, short active sentences, lead with the answer |
|
|
34
35
|
| `prompt-workflow-context-controls.md` | Keep prompt-workflow instruction layers small and stable; load heavy skills on demand |
|
|
35
36
|
| `research-mode.md` | Three anti-hallucination constraints: say "I don't know", verify with citations, quote for factual grounding |
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
# Orphan File Reference in a Per-Directory CLAUDE.md
|
|
2
2
|
|
|
3
|
-
**When this applies:** Any Write, Edit, or MultiEdit to a file named `CLAUDE.md` that lists files in a markdown table whose first column names each file in backticks.
|
|
3
|
+
**When this applies:** Any Write, Edit, or MultiEdit to a file named `CLAUDE.md` that lists files in a markdown table whose first column names each file in backticks, or that shows run commands invoking those files inside fenced code blocks.
|
|
4
4
|
|
|
5
5
|
## Rule
|
|
6
6
|
|
|
7
|
-
Every bare filename a per-directory `CLAUDE.md`
|
|
7
|
+
Every bare filename a per-directory `CLAUDE.md` names points at a file that exists in the directory subtree the `CLAUDE.md` describes — both the filenames its table cells list and the scripts its fenced run commands invoke (`python script.py`). A table cell or a run command naming a file that exists nowhere in that subtree points a reader at something that is not there: the doc claims a file the directory does not hold.
|
|
8
8
|
|
|
9
|
-
When you add a table row, the file it names already exists in this directory or a subdirectory of it. When you remove a file, drop the row that named it.
|
|
9
|
+
When you add a table row or a run command, the file it names already exists in this directory or a subdirectory of it. When you remove a file, drop the row and the run command that named it.
|
|
10
10
|
|
|
11
11
|
## What the gate checks
|
|
12
12
|
|
|
13
13
|
The `claude_md_orphan_file_blocker.py` hook runs on every Write, Edit, and MultiEdit whose target basename is `CLAUDE.md`. It:
|
|
14
14
|
|
|
15
15
|
1. Reads the content the tool would leave on disk. For a Write that is the full `content`. For an Edit or MultiEdit it reconstructs the post-edit file — the existing on-disk file with the replacements applied — and also notes which orphans the file already held before the edit, so a pre-existing orphan on an untouched line is excluded and only an orphan the edit introduces is reported; when the existing file cannot be read, it scans the raw `new_string` fragment(s) instead.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4. Blocks the write when a named file exists nowhere under the scan root — the `CLAUDE.md` directory's parent, which covers the directory, its subdirectories, and its siblings. A filesystem error that halts the whole subtree walk fails open (the write proceeds), so an unreadable tree never blocks a write.
|
|
16
|
+
2. Collects two kinds of referenced filename. Table cells: the first column of each markdown table row **outside** a fenced code block, keeping cells that name a bare filename wrapped in backticks, no path separator, not a slash-command, ending in a known file extension (`.py`, `.md`, `.json`, `.mjs`, `.js`, `.ts`, `.ps1`, `.cmd`, `.ahk`, `.yml`, `.yaml`, `.sh`, `.txt`, `.cfg`, `.toml`, `.ini`). Run commands: each line **inside** a fenced code block (between a ``` or `~~~` fence pair) that invokes an interpreter (`python`, `python.exe`, `python3`, `node`, `pwsh`, `powershell`, `bash`, `sh`, `ruby`, `perl`) on a script, taking that script's basename when it ends in `.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`, `.rb`, or `.pl`. A fenced *table row* is an example, not a live listing, so it contributes no table-cell filename; a fenced *run command* is the contract a reader runs, so its script filename is checked.
|
|
17
|
+
3. Blocks the write when a referenced filename — from a table cell or a fenced run command — exists nowhere under the scan root — the `CLAUDE.md` directory's parent, which covers the directory, its subdirectories, and its siblings. A filesystem error that halts the whole subtree walk fails open (the write proceeds), so an unreadable tree never blocks a write.
|
|
19
18
|
|
|
20
|
-
The check stays quiet for a target that is not a `CLAUDE.md`, for a table cell that holds a path, a subdirectory ending in `/`, or a slash-command, for a table row inside a fenced code block, and for a table whose content names an explicit relative-path source (a `../` token), since that table documents files that sit outside the subtree by design.
|
|
19
|
+
The check stays quiet for a target that is not a `CLAUDE.md`, for a table cell that holds a path, a subdirectory ending in `/`, or a slash-command, for a table row inside a fenced code block, for an inline `python x.py` mention outside a fence (prose, not a runnable contract), and for a table whose content names an explicit relative-path source (a `../` token), since that table documents files that sit outside the subtree by design.
|
|
21
20
|
|
|
22
21
|
## Why this is a hook, not a lint pass
|
|
23
22
|
|
|
24
|
-
A table row that names an absent file reads as a contract: a reader trusts the listing to map the directory. A wrong row sends the reader looking for a file that is not there
|
|
23
|
+
A table row or a run command that names an absent file reads as a contract: a reader trusts the listing to map the directory and trusts the shown command to run. A wrong row sends the reader looking for a file that is not there; a stale run command fails the moment the reader runs it. Both erode trust in every other entry. Catching them as each line is written keeps the doc and the directory in step.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
|
|
8
8
|
|
|
9
|
-
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names.
|
|
9
|
+
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Six more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_fallback_branch_coverage` covers a summary that scopes a fallback to a single condition (`only when`, `falls back to ... when`) while the body routes to that same fallback call from two or more distinct early-return guards. `check_class_docstring_names_public_methods` covers a class whose docstring is a single summary line while the class exposes two or more public methods whose names the summary never spells out — the drift where a one-line class summary keeps naming its first feature after the class grows a second public entry point. `check_docstring_no_consumer_claim` covers a producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) — a transitional claim that drifts the moment a reader lands and contradicts any companion `SKILL.md` that documents the consumer; this is the deterministic slice of the O8 companion-doc producer/consumer drift below. `check_docstring_returns_plural_cardinality` covers a `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) while the returned dict literal holds exactly one key in that family (`sheen_mid`) — the drift where a single-key family carries a plural noun, so the prose claims a cardinality of two or more that the dict does not hold. `check_docstring_args_single_line_scope_vs_span` covers an `Args:` entry whose prose scopes a finding to a single named line (`only when its block-anchor line is among the changed lines`) while the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper — the drift where the Args entry claims a narrower single-line scope than the span-intersection body applies, so an edit touching any non-anchor line of the span still blocks. `check_docstring_cardinal_count_matches_constant_family` covers a docstring that states a cardinal count of an outcome family (`Covers the four outcome branches: ...`) and lists those members, while the module references more members of the same `UPPER_SNAKE` constant family than the count names (`OUTCOME_OFFENDER_UNREADABLE` is imported and exercised, yet the summary stops at four) — the drift where a summary keeps the old count after the code grows another branch; this gate runs on test modules as well as production modules. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and module-level responsibility paragraphs — has no signature, method roster, or single structural shape to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose; the audit lane below is the enforcement for everything outside the seven gated slices.
|
|
10
10
|
|
|
11
11
|
## What to check before you write the docstring
|
|
12
12
|
|
|
@@ -16,7 +16,11 @@ Read the body and the docstring side by side:
|
|
|
16
16
|
- **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
|
|
17
17
|
- **Shared fallback routes.** A summary that scopes a fallback call to one condition names every condition that reaches that call. When the body routes to the same fallback from two or more early-return guards (`if a is None: fallback(); return` and `if random() < p: fallback(); return`), the prose enumerates both guards. The `check_docstring_fallback_branch_coverage` gate blocks the single-condition form of this drift at Write/Edit time.
|
|
18
18
|
- **Step order.** A docstring that says `A then B then C` matches the call order in the body. A step enumeration that names the body's linear steps also names every corrective step the body guards inside an `if`/`elif` branch (`if not await cancel_and_reinitiate_update(...): return`). The `check_docstring_step_enumeration_dispatch_coverage` gate blocks the branch-guarded-dispatch form of this drift — a step-enumeration docstring that omits a two-or-more-token dispatch step the body guards inside a branch — at Write/Edit time.
|
|
19
|
+
- **JS/`.mjs` resume-task enumerations.** A `spawn<Role>Agent` JSDoc that enumerates its sibling `resume<Role>Agent`'s resume tasks in a parenthetical `resume (repair-verify, hardening-verify)` list names every `task === '<name>'` branch the resume body dispatches on. The `check_js_resume_task_enumeration_coverage` gate blocks the JavaScript form of this drift — a spawn JSDoc whose resume enumeration omits a dispatched task — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
|
|
19
20
|
- **Returns-clause cardinality.** A `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) matches the count of keys in that family in the returned dict literal. When the dict holds one key in the family (`sheen_mid`), the noun is singular (`the sheen stop`); a plural noun there claims two or more entries the dict does not hold. The `check_docstring_returns_plural_cardinality` gate blocks the single-key-with-plural-noun form of this drift at Write/Edit time.
|
|
21
|
+
- **Args single-line scope vs span body.** An `Args:` entry that scopes a finding to one named line (`a finding blocks only when its block-anchor line is among the changed lines`) matches the line breadth the body scopes by. When the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper that blocks when any line of the span is among the changed lines, the single-line Args wording understates the scope: an edit touching a non-anchor line of the span still blocks. State the Args entry on the same span breadth the body uses (`a finding blocks when any line of its block span is among the changed lines`). The `check_docstring_args_single_line_scope_vs_span` gate blocks the single-line-Args-over-span-body form of this drift at Write/Edit time.
|
|
22
|
+
- **Cardinal-count enumerations.** A docstring that states a count of an outcome family (`the four outcome branches`) and lists those members names every member of that family the module references. When the module imports and exercises a fifth `OUTCOME_*` constant the summary leaves out, the count and the list both under-describe the code. The `check_docstring_cardinal_count_matches_constant_family` gate blocks this drift — a cardinal-count docstring that names two or more members of a referenced `UPPER_SNAKE` constant family, leaves at least one referenced member out, and states a count below the family size — at Write/Edit time, on test modules as well as production modules.
|
|
23
|
+
- **Field meaning: run mode versus per record.** A dataclass or `TypedDict` field documented in the class `Attributes:` block states what the field means for one record. When the code sets that field the same way for every record (a run-mode flag such as `is_dry_run = not is_execute` at each write site), the description states the run-mode meaning, not a per-record outcome. A field named `is_dry_run` documented as `True when no STP was written` reads as a per-record write result, but the value tracks the run mode, so a record that writes no file during an execute run still stores `False`. State the run-mode meaning the assignment gives the field. The assignment that sets the field sits in another module, out of reach of the write-time gate, so this stays an O6 audit-lane judgment finding.
|
|
20
24
|
- **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
|
|
21
25
|
- **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags (`plain logging, screenshot, or method-on-local calls inside a branch are not dispatch steps`) keys the exclusion to the same axis the body's classification keys on. When the body decides on one axis (a call sits in an `If.test` guard versus a plain statement) but the prose excludes on a different axis (the call's receiver shape — a method on a local), the exclusion clause names a category the body still flags: a guarded method-on-local call is flagged even though the prose lists method-on-local calls as excluded. Read the body's actual branch condition, then state the exclusion on that same axis (`plain (unguarded) calls inside a branch body are not dispatch steps`), so every member the prose excludes is a member the body also excludes.
|
|
22
26
|
- **Companion-doc ordering and content claims.** A `SKILL.md` (or sibling `.md`) sentence that names a produced artifact and claims its order (`sorted`, `alphabetical`, `in sorted order`) or its content (`the at-risk names`, `just the current set`) matches the producer function's docstring and body for that same artifact. A producer that builds the artifact by merging stored names with new names and appending — preserving file order, not re-sorting the union — leaves a doc that still says `sorted` drifted on both counts: the order claim is wrong, and the content claim hides the merged-in prior entries. When the producer's ordering or union changes, the same change updates the companion doc. The two move together in one commit, even when the producer edit does not touch the `.md` file.
|
|
@@ -8,6 +8,22 @@ A package directory that documents its own files in a `README.md` Layout table o
|
|
|
8
8
|
|
|
9
9
|
When you create a new production file in such a directory, add an entry naming it — a row in the `README.md` table, a bullet in the `CLAUDE.md` list — in the same change. The entry names the file in backticks and says what it does.
|
|
10
10
|
|
|
11
|
+
## Companion: keep the Purpose/scope sentence in step with the new responsibility
|
|
12
|
+
|
|
13
|
+
The file-list entry is the deterministic slice the gate enforces. A package inventory also carries a free-prose scope sentence — a `## Purpose` paragraph in a `CLAUDE.md`, a one-line summary the parent directory's inventory gives each subdirectory — that names the responsibilities the package's modules cover. When the new module adds a responsibility the scope sentence omits, the same change broadens that sentence to name it, and updates the parent inventory's one-line summary of this subdirectory to match.
|
|
14
|
+
|
|
15
|
+
Take a `files/` package whose `Purpose` reads "Holds helpers for downloading files over HTTP and extracting zip archives" and whose parent summary reads "file download, extraction, and path config helpers". Once a `force_remove.py` module that removes a directory tree sits beside the download helpers, both sentences name a narrower responsibility set than the directory holds. The required file-list bullet alone leaves that gap open. Broaden the `Purpose` sentence to name directory removal, and broaden the parent summary to match, in the same change that adds the module and its bullet.
|
|
16
|
+
|
|
17
|
+
This scope-sentence slice is free prose: a hook cannot derive a module's responsibility from its filename, so the gate leaves it to judgment. It is the judgment companion to the file-list entry the gate enforces, and it belongs in the same change. This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape applied to a package inventory: a behavior change orphans a scope claim the prose still makes.
|
|
18
|
+
|
|
19
|
+
## Companion: keep a per-file description in step with the file it describes
|
|
20
|
+
|
|
21
|
+
The per-file entry a `CLAUDE.md` "Key files" list or a `README.md` Layout table gives each file carries more than the backticked filename the gate checks for. The clause after the file name — the em-dash description — is itself a free-prose scope claim about what the file holds. When the file gains a responsibility the description omits — a new public function, a new constant — the same change broadens the description clause to name it. The gate's file-list check passes the moment the file name appears once; it never reads the description clause, so a stale description beside a present file name stays invisible to the gate.
|
|
22
|
+
|
|
23
|
+
A constants module is the common shape of this drift. A file whose name ends `_constants.py`, or any `.py` directly inside a `config/` directory, holds a set of module-level constants, and a sibling inventory describes that file by listing the set — `` `stp_constants.py` — the STP archive member constants: the Properties.xml member name, the workspace prefix every asset reference carries, and the source-form nine-patch filename suffix ``. When the file gains a module-level constant the list omits, three claims drift together: the list itself, the scope label that heads it (`the STP archive member constants`), and the package `## Purpose` sentence when that sentence describes the file's contents. The constant's other home — the module docstring of the constants file — and the sibling inventory's description of that file cover the same set, so the clause that lands in the docstring lands in the inventory description in the same change.
|
|
24
|
+
|
|
25
|
+
This slice sits outside the gate. The gate fires on a Write that creates a new file, and it skips a file directly inside a `config/` directory, so an Edit that adds a constant to an existing `config/` constants module matches neither path. Like the Purpose/scope companion above, it is free prose a hook cannot derive from a file name, so it stays judgment here and a Category O8 finding at audit: a behavior change orphans a description claim the inventory still makes.
|
|
26
|
+
|
|
11
27
|
## What the gate checks
|
|
12
28
|
|
|
13
29
|
The `package_inventory_stale_blocker.py` hook runs on every Write whose target is a new file (a path not yet on disk). It:
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Plain, Illustrative Docstrings
|
|
2
|
+
|
|
3
|
+
**When this applies:** Any Write or Edit to a public function, method, class, or module docstring whose narrative prose — the summary and description before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section — says what the code is for or how it behaves. The standard governs the prose a reader meets first, not the structured `Args:` / `Returns:` entries below it.
|
|
4
|
+
|
|
5
|
+
## Rule
|
|
6
|
+
|
|
7
|
+
A docstring's narrative reads plainly enough that a general developer follows it on the first read. Two things make that true:
|
|
8
|
+
|
|
9
|
+
- **Illustrative.** The prose paints a concrete scene — the reader pictures the moment the code matters, the input it sees, the outcome it produces. A reader who finishes the narrative can say what breaks without it.
|
|
10
|
+
- **Brief.** The narrative makes its point in few words. Short sentences, each carrying one idea.
|
|
11
|
+
|
|
12
|
+
Two shapes break the standard. Hold the prose clear of both:
|
|
13
|
+
|
|
14
|
+
- **No machinery nouns stacked into a wall.** A sentence that chains abstract machinery terms (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`) names parts without painting a scene. Name what the reader sees and why it matters.
|
|
15
|
+
- **No defining by negation.** Prose that explains a thing by what it is not (`the non-promoter-specific machinery`) leaves the reader without a picture. Say what the thing is.
|
|
16
|
+
|
|
17
|
+
## What to check before you write the docstring
|
|
18
|
+
|
|
19
|
+
Read the narrative back as a stranger would:
|
|
20
|
+
|
|
21
|
+
- Does one sentence run long while joining clauses with an em-dash or a semicolon? That is the wall mark — break it into short sentences.
|
|
22
|
+
- Does the prose name a concrete moment, input, and outcome, or only abstract parts?
|
|
23
|
+
- Does any sentence define the thing by what it is not? Rewrite it to say what the thing is.
|
|
24
|
+
|
|
25
|
+
## Worked example
|
|
26
|
+
|
|
27
|
+
A dense wall — one long sentence, machinery nouns, a term defined by negation:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Owns the SIGINT install/restore/installability check, the atexit terminal-record
|
|
31
|
+
registration, and the interrupted-run finalizer — the non-promoter-specific
|
|
32
|
+
machinery that brackets a run so the JSONL artifact always carries a terminal
|
|
33
|
+
record and an in-flight theme record on interrupt.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The same contract, plain and illustrative:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Make sure a run's log always records how it ended.
|
|
40
|
+
|
|
41
|
+
So when you reopen the report, the last line tells you the truth: the run
|
|
42
|
+
finished cleanly, or you hit Ctrl-C while theme 42 was processing, or it died
|
|
43
|
+
on an unexpected error. Without this, a killed run looks identical to a clean
|
|
44
|
+
one — and you're debugging blind.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Enforcement
|
|
48
|
+
|
|
49
|
+
Two surfaces carry this standard:
|
|
50
|
+
|
|
51
|
+
- **Hook (the run-on backstop).** `check_docstring_runon_sentence` in `packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py` flags the one mechanical mark of a wall: a single narrative sentence that is both over the word limit and joined by an em-dash or a semicolon. A hook cannot judge whether prose paints a picture, so it catches only this structural mark.
|
|
52
|
+
- **Audit (the judgment lane).** Category O sub-bucket O9 in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` carries the illustrative-and-brief judgment the hook cannot. The audit teammate reads each changed docstring's narrative and asks whether a general developer follows it on the first read.
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
A docstring earns its place by saving the reader a trip into the body. A wall of stacked machinery nouns costs more to read than the code it describes, so the reader skips it and the docstring becomes dead weight. Prose that paints a concrete scene — the moment, the input, the outcome — lets a reader reason about the code without reading it. Naming this standard makes the wall a finding at write time and at audit, rather than a slow defect a reader meets months later.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Trigger:** `/anthropic-plan`, `/plan`, "plan this first", "think before coding", "make a plan", "scope this out", "don't code yet", and non-trivial requests that need source-grounded design before build work.
|
|
4
4
|
|
|
5
|
-
Creates a repo-local plan packet under `docs/plans/<slug>/` by running the `plan-packet.mjs` workflow. The packet holds context, spec, implementation steps, validation, and a handoff prompt for the build agent. The skill stops before any production code changes.
|
|
5
|
+
Creates a repo-local plan packet under `docs/plans/<slug>/` by running the `plan-packet.mjs` workflow. The skill first drafts a short starting plan and gets the user's approval in plan mode (`EnterPlanMode` / `ExitPlanMode`); on approval it runs the workflow. The packet holds context, spec, implementation steps, validation, and a handoff prompt for the build agent. The skill stops before any production code changes.
|
|
6
6
|
|
|
7
7
|
## Subdirectories
|
|
8
8
|
|
|
@@ -5,9 +5,20 @@ description: Workflow-backed implementation planning that creates a deep repo-lo
|
|
|
5
5
|
|
|
6
6
|
# Anthropic Plan
|
|
7
7
|
|
|
8
|
-
Create a source-grounded plan packet through the Claude Code Workflow runtime.
|
|
8
|
+
Create a source-grounded plan packet through the Claude Code Workflow runtime. First draft a short starting plan and get it approved in plan mode; then the workflow builds the full repo-local `docs/plans/<slug>/` packet — context, spec, implementation, validation, and handoff docs. Stop before implementation.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Plan first (plan mode)
|
|
11
|
+
|
|
12
|
+
Before any worktree or workflow, build a short starting plan in plan mode and get the user's approval. This is a cheap gate: it confirms the direction before the workflow spends tokens building the full packet.
|
|
13
|
+
|
|
14
|
+
1. Enter plan mode with `EnterPlanMode` if the session is not already in it.
|
|
15
|
+
2. Read the handful of files closest to the request — the entry points, the modules the change touches, the tests that cover them — enough to ground the approach in real source.
|
|
16
|
+
3. Write a short plan: the goal in a sentence or two, the approach, the files the build will touch, and the main risks or open choices. Use `AskUserQuestion` for any product choice that local context cannot settle.
|
|
17
|
+
4. Call `ExitPlanMode` to hand the plan to the user for approval.
|
|
18
|
+
|
|
19
|
+
On approval, fold the approved scope and direction into the `task` payload at launch, then continue with the steps below. If the user revises the plan, fold the changes in and present it again. Do not isolate a worktree or launch the workflow until the starting plan is approved.
|
|
20
|
+
|
|
21
|
+
## Isolate the session
|
|
11
22
|
|
|
12
23
|
The workflow's background subagents write the packet into the working tree. A background session that has not isolated into a worktree cannot write a shared checkout — the background-isolation guard rejects the write. So put the session in a worktree before launching the workflow:
|
|
13
24
|
|
|
@@ -27,6 +38,8 @@ Workflow({
|
|
|
27
38
|
})
|
|
28
39
|
```
|
|
29
40
|
|
|
41
|
+
The `task` string carries the user request together with the approved starting plan, so the packet reflects the direction the user signed off on.
|
|
42
|
+
|
|
30
43
|
If the Workflow tool is unavailable, say `anthropic-plan requires the Workflow tool; aborting` and stop.
|
|
31
44
|
|
|
32
45
|
## Self-healing writes
|
|
@@ -294,11 +294,11 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
|
|
|
294
294
|
}
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
test('resumeFixerAgent
|
|
297
|
+
test('resumeFixerAgent never verifies its own session — verification belongs to the separate verifier', () => {
|
|
298
298
|
const fixerBody = lensPromptBody('resumeFixerAgent');
|
|
299
|
-
assert.
|
|
300
|
-
assert.
|
|
301
|
-
assert.match(fixerBody, /
|
|
299
|
+
assert.doesNotMatch(fixerBody, /buildVerdictFenceSteps\(/, 'expected the fixer to not emit a verdict fence — the separate verifier does');
|
|
300
|
+
assert.doesNotMatch(fixerBody, /agentType:\s*'code-verifier'/, 'expected the fixer session to be clean-coder only');
|
|
301
|
+
assert.match(fixerBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder for its commit and recovery edits');
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
|
|
@@ -315,15 +315,14 @@ test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening bra
|
|
|
315
315
|
);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
-
test('resumeVerifierAgent
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
318
|
+
test('resumeVerifierAgent passes PR coordinates to buildVerdictFenceSteps for the fix-verify and repair-verify tasks', () => {
|
|
319
|
+
const verifyBody = lensPromptBody('resumeVerifierAgent');
|
|
320
|
+
assert.match(verifyBody, /task === 'fix-verify'/, 'expected resumeVerifierAgent to carry the fix-path verify task');
|
|
321
|
+
assert.match(
|
|
322
|
+
verifyBody,
|
|
323
|
+
/buildVerdictFenceSteps\(input\.owner, input\.repo, input\.prNumber\)/,
|
|
324
|
+
'expected resumeVerifierAgent to pass PR coordinates to buildVerdictFenceSteps',
|
|
325
|
+
);
|
|
327
326
|
});
|
|
328
327
|
|
|
329
328
|
test('the commit path in resumeFixerAgent forbids further edits and uses clean-coder', () => {
|
|
@@ -663,7 +662,6 @@ const newSpawnResumeHelpers = [
|
|
|
663
662
|
{ name: 'resumeGeneralUtilityAgent', isAsync: false },
|
|
664
663
|
{ name: 'spawnConvergenceCheckAgent', isAsync: true },
|
|
665
664
|
{ name: 'resumeConvergenceCheckAgent', isAsync: false },
|
|
666
|
-
{ name: 'extractVerdict', isAsync: false },
|
|
667
665
|
];
|
|
668
666
|
|
|
669
667
|
for (const { name, isAsync } of newSpawnResumeHelpers) {
|
|
@@ -824,8 +822,3 @@ test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
|
824
822
|
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
825
823
|
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
826
824
|
});
|
|
827
|
-
|
|
828
|
-
test('extractVerdict calls parseLastVerdictFence', () => {
|
|
829
|
-
const verdictBody = lensPromptBody('extractVerdict');
|
|
830
|
-
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected extractVerdict to call the shared parser');
|
|
831
|
-
});
|
|
@@ -167,6 +167,77 @@ test('applyFixes routes through spawnFixerAgent and fixerWithRecovery', () => {
|
|
|
167
167
|
assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
test('applyFixes spawns a separate verifier and passes both ids into fixerWithRecovery', () => {
|
|
171
|
+
const applyFixesBody = functionSource('applyFixes');
|
|
172
|
+
assert.match(
|
|
173
|
+
applyFixesBody,
|
|
174
|
+
/spawnVerifierAgent\(/,
|
|
175
|
+
'expected applyFixes to spawn a separate verifier agent so the fix-path verdict is independent of the fixer',
|
|
176
|
+
);
|
|
177
|
+
const fixerSpawnIndex = applyFixesBody.search(/spawnFixerAgent\(/);
|
|
178
|
+
const verifierSpawnIndex = applyFixesBody.search(/spawnVerifierAgent\(/);
|
|
179
|
+
assert.notEqual(fixerSpawnIndex, -1, 'expected applyFixes to spawn the fixer');
|
|
180
|
+
assert.notEqual(verifierSpawnIndex, -1, 'expected applyFixes to spawn the verifier');
|
|
181
|
+
assert.match(
|
|
182
|
+
applyFixesBody,
|
|
183
|
+
/fixerWithRecovery\(\s*fixerAgentId,\s*verifierId,/,
|
|
184
|
+
'expected applyFixes to pass both the fixer and verifier ids into fixerWithRecovery',
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('fixerWithRecovery runs verify on the verifier id and edits/commits on the fixer id', () => {
|
|
189
|
+
const recoveryBody = functionSource('fixerWithRecovery');
|
|
190
|
+
assert.match(
|
|
191
|
+
recoveryBody,
|
|
192
|
+
/resumeVerifierAgent\(\s*verifierId,/,
|
|
193
|
+
'expected fixerWithRecovery to resume the separate verifier for the verify step',
|
|
194
|
+
);
|
|
195
|
+
assert.match(
|
|
196
|
+
recoveryBody,
|
|
197
|
+
/resumeFixerAgent\(\s*fixerAgentId,\s*'commit'/,
|
|
198
|
+
'expected fixerWithRecovery to resume the fixer for the commit step',
|
|
199
|
+
);
|
|
200
|
+
assert.doesNotMatch(
|
|
201
|
+
recoveryBody,
|
|
202
|
+
/resumeFixerAgent\(\s*fixerAgentId,\s*'(?:verify-commit|fix-verify)'/,
|
|
203
|
+
'expected the verify step never to resume the fixer session — the verifier must grade a different session than the one that edits',
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('the fix-path verify resumes a different agent id than the fix-path edits, mirroring the repair path', () => {
|
|
208
|
+
const recoveryBody = functionSource('fixerWithRecovery');
|
|
209
|
+
const verifyResumeMatch = /resumeVerifierAgent\(\s*(\w+),/.exec(recoveryBody);
|
|
210
|
+
const editResumeMatch = /resumeFixerAgent\(\s*(\w+),/.exec(recoveryBody);
|
|
211
|
+
assert.ok(verifyResumeMatch, 'expected a verify resume call naming its agent id');
|
|
212
|
+
assert.ok(editResumeMatch, 'expected an edit/commit resume call naming its agent id');
|
|
213
|
+
assert.notEqual(
|
|
214
|
+
verifyResumeMatch[1],
|
|
215
|
+
editResumeMatch[1],
|
|
216
|
+
'expected the verify step to resume a different agent id than the edit/commit step',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('the verifier carries a fix-verify task so the fix path verdict comes from the verifier group', () => {
|
|
221
|
+
const verifierBody = functionSource('resumeVerifierAgent');
|
|
222
|
+
assert.match(verifierBody, /task === 'fix-verify'/, 'expected resumeVerifierAgent to handle the fix-verify task');
|
|
223
|
+
assert.match(verifierBody, /buildVerdictFenceSteps\(/, 'expected the fix-verify task to emit a verdict fence');
|
|
224
|
+
assert.match(verifierBody, /agentType:\s*'code-verifier'/, 'expected the fix-verify task to run as code-verifier');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('resumeFixerAgent no longer verifies its own edits — no code-verifier verify-commit task remains', () => {
|
|
228
|
+
const fixerBody = functionSource('resumeFixerAgent');
|
|
229
|
+
assert.doesNotMatch(
|
|
230
|
+
fixerBody,
|
|
231
|
+
/task === 'verify-commit'/,
|
|
232
|
+
'expected the fixer to no longer carry the verify-commit task that graded its own session',
|
|
233
|
+
);
|
|
234
|
+
assert.doesNotMatch(
|
|
235
|
+
fixerBody,
|
|
236
|
+
/agentType:\s*'code-verifier'/,
|
|
237
|
+
'expected the fixer session to be clean-coder only — a separate verifier grades the working tree',
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
170
241
|
test('repairConvergence routes its commit through commitWithRecovery wired to the repair-path steps', () => {
|
|
171
242
|
const repairBody = functionSource('repairConvergence');
|
|
172
243
|
assert.match(repairBody, /commitWithRecovery\(/, 'expected repairConvergence to call commitWithRecovery');
|