claude-dev-env 1.78.0 → 1.80.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 (63) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
  6. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  7. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  8. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  9. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
  10. package/bin/install.mjs +1 -0
  11. package/bin/install.test.mjs +3 -2
  12. package/hooks/blocking/CLAUDE.md +3 -2
  13. package/hooks/blocking/code_rules_docstrings.py +609 -16
  14. package/hooks/blocking/code_rules_enforcer.py +41 -0
  15. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  16. package/hooks/blocking/code_rules_naming_collection.py +76 -1
  17. package/hooks/blocking/code_rules_paired_test.py +240 -22
  18. package/hooks/blocking/code_rules_test_assertions.py +159 -1
  19. package/hooks/blocking/code_rules_unused_imports.py +7 -63
  20. package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
  21. package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
  25. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
  26. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  27. package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
  28. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
  29. package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
  31. package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
  32. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  33. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
  34. package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
  36. package/hooks/hooks_constants/CLAUDE.md +1 -0
  37. package/hooks/hooks_constants/blocking_check_limits.py +79 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
  39. package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
  40. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
  41. package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  44. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/CLAUDE.md +1 -0
  47. package/rules/docstring-prose-matches-implementation.md +57 -54
  48. package/rules/env-var-table-code-drift.md +24 -0
  49. package/rules/package-inventory-stale-entry.md +4 -4
  50. package/rules/paired-test-coverage.md +12 -5
  51. package/skills/autoconverge/SKILL.md +26 -5
  52. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  53. package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
  54. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
  55. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  56. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  57. package/skills/autoconverge/workflow/converge.mjs +110 -228
  58. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  59. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  60. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  61. package/skills/pr-converge/SKILL.md +28 -2
  62. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  63. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -0,0 +1,93 @@
1
+ """Behavior tests for the JS returns-object schema-less detection patterns.
2
+
3
+ These regexes drive ``check_js_returns_object_schemaless_branch``: they locate a
4
+ JSDoc-documented ``function`` declaration, decide whether its ``@returns`` clause
5
+ promises a ``Promise<object>``, find each ``return`` that calls a helper, and spot
6
+ a ``schema`` key inside an options object. Each test pins one pattern against the
7
+ shapes it must accept and the near-misses it must reject.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
16
+ if str(_HOOKS_ROOT) not in sys.path:
17
+ sys.path.insert(0, str(_HOOKS_ROOT))
18
+
19
+ from hooks_constants.code_rules_enforcer_constants import (
20
+ FUNCTION_WITH_JSDOC_PATTERN,
21
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
22
+ RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
23
+ SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN,
24
+ )
25
+
26
+
27
+ def test_function_with_jsdoc_pattern_captures_name_and_jsdoc() -> None:
28
+ source = (
29
+ "/**\n * @returns {Promise<object>} the structured output\n */\n"
30
+ "function runGitTask(task, head) {\n"
31
+ )
32
+ match = FUNCTION_WITH_JSDOC_PATTERN.search(source)
33
+ assert match is not None
34
+ assert match.group("name") == "runGitTask"
35
+ assert "@returns" in match.group("jsdoc")
36
+
37
+
38
+ def test_function_with_jsdoc_pattern_captures_async_declaration() -> None:
39
+ source = "/** @returns {Promise<object>} */\nasync function fixerWithRecovery(head) {\n"
40
+ match = FUNCTION_WITH_JSDOC_PATTERN.search(source)
41
+ assert match is not None
42
+ assert match.group("name") == "fixerWithRecovery"
43
+
44
+
45
+ def test_returns_object_promise_pattern_matches_structured_object_claim() -> None:
46
+ lowercase_match = JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search(
47
+ "@returns {Promise<object>} the structured output"
48
+ )
49
+ assert lowercase_match is not None
50
+ assert lowercase_match.group(0) == "@returns {Promise<object>}"
51
+ capitalized_match = JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search(
52
+ "@return {Promise<Object>}"
53
+ )
54
+ assert capitalized_match is not None
55
+ assert capitalized_match.group(0) == "@return {Promise<Object>}"
56
+
57
+
58
+ def test_returns_object_promise_pattern_rejects_string_and_array_claims() -> None:
59
+ assert JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search("@returns {Promise<*>}") is None
60
+ assert (
61
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search("@returns {Promise<string>}") is None
62
+ )
63
+ assert (
64
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search("@returns {Promise<object[]>}")
65
+ is None
66
+ )
67
+
68
+
69
+ def test_return_call_pattern_captures_plain_and_awaited_callees() -> None:
70
+ plain_match = RETURN_CALL_OPENING_PARENTHESIS_PATTERN.search(
71
+ " return convergeAgent(prompt, {})"
72
+ )
73
+ assert plain_match is not None
74
+ assert plain_match.group("callee") == "convergeAgent"
75
+ awaited_match = RETURN_CALL_OPENING_PARENTHESIS_PATTERN.search(
76
+ " return await commitWithRecovery({})"
77
+ )
78
+ assert awaited_match is not None
79
+ assert awaited_match.group("callee") == "commitWithRecovery"
80
+
81
+
82
+ def test_return_call_pattern_ignores_identifiers_beginning_with_return() -> None:
83
+ assert RETURN_CALL_OPENING_PARENTHESIS_PATTERN.search("const x = returnValue(payload)") is None
84
+
85
+
86
+ def test_schema_property_key_pattern_matches_only_the_options_key() -> None:
87
+ assert (
88
+ SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ label: 'x', schema: HEAD_SCHEMA }")
89
+ is not None
90
+ )
91
+ assert SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ label: 'x', schema : X }") is not None
92
+ assert SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ agentType, phase }, HEAD_SCHEMA") is None
93
+ assert SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ myschema: X }") is None
@@ -14,7 +14,6 @@ UNUSED_IMPORT_GUIDANCE: str = (
14
14
  "remove unused import; if kept for side effects, mark with `# noqa: F401`"
15
15
  )
16
16
  TYPE_CHECKING_IDENTIFIER: str = "TYPE_CHECKING"
17
- ALL_TYPING_MODULE_NAMES: frozenset[str] = frozenset({"typing", "typing_extensions"})
18
17
 
19
18
 
20
19
  def _comment_text_from_line(line_text: str) -> str | None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.78.0",
3
+ "version": "1.80.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/rules/CLAUDE.md CHANGED
@@ -27,6 +27,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
27
27
  | `no-cross-skill-duplicate-helpers.md` | No duplicating shared helpers across skills; use `_shared/` |
28
28
  | `no-historical-clutter.md` | Documentation describes current state only; no historical or transitional language |
29
29
  | `no-inline-destructive-literals.md` | No destructive-command literals in Bash tool command strings, even as data |
30
+ | `env-var-table-code-drift.md` | Every env-var summary table row in a `.md` file names a code file whose source references the variable |
30
31
  | `orphan-css-class.md` | Every `class="..."` attribute in Python-generated markup has a matching selector in the `<style>` block |
31
32
  | `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
33
  | `paired-test-coverage.md` | A public function omitted by a module's established paired test suite must get a behavioral test |
@@ -1,54 +1,57 @@
1
- # Docstring Prose Matches Implementation
2
-
3
- **When this applies:** Any Write or Edit to a public function, method, class, or module whose docstring prose makes an enumerable claim about behavior — a list of inputs the code handles, the conditions it treats as a match, the cases it skips, or the order of its steps. It applies equally to a skill's companion `SKILL.md` (or any sibling `.md`) that describes a producer the skill's `scripts/` carry out: a doc sentence that claims a produced artifact's ordering or content is the prose this rule governs, and it tracks the producer function's own docstring and body.
4
-
5
- ## Rule
6
-
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
-
9
- The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Seven 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. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. 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 eight gated slices.
10
-
11
- ## What to check before you write the docstring
12
-
13
- Read the body and the docstring side by side:
14
-
15
- - **Read-source / match-source unions.** A body that computes `read_names = a | b | c` (or any union of "what counts") names each union member in the prose enumeration. A union member the code applies but the prose omits is a gap.
16
- - **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
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
- - **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.
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
- - **Raises-clause reachability for `LargeZipFile`.** A `Raises:` clause that names `zipfile.LargeZipFile` matches a writer the body opens with ZIP64 forbidden. `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False; a function that opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` at its default of True allows ZIP64 and never raises it, so the clause documents an unreachable exception. Drop the entry, or pass `allowZip64=False` when forbidding ZIP64 is the goal. The `check_docstring_raises_unraisable_largezipfile` gate blocks the default-ZIP64-writer form of this drift at Write/Edit time; a writer that forbids ZIP64 on any open, a read-only open, and a function that opens no writer are all left alone.
24
- - **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.
25
- - **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.
26
- - **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.
27
- - **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.
28
-
29
- When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
30
-
31
- ## Worked example
32
-
33
- A `@dataclass` dead-field check builds its set of "field counts as read" sources by union:
34
-
35
- ```python
36
- read_names = (
37
- attribute_read_names
38
- | dynamic_literal_names
39
- | _match_pattern_attribute_names(tree)
40
- | _exported_names(tree)
41
- )
42
- ```
43
-
44
- A docstring that enumerates "attribute read, augmented-assignment target, class-pattern keyword, literal `getattr`/`attrgetter`" but omits the `__all__` source (`_exported_names`) is drifted: a field whose name appears in `__all__` is treated as read, and the prose hides that. The fix adds the missing source to the enumeration so the list matches the union.
45
-
46
- ## Enforcement (audit lane)
47
-
48
- This drift class is sub-bucket **O6** in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` (free-form `Note:` / `Returns:` / responsibility-list claims). The audit teammate lists every prose enumeration in a changed docstring and verifies each item against the body, and lists every union member / suppressor / step in the body and verifies each appears in the prose. A union member or suppressor in the body that the prose omits is an O6 finding. The single-condition shared-fallback shape of this drift is gated deterministically by `check_docstring_fallback_branch_coverage` (`packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py`); the audit lane covers every O6 shape the gate cannot match.
49
-
50
- When a changed PR touches a producer function whose ordering or union shifts, the O8 audit lane also reads that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact. A doc sentence that claims the artifact is `sorted` or holds `just the at-risk names` while the producer merges prior names and appends without re-sorting is an O8 finding, even when the PR diff never touched the `.md` file — the behavior change orphaned the doc claim.
51
-
52
- ## Why
53
-
54
- A docstring enumeration earns its place by being trustworthy. A complete list lets a reader reason about the function without scanning the body; a list missing one item is worse than no list, because it asserts completeness it does not have. Naming this standard makes the gap a first-class finding at write time and at audit, rather than a surprise a reader hits months later.
1
+ # Docstring Prose Matches Implementation
2
+
3
+ **When this applies:** Any Write or Edit to a public function, method, class, or module whose docstring prose makes an enumerable claim about behavior — a list of inputs the code handles, the conditions it treats as a match, the cases it skips, or the order of its steps. It applies equally to a skill's companion `SKILL.md` (or any sibling `.md`) that describes a producer the skill's `scripts/` carry out: a doc sentence that claims a produced artifact's ordering or content is the prose this rule governs, and it tracks the producer function's own docstring and body.
4
+
5
+ ## Rule
6
+
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
+
9
+ The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Nine more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_length_constant_superlative_vs_exact_gate` covers a length-constant module whose docstring describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`) while the only code consuming the constant compares `len(...)` against it with `==`/`!=` — an exact-length gate that rejects every other length — and never with an ordered operator; the check scans the constant module's package tree (its own directory, or the parent package when the module sits in a `config/` subdirectory), so the mismatching consumer may sit in a sibling module. `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. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. `check_docstring_no_network_claim_with_metadata_access` covers a function docstring promising a code path returns `without touching the network` (or a sibling no-network phrase) while the body calls a path-metadata method (`is_file`, `is_dir`, `exists`, `stat`, `lstat`) — on a network share each metadata call is a round-trip over the wire, so a cache-hit path the docstring swore avoids the network still pays a stat on every call; reword the claim to state the path is stat-checked on every call, or short-circuit to the cached path before the share is touched. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and the broader module-responsibility paragraph outside the user-facing-text-scope slice the checklist below names — 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 ten gated slices.
10
+
11
+ ## What to check before you write the docstring
12
+
13
+ Read the body and the docstring side by side:
14
+
15
+ - **Read-source / match-source unions.** A body that computes `read_names = a | b | c` (or any union of "what counts") names each union member in the prose enumeration. A union member the code applies but the prose omits is a gap.
16
+ - **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
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
+ - **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.
20
+ - **JS/`.mjs` `@returns` object with a schema-less branch.** A `function` whose JSDoc `@returns {Promise<object>}` promises a structured object names a return type every branch honors. When the body returns one agent-spawn helper both with a `schema` options object and without one, the schema-less branch resolves to a transcript string, not the object the JSDoc claims. The `check_js_returns_object_schemaless_branch` gate blocks this drift a `Promise<object>` JSDoc whose body returns the same helper with and without a `schema` key 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.
21
+ - **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.
22
+ - **Length-constant superlative vs exact gate.** A module docstring that describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`, `no longer than`) matches how the code consumes the constant. When the only consumer compares `len(...)` against the constant with `==`/`!=` an exact-length gate where every other length is rejected, not accepted at a shorter lengththe superlative prose claims a range of accepted lengths the code never allows. State the exact required length (`the exact #AARRGGBB length`), not a longest/range form. The `check_docstring_length_constant_superlative_vs_exact_gate` gate blocks this drift at Write/Edit time, scanning the constant module's package tree so it sees a consumer that lives in a sibling module; a constant genuinely used as a ceiling (`len(x) <= LIMIT`) is left alone.
23
+ - **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.
24
+ - **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.
25
+ - **Raises-clause reachability for `LargeZipFile`.** A `Raises:` clause that names `zipfile.LargeZipFile` matches a writer the body opens with ZIP64 forbidden. `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False; a function that opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` at its default of True allows ZIP64 and never raises it, so the clause documents an unreachable exception. Drop the entry, or pass `allowZip64=False` when forbidding ZIP64 is the goal. The `check_docstring_raises_unraisable_largezipfile` gate blocks the default-ZIP64-writer form of this drift at Write/Edit time; a writer that forbids ZIP64 on any open, a read-only open, and a function that opens no writer are all left alone.
26
+ - **Module summary scope versus data-schema constants.** A module whose one-line docstring scopes its contents to user-facing text (`User-facing strings: CLI flag names, help text, and log messages`) names every category of constant the body holds. When the body also defines serialization field keys (`JSONL_FIELD_*`), run-metadata schema keys (`RUN_METADATA_CLI_ARG_KEY_*`), or runtime config (`STDOUT_ENCODING`, `MAIN_LOGGING_FORMAT_STRING`), the strings-only summary under-describes the modulethe module-responsibility drift the repo flags. Broaden the summary to name the data-schema keys and runtime config. The `check_module_docstring_scope_omits_data_schema_constants` gate blocks this drift at Write/Edit time, and fires only when the summary claims a user-facing-text scope and names no data-schema or runtime-config category, so a summary that already names `field keys`, `schema`, or `runtime config` passes.
27
+ - **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 `check_docstring_field_runmode_outcome` gate blocks the single-file shape of this drift at Write/Edit time — an `Attributes:` entry for a run-mode flag field (a name carrying `dry_run`) whose description carries a per-record write-outcome phrase and no run-mode phrase. The assignment that sets the field sits in another module, out of reach of that single-file gate, so any shape the gate cannot key on stays an O6 audit-lane judgment finding.
28
+ - **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.
29
+ - **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.
30
+ - **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.
31
+
32
+ When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
33
+
34
+ ## Worked example
35
+
36
+ A `@dataclass` dead-field check builds its set of "field counts as read" sources by union:
37
+
38
+ ```python
39
+ read_names = (
40
+ attribute_read_names
41
+ | dynamic_literal_names
42
+ | _match_pattern_attribute_names(tree)
43
+ | _exported_names(tree)
44
+ )
45
+ ```
46
+
47
+ A docstring that enumerates "attribute read, augmented-assignment target, class-pattern keyword, literal `getattr`/`attrgetter`" but omits the `__all__` source (`_exported_names`) is drifted: a field whose name appears in `__all__` is treated as read, and the prose hides that. The fix adds the missing source to the enumeration so the list matches the union.
48
+
49
+ ## Enforcement (audit lane)
50
+
51
+ This drift class is sub-bucket **O6** in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` (free-form `Note:` / `Returns:` / responsibility-list claims). The audit teammate lists every prose enumeration in a changed docstring and verifies each item against the body, and lists every union member / suppressor / step in the body and verifies each appears in the prose. A union member or suppressor in the body that the prose omits is an O6 finding. The single-condition shared-fallback shape of this drift is gated deterministically by `check_docstring_fallback_branch_coverage` (`packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py`); the audit lane covers every O6 shape the gate cannot match.
52
+
53
+ When a changed PR touches a producer function whose ordering or union shifts, the O8 audit lane also reads that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact. A doc sentence that claims the artifact is `sorted` or holds `just the at-risk names` while the producer merges prior names and appends without re-sorting is an O8 finding, even when the PR diff never touched the `.md` file — the behavior change orphaned the doc claim.
54
+
55
+ ## Why
56
+
57
+ A docstring enumeration earns its place by being trustworthy. A complete list lets a reader reason about the function without scanning the body; a list missing one item is worse than no list, because it asserts completeness it does not have. Naming this standard makes the gap a first-class finding at write time and at audit, rather than a surprise a reader hits months later.
@@ -0,0 +1,24 @@
1
+ # Env-Var Summary Table Names a Code File That Reads the Variable
2
+
3
+ **When this applies:** Any Write, Edit, or MultiEdit to a markdown (`.md`) file that carries an environment-variable summary table — a markdown table whose rows pair a `` `VARIABLE` `` name with the `` `code/file.py` `` that reads it.
4
+
5
+ ## Rule
6
+
7
+ Every row in an env-var summary table names a code file whose source references the variable. A row pairs an UPPER_SNAKE variable with a code-file path, written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | ... | ``, and the named file reads that variable. When the file exists yet its source never mentions the variable name, the row is stale: the table points a reader at a consumer relationship the code does not have, so a reader trusts the doc to behavior the code dropped.
8
+
9
+ When a code change removes the last read of a variable from a file, the same change drops or corrects the table row that names that file. The doc and the code move together in one commit.
10
+
11
+ ## What the gate checks
12
+
13
+ The `env_var_table_code_drift_blocker.py` hook runs on every Write, Edit, and MultiEdit whose target is a `.md` file. It:
14
+
15
+ 1. Reads the content the tool would leave on disk, skipping lines inside a fenced code block.
16
+ 2. Collects each table row whose first cell names an UPPER_SNAKE variable and whose later cell names a code file with a recognized extension (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`).
17
+ 3. Resolves the named code file under the repository root (the nearest `.git`-bearing ancestor of the markdown file) and reads its source.
18
+ 4. Blocks the write when the file resolves yet its source never references the variable name. For an Edit, drift the file already held on an untouched row is excluded, so only drift the edit introduces is reported.
19
+
20
+ The check stays quiet for a row whose code file resolves nowhere under the repository root (it cannot prove the drift), a row whose second cell holds no code-file path, a table row inside a fenced code block, and any test file.
21
+
22
+ ## Why this is a hook, not a lint pass
23
+
24
+ An env-var table that names a file whose source skips the variable reads as a correct map of which code consumes which setting, while pointing one row at behavior the code dropped. A reader trusting the row chases a setting the file ignores, and the gap survives review because the table still looks complete. Catching it as the doc is written keeps the table and the code in step.
@@ -1,12 +1,12 @@
1
1
  # New Production File Absent From Its Package Inventory
2
2
 
3
- **When this applies:** Any Write that creates a new production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`) in a directory whose sibling `README.md` or `CLAUDE.md` already names two or more of the directory's files in backticks.
3
+ **When this applies:** Any Write that creates a new production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`) in a directory whose sibling `README.md` or `CLAUDE.md` already names two or more of the directory's files in backticks, or in a skill's `scripts/` subdirectory whose parent `SKILL.md` Layout table already names two or more of those scripts.
4
4
 
5
5
  ## Rule
6
6
 
7
- A package directory that documents its own files in a `README.md` Layout table or a `CLAUDE.md` "Key files" list keeps that inventory in step with the directory. A new production file the inventory does not name leaves the inventory and the directory disagreeing on the package's file set: a reader who trusts the inventory to map the directory misses the new file.
7
+ A package directory that documents its own files in a `README.md` Layout table or a `CLAUDE.md` "Key files" list keeps that inventory in step with the directory. A skill package does the same in its `SKILL.md` Layout table, which sits at the skill root and maps the `scripts/` subdirectory by naming each script with its `scripts/<name>` path. A new production file the inventory does not name leaves the inventory and the directory disagreeing on the package's file set: a reader who trusts the inventory to map the directory misses the new file.
8
8
 
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.
9
+ When you create a new production file in such a directory, add an entry naming it — a row in the `README.md` or `SKILL.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
11
  ## Companion: keep the Purpose/scope sentence in step with the new responsibility
12
12
 
@@ -29,7 +29,7 @@ This slice sits outside the gate. The gate fires on a Write that creates a new f
29
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:
30
30
 
31
31
  1. Skips a target that is not a production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`), an exempt basename (`__init__.py`, `conftest.py`, `setup.py`, `_path_setup.py`), a test file (`test_*.py`, `*_test.py`, `*.spec.*`, `*.test.*`), or a file directly inside a `config/` or `tests/` directory.
32
- 2. Reads each `README.md` and `CLAUDE.md` present in the target's own directory and collects every bare filename they name in backticks. A backticked token holding a path contributes its final segment, so `pipeline/seam_continuity.py` in an inventory counts as naming `seam_continuity.py`. A multi-word command-example span — one carrying whitespace or shell punctuation (`:`, `$`, `<`, `>`), such as `parent:node_modules package.json` or `python <file>.py` — names no literal file and is dropped.
32
+ 2. Reads each `README.md`, `CLAUDE.md`, and `SKILL.md` present in the target's own directory and, when the target sits in a `scripts/` subdirectory, the parent directory's `SKILL.md`, and collects every bare filename they name in backticks. A backticked token holding a path contributes its final segment, so `pipeline/seam_continuity.py` in an inventory counts as naming `seam_continuity.py` and `scripts/stp_selection.py` in a parent `SKILL.md` Layout table counts as naming `stp_selection.py`. A multi-word command-example span — one carrying whitespace or shell punctuation (`:`, `$`, `<`, `>`), such as `parent:node_modules package.json` or `python <file>.py` — names no literal file and is dropped.
33
33
  3. Filters the named basenames to those that exist as a file in the target's own directory — the inventory's own sibling files — and treats the directory as carrying a maintained inventory only when two or more such sibling files are named. A directory with no inventory, one whose `README.md` mentions a single file in passing, or one whose inventory prose names only files living in other directories (so no named basename is an on-disk sibling) is out of scope.
34
34
  4. Blocks the write when the new file's basename appears in no present inventory. An unreadable or oversized inventory document is skipped, so a missing inventory never blocks a write.
35
35
 
@@ -1,23 +1,30 @@
1
1
  # Public-Function Paired-Test Coverage
2
2
 
3
- **When this applies:** Any Write or Edit to a production Python module that already has a dedicated stem-matched test file `test_<stem>.py` beside the module or under an ancestor `tests/` directory — and whose paired test suite already exercises at least one of the module's public functions.
3
+ **When this applies:** Either side of a paired module/test pair, so the check fires whichever file the write touches:
4
+
5
+ - A Write or Edit to a production Python module that already has a dedicated stem-matched test file — `test_<stem>.py` beside the module or under an ancestor `tests/` directory — whose paired suite already exercises the module, either by covering at least one of its public functions or by referencing one of its private helpers by name.
6
+ - A Write or Edit to a stem-matched test file (`test_<stem>.py` or `<stem>_test.py`) whose paired production module exists on disk and whose post-edit suite already exercises at least one of that module's public functions.
4
7
 
5
8
  ## Rule
6
9
 
7
10
  Every public function a production module defines is exercised by a test in the module's paired test suite. The suite proves the module is unit-tested function by function, so a public entry point the suite omits is a forgotten test: a reader who trusts the suite to cover the module's public surface misses that gap.
8
11
 
9
- When you add a public function to a module whose test suite already covers a sibling public function, add a behavioral test that calls the new function and asserts on its return value or side effect — in the same change that adds the function. This is the function-level half of the project rule "Every new production code path gets a paired behavioral test ... call the path and assert on what it does."
12
+ When you add a public function to a module whose test suite already exercises that module — covering a sibling public function, or testing one of its private helpers — add a behavioral test that calls the new function and asserts on its return value or side effect — in the same change that adds the function. A suite that exercises only a private helper (such as a color-conversion helper) while leaving the module's public renderers untested is the exact gap this rule closes. This is the function-level half of the project rule "Every new production code path gets a paired behavioral test ... call the path and assert on what it does."
10
13
 
11
14
  ## What the gate checks
12
15
 
13
- The `check_public_function_missing_paired_test` check in `code_rules_paired_test.py` (dispatched from `code_rules_enforcer.py`) runs on every production Python write or edit. It flags a public function when all of these hold:
16
+ Two complementary checks in `code_rules_paired_test.py` (both dispatched from `code_rules_enforcer.py`) cover the two write orders.
17
+
18
+ `check_public_function_missing_paired_test` runs on a production Python write or edit and flags a public function when all of these hold:
14
19
 
15
20
  1. The target is production code — not a test module, hook infrastructure, config module, migration, workflow registry, or `__init__.py`.
16
21
  2. A stem-matched test file (`test_<stem>.py` or `<stem>_test.py`) exists for the module.
17
- 3. That suite already references at least one public function the module defines — the signature of a maintained per-function suite rather than a placeholder or unrelated test file.
22
+ 3. That suite already exercises the module — referencing at least one public function the module defines, or referencing one of its private (underscore-prefixed) helper functions by name — the signature of a maintained per-module suite rather than a placeholder or unrelated test file.
18
23
  4. The public function is referenced by no test file in the directory that holds the stem-matched test.
19
24
 
20
- A public function counts as covered when its name appears imported, called, or named in any `test_*.py` or `*_test.py` file in that directory, so a function exercised by a differently-named sibling test still counts. `main` and underscore-prefixed functions are never required to carry a test.
25
+ `check_test_file_omits_module_public_function` runs on a stem-matched test-file write or edit and closes the reverse order, in which the production module is written before its test file exists. It resolves the production module the written `test_<stem>.py` or `<stem>_test.py` file pairs with — beside the test file, or in the parent of the `tests/` directory that holds it — reads that module from disk, and flags every public function the post-edit suite references nowhere, subject to the same established-suite precondition (the suite already covers at least one of the module's public functions). A production module that is itself exempt — a test module, hook infrastructure, config module, migration, workflow registry, or `__init__.py` — is skipped.
26
+
27
+ A public function counts as covered when its name appears — imported, called, or named — in any `test_*.py` or `*_test.py` file in the suite directory, so a function exercised by a differently-named sibling test still counts. `main` and underscore-prefixed functions are never required to carry a test.
21
28
 
22
29
  ## Relationship to the file-level TDD gate
23
30
 
@@ -97,6 +97,18 @@ PR's owner.
97
97
  prompt can add a standing Bash permission allow-rule for that script in their
98
98
  settings.
99
99
 
100
+ 5. **Copilot quota pre-check.** Before the `Workflow` call, run once:
101
+ `python "$HOME/.claude/_shared/pr-loop/scripts/copilot_quota.py"`
102
+ It reads the account's remaining Copilot premium-request quota via
103
+ `gh api copilot_internal/user` and prints one line — log that line. Exit 0
104
+ means Copilot has quota to run, so pass `copilotDisabled: false`. Any non-zero
105
+ exit means skip Copilot for this run — the account is out of quota, the quota
106
+ API or account access is down, or no account is set — so pass
107
+ `copilotDisabled: true`; the workflow then skips the Copilot gate with no agent
108
+ spawned. The account comes from the `COPILOT_QUOTA_ACCOUNT` environment
109
+ variable or a git-ignored `.env` file, and the no-account line names the exact
110
+ `.env` path and key to set.
111
+
100
112
  ## Run the workflow
101
113
 
102
114
  Call the `Workflow` tool against the colocated script:
@@ -104,7 +116,7 @@ Call the `Workflow` tool against the colocated script:
104
116
  ```
105
117
  Workflow({
106
118
  scriptPath: "<this skill dir>/workflow/converge.mjs",
107
- args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false }
119
+ args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false, copilotDisabled: false }
108
120
  })
109
121
  ```
110
122
 
@@ -113,8 +125,10 @@ own directory (on this install,
113
125
  `<home>/.claude/skills/autoconverge/workflow/converge.mjs`). Set
114
126
  `bugbotDisabled: true` only when the user has opted Cursor Bugbot out for the
115
127
  run; otherwise the workflow detects an opt-out or an unreachable Bugbot on its
116
- own. The workflow runs in the background and notifies this session on
117
- completion. Watch live progress with `/workflows`.
128
+ own. Set `copilotDisabled: true` when the step 5 quota pre-check exits non-zero,
129
+ and `false` when it exits 0; on `true` the workflow skips the Copilot gate with
130
+ no agent spawned. The workflow runs in the background and notifies this session
131
+ on completion. Watch live progress with `/workflows`.
118
132
 
119
133
  The workflow returns
120
134
  `{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote }`.
@@ -341,6 +355,13 @@ each PR its own checkout with `git worktree add`. For each PR the user named:
341
355
  4. **Grant project permissions once per repository** — the single-PR pre-flight
342
356
  step 4 grant covers every worktree of the same repo, so run it one time for
343
357
  the repo the PRs live in.
358
+ 5. **Copilot quota pre-check once for the whole run** — run the single-PR
359
+ pre-flight step 5 check one time:
360
+ `python "$HOME/.claude/_shared/pr-loop/scripts/copilot_quota.py"`. Every PR in
361
+ the run shares one account's Copilot premium-request quota, so one check covers
362
+ them all. Exit 0 sets `copilotDisabled: false` on every PR entry below; any
363
+ non-zero exit sets `copilotDisabled: true` on every entry, so each child skips
364
+ the Copilot gate with no agent spawned.
344
365
 
345
366
  ### Launch the multi-PR workflow
346
367
 
@@ -353,8 +374,8 @@ Workflow({
353
374
  args: {
354
375
  convergeScriptPath: "<this skill dir>/workflow/converge.mjs",
355
376
  prs: [
356
- { owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false },
357
- { owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false }
377
+ { owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false, copilotDisabled: false },
378
+ { owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false, copilotDisabled: false }
358
379
  ]
359
380
  }
360
381
  })
@@ -40,8 +40,8 @@ test('cleanAuditBlocker falls back to a no-result reason when the post agent die
40
40
  assert.match(message, /the post agent returned no result/);
41
41
  });
42
42
 
43
- test('the post-clean-audit task in resumeGeneralUtilityAgent returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
44
- const body = functionBody('resumeGeneralUtilityAgent');
43
+ test('the post-clean-audit task in runGeneralUtilityTask returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
44
+ const body = functionBody('runGeneralUtilityTask');
45
45
  assert.match(body, /task === 'post-clean-audit'/);
46
46
  assert.match(body, /schema: CLEAN_AUDIT_SCHEMA/);
47
47
  assert.doesNotMatch(body, /agent transcript \(unused\)/);
@@ -59,7 +59,7 @@ test('the standards-only call site breaks with a clean-audit blocker when the po
59
59
  convergeSource.indexOf('if (isStandardsOnlyRound(findings)) {'),
60
60
  convergeSource.indexOf('if (findings.length > 0) {'),
61
61
  );
62
- assert.match(branch, /resumeGeneralUtilityAgent\(.*'post-clean-audit'/);
62
+ assert.match(branch, /runGeneralUtilityTask\(.*'post-clean-audit'/);
63
63
  assert.match(branch, /if \(!auditResult\?\.posted\)/);
64
64
  assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
65
65
  assert.match(branch, /\bbreak\b/);
@@ -70,7 +70,7 @@ test('the all-clean call site breaks with a clean-audit blocker when the post do
70
70
  convergeSource.indexOf('all lenses clean on'),
71
71
  convergeSource.indexOf("if (phase === 'COPILOT') {"),
72
72
  );
73
- assert.match(branch, /resumeGeneralUtilityAgent\(.*'post-clean-audit'/);
73
+ assert.match(branch, /runGeneralUtilityTask\(.*'post-clean-audit'/);
74
74
  assert.match(branch, /if \(!auditResult\?\.posted\)/);
75
75
  assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
76
76
  assert.match(branch, /\bbreak\b/);