claude-dev-env 1.83.0 → 1.84.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/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +0 -4
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +116 -6
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +11 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/autoconverge/SKILL.md +38 -57
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +70 -73
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -8,7 +8,8 @@ Shared output schema and audit-loop contract used by `/bugteam`, `/qbug`, `/find
|
|
|
8
8
|
- Adversarial second pass
|
|
9
9
|
- Haiku secondary auditor
|
|
10
10
|
- Post-fix self-audit
|
|
11
|
-
-
|
|
11
|
+
- De-dup and merge
|
|
12
|
+
- Persistence (loop-<L>-audit.json, loop-<L>-diagnostics.json)
|
|
12
13
|
|
|
13
14
|
## Finding schema
|
|
14
15
|
|
|
@@ -18,7 +19,7 @@ Each finding an audit produces MUST be one of exactly two shapes.
|
|
|
18
19
|
|
|
19
20
|
```json
|
|
20
21
|
{
|
|
21
|
-
"id": "loop<
|
|
22
|
+
"id": "loop<L>-<K>",
|
|
22
23
|
"file": "path/relative/to/repo/root.py",
|
|
23
24
|
"line": 123,
|
|
24
25
|
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P",
|
|
@@ -29,7 +30,7 @@ Each finding an audit produces MUST be one of exactly two shapes.
|
|
|
29
30
|
}
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
`id` is `loop<
|
|
33
|
+
`id` is `loop<L>-<K>` where `L` is the loop counter (1-based) and `K` is the 1-based index within the loop. For `/findbugs` which runs once, use `find<K>`.
|
|
33
34
|
|
|
34
35
|
### Shape B — structured proof-of-absence
|
|
35
36
|
|
|
@@ -98,16 +99,16 @@ For single-subagent skills (`/qbug`, `/findbugs`) the LEAD spawns two `Agent()`
|
|
|
98
99
|
|
|
99
100
|
Both audit the same diff. The secondary returns findings to the LEAD only — never posted to the PR.
|
|
100
101
|
|
|
101
|
-
Merge rules:
|
|
102
|
+
Merge rules — applied whenever the LEAD combines findings from multiple sources (primary auditor, Haiku secondary auditor, adversarial pass):
|
|
102
103
|
|
|
103
|
-
- **De-dup key**: `(file, line, category)`.
|
|
104
|
-
- **Severity conflict**: max wins (P0 > P1 > P2).
|
|
105
|
-
- **Unique-to-
|
|
104
|
+
- **De-dup key**: `(file, line, category)`. Two findings sharing the same `(file, line, category)` tuple are the same finding and collapse into one entry.
|
|
105
|
+
- **Severity conflict**: max wins (`P0 > P1 > P2`). When sources disagree on severity for the same de-dup key, the merged entry keeps the highest severity.
|
|
106
|
+
- **Unique-to-secondary findings**: added to the merged set with the secondary's severity and source annotation.
|
|
106
107
|
- **Unique-to-primary findings**: kept as-is.
|
|
107
|
-
- **Zero
|
|
108
|
-
- **Malformed or non-parseable
|
|
108
|
+
- **Zero secondary findings**: the primary set is trusted and the audit moves on.
|
|
109
|
+
- **Malformed or non-parseable secondary output**: lead trusts the primary set and logs the event in `loop-<L>-diagnostics.json` under `haiku_findings` as `[{"parse_error": "<message>"}]`.
|
|
109
110
|
|
|
110
|
-
For
|
|
111
|
+
For `/bugteam`, the single audit agent provides per-category coverage by walking all A–P rubrics in one invocation.
|
|
111
112
|
|
|
112
113
|
## Post-fix self-audit
|
|
113
114
|
|
|
@@ -128,11 +129,22 @@ Sequence:
|
|
|
128
129
|
|
|
129
130
|
`converged` exit condition: `primary_audit_clean AND post_fix_audit_clean` for the committing loop.
|
|
130
131
|
|
|
132
|
+
## De-dup and merge
|
|
133
|
+
|
|
134
|
+
Findings from primary, adversarial, Haiku secondary, and post-fix passes are merged into a single deduped finding list before persistence.
|
|
135
|
+
|
|
136
|
+
- **De-dup key:** `(file, line, category)`. Two findings sharing the same `(file, line, category)` tuple collapse into a single deduped entry.
|
|
137
|
+
- **Severity conflict resolution:** `max wins`. When merged findings disagree on severity, the deduped entry carries the highest severity (`P0 > P1 > P2`).
|
|
138
|
+
- **Excerpt and failure_mode:** the deduped entry inherits these fields from the highest-severity contributing finding. Ties keep the first observed contributor.
|
|
139
|
+
- **`evidence_files`:** the deduped entry carries the union of every contributor's `evidence_files`, deduplicated and sorted.
|
|
140
|
+
|
|
141
|
+
The merged list lands in `loop-<L>-diagnostics.json` under both `merged` (one entry per contributing finding) and `deduped` (one entry per unique `(file, line, category)` tuple).
|
|
142
|
+
|
|
131
143
|
## Persistence
|
|
132
144
|
|
|
133
145
|
Every audit loop writes two JSON files under the skill's scoped temp directory (resolved via `tempfile.gettempdir()`):
|
|
134
146
|
|
|
135
|
-
### `loop-<
|
|
147
|
+
### `loop-<L>-audit.json`
|
|
136
148
|
|
|
137
149
|
```json
|
|
138
150
|
{
|
|
@@ -142,7 +154,7 @@ Every audit loop writes two JSON files under the skill's scoped temp directory (
|
|
|
142
154
|
}
|
|
143
155
|
```
|
|
144
156
|
|
|
145
|
-
### `loop-<
|
|
157
|
+
### `loop-<L>-diagnostics.json`
|
|
146
158
|
|
|
147
159
|
```json
|
|
148
160
|
{
|
|
@@ -299,9 +299,11 @@ def write_atomically_with_mode(
|
|
|
299
299
|
|
|
300
300
|
Raises:
|
|
301
301
|
OSError: When os.open or os.fdopen fails. The raw file descriptor
|
|
302
|
-
is closed
|
|
302
|
+
is closed and the on-disk temporary file is unlinked before
|
|
303
|
+
re-raising so neither the descriptor nor the file leaks.
|
|
303
304
|
MemoryError: When os.fdopen runs out of buffer memory; the file
|
|
304
|
-
descriptor is closed
|
|
305
|
+
descriptor is closed and the temporary file is unlinked
|
|
306
|
+
before re-raising.
|
|
305
307
|
"""
|
|
306
308
|
file_descriptor = os.open(
|
|
307
309
|
str(temporary_path),
|
|
@@ -312,6 +314,10 @@ def write_atomically_with_mode(
|
|
|
312
314
|
writer = os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING)
|
|
313
315
|
except (OSError, MemoryError):
|
|
314
316
|
os.close(file_descriptor)
|
|
317
|
+
try:
|
|
318
|
+
os.unlink(str(temporary_path))
|
|
319
|
+
except OSError:
|
|
320
|
+
pass
|
|
315
321
|
raise
|
|
316
322
|
with writer:
|
|
317
323
|
writer.write(serialized_content)
|
|
@@ -321,7 +327,8 @@ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
|
|
|
321
327
|
"""Write settings to a JSON file atomically with permission preservation.
|
|
322
328
|
|
|
323
329
|
Creates a temporary sibling file, writes content, then atomically
|
|
324
|
-
replaces the target. Cleans up the temporary file in a finally block
|
|
330
|
+
replaces the target. Cleans up the temporary file in a finally block,
|
|
331
|
+
printing a warning to stderr when that cleanup unlink fails.
|
|
325
332
|
|
|
326
333
|
Args:
|
|
327
334
|
settings_path: Path to the target settings JSON file.
|
|
@@ -352,8 +359,12 @@ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
|
|
|
352
359
|
if temporary_path.exists():
|
|
353
360
|
try:
|
|
354
361
|
temporary_path.unlink()
|
|
355
|
-
except OSError:
|
|
356
|
-
|
|
362
|
+
except OSError as unlink_error:
|
|
363
|
+
print(
|
|
364
|
+
f"Warning: could not remove temp file {temporary_path}: "
|
|
365
|
+
f"{type(unlink_error).__name__}: {unlink_error}",
|
|
366
|
+
file=sys.stderr,
|
|
367
|
+
)
|
|
357
368
|
|
|
358
369
|
|
|
359
370
|
def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
|
|
@@ -1673,7 +1673,6 @@ def main(all_arguments: list[str]) -> int:
|
|
|
1673
1673
|
if arguments.staged:
|
|
1674
1674
|
all_terminology_findings = staged_terminology_findings(repository_root)
|
|
1675
1675
|
_report_terminology_findings(all_terminology_findings)
|
|
1676
|
-
terminology_exit_code = 1 if all_terminology_findings else 0
|
|
1677
1676
|
staged_test_exit_code = run_staged_test_files(repository_root)
|
|
1678
1677
|
staged_file_paths = paths_from_git_staged(repository_root)
|
|
1679
1678
|
staged_file_paths = filter_paths_under_prefixes(
|
|
@@ -1682,7 +1681,7 @@ def main(all_arguments: list[str]) -> int:
|
|
|
1682
1681
|
arguments.only_under,
|
|
1683
1682
|
)
|
|
1684
1683
|
if not staged_file_paths:
|
|
1685
|
-
return
|
|
1684
|
+
return staged_test_exit_code
|
|
1686
1685
|
staged_added_lines = added_lines_by_file_staged(
|
|
1687
1686
|
repository_root, staged_file_paths
|
|
1688
1687
|
)
|
|
@@ -1693,7 +1692,7 @@ def main(all_arguments: list[str]) -> int:
|
|
|
1693
1692
|
all_added_lines_by_path=staged_added_lines,
|
|
1694
1693
|
read_staged_content_flag=True,
|
|
1695
1694
|
)
|
|
1696
|
-
return gate_exit_code or
|
|
1695
|
+
return gate_exit_code or staged_test_exit_code
|
|
1697
1696
|
file_paths = paths_from_git_diff(repository_root, arguments.base)
|
|
1698
1697
|
file_paths = filter_paths_under_prefixes(
|
|
1699
1698
|
file_paths,
|
|
@@ -103,6 +103,7 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
103
103
|
choices=[
|
|
104
104
|
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
|
|
105
105
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
|
|
106
|
+
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
106
107
|
],
|
|
107
108
|
help="Reviewer token to test against CLAUDE_REVIEWS_DISABLED",
|
|
108
109
|
)
|
|
@@ -122,6 +123,7 @@ def main(all_arguments: list[str]) -> int:
|
|
|
122
123
|
disabled_checker_by_reviewer = {
|
|
123
124
|
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: is_bugbot_disabled_via_env,
|
|
124
125
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: is_bugteam_disabled_via_env,
|
|
126
|
+
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN: is_copilot_disabled_via_env,
|
|
125
127
|
}
|
|
126
128
|
return 0 if disabled_checker_by_reviewer[arguments.reviewer]() else 1
|
|
127
129
|
|
|
@@ -133,3 +133,215 @@ def test_path_contains_glob_metacharacters_accepts_windows_paths_with_parens() -
|
|
|
133
133
|
assert common.path_contains_glob_metacharacters("C:/Users/Jon (Admin)/project") is False
|
|
134
134
|
assert common.path_contains_glob_metacharacters("C:/Projects/a,b/file.py") is False
|
|
135
135
|
|
|
136
|
+
|
|
137
|
+
def test_get_mode_to_preserve_returns_existing_file_mode(tmp_path: Path) -> None:
|
|
138
|
+
"""When the file exists, the actual filesystem mode must be returned (not the default)."""
|
|
139
|
+
target_path = tmp_path / "settings.json"
|
|
140
|
+
target_path.write_text("{}", encoding="utf-8")
|
|
141
|
+
actual_filesystem_mode = target_path.stat().st_mode & 0o777
|
|
142
|
+
returned_mode = common.get_mode_to_preserve(target_path)
|
|
143
|
+
assert returned_mode == actual_filesystem_mode
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_get_mode_to_preserve_returns_secure_default_when_file_missing(
|
|
147
|
+
tmp_path: Path,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""A missing settings file must fall back to the secure 0o600 default mode."""
|
|
150
|
+
missing_settings_path = tmp_path / "no_such_file.json"
|
|
151
|
+
returned_mode = common.get_mode_to_preserve(missing_settings_path)
|
|
152
|
+
assert returned_mode == 0o600
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_write_atomically_with_mode_raises_oserror_when_open_fails(
|
|
156
|
+
tmp_path: Path,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""OSError from os.open must propagate to the caller."""
|
|
159
|
+
target_path = tmp_path / "subdirectory" / "missing" / "settings.json.tmp"
|
|
160
|
+
with pytest.raises(OSError):
|
|
161
|
+
common.write_atomically_with_mode(target_path, "payload", file_mode=0o600)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_write_atomically_with_mode_unlinks_temp_when_fdopen_raises(
|
|
165
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
166
|
+
) -> None:
|
|
167
|
+
"""A failure inside os.fdopen must remove the on-disk temp file.
|
|
168
|
+
|
|
169
|
+
The file exists the moment os.open returns; closing only the raw
|
|
170
|
+
descriptor would leave an empty temp sibling on disk after the
|
|
171
|
+
exception propagates to the caller.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def failing_fdopen(*_args: object, **_kwargs: object) -> object:
|
|
175
|
+
raise MemoryError("fdopen failure")
|
|
176
|
+
|
|
177
|
+
monkeypatch.setattr(common.os, "fdopen", failing_fdopen)
|
|
178
|
+
target_path = tmp_path / "settings.json.tmp"
|
|
179
|
+
with pytest.raises(MemoryError):
|
|
180
|
+
common.write_atomically_with_mode(target_path, "payload", file_mode=0o600)
|
|
181
|
+
assert not target_path.exists(), (
|
|
182
|
+
"the temp file created by os.open must be unlinked before re-raising"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_save_settings_warns_on_stderr_when_temp_unlink_fails(
|
|
187
|
+
tmp_path: Path,
|
|
188
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
189
|
+
capsys: pytest.CaptureFixture[str],
|
|
190
|
+
) -> None:
|
|
191
|
+
"""A swallowed unlink in the finally block must surface to stderr.
|
|
192
|
+
|
|
193
|
+
Forces a write success followed by os.replace failure so the temp file
|
|
194
|
+
survives into the finally branch, then makes Path.unlink raise.
|
|
195
|
+
"""
|
|
196
|
+
settings_path = tmp_path / "settings.json"
|
|
197
|
+
settings_path.write_text('{"existing": true}\n', encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
def failing_replace(*_args: object, **_kwargs: object) -> None:
|
|
200
|
+
raise OSError("replace blocked by AV")
|
|
201
|
+
|
|
202
|
+
def failing_unlink(_self: Path, *_args: object, **_kwargs: object) -> None:
|
|
203
|
+
raise PermissionError("temp file held by AV")
|
|
204
|
+
|
|
205
|
+
monkeypatch.setattr(common.os, "replace", failing_replace)
|
|
206
|
+
monkeypatch.setattr(Path, "unlink", failing_unlink)
|
|
207
|
+
with pytest.raises(SystemExit):
|
|
208
|
+
common.save_settings(settings_path, {"new_key": "value"})
|
|
209
|
+
captured = capsys.readouterr()
|
|
210
|
+
assert ".tmp" in captured.err
|
|
211
|
+
assert "PermissionError" in captured.err or "held by AV" in captured.err
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_save_settings_finally_skips_unlink_when_no_temp_was_created(
|
|
215
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
216
|
+
) -> None:
|
|
217
|
+
"""When this invocation never created the temp file, finally must not unlink it."""
|
|
218
|
+
settings_path = tmp_path / "settings.json"
|
|
219
|
+
settings_path.write_text('{"existing": true}\n', encoding="utf-8")
|
|
220
|
+
|
|
221
|
+
unlink_call_paths: list[Path] = []
|
|
222
|
+
original_unlink = Path.unlink
|
|
223
|
+
|
|
224
|
+
def recording_unlink(unlink_target: Path, *args: object, **kwargs: object) -> None:
|
|
225
|
+
unlink_call_paths.append(unlink_target)
|
|
226
|
+
original_unlink(unlink_target, *args, **kwargs)
|
|
227
|
+
|
|
228
|
+
def write_raises(*_args: object, **_kwargs: object) -> None:
|
|
229
|
+
raise FileExistsError("another writer's temp")
|
|
230
|
+
|
|
231
|
+
monkeypatch.setattr(common, "write_atomically_with_mode", write_raises)
|
|
232
|
+
monkeypatch.setattr(Path, "unlink", recording_unlink)
|
|
233
|
+
with pytest.raises(SystemExit):
|
|
234
|
+
common.save_settings(settings_path, {"new_key": "value"})
|
|
235
|
+
assert not any(
|
|
236
|
+
".tmp." in each_path.name for each_path in unlink_call_paths
|
|
237
|
+
), "finally must not unlink a temp file this invocation never created"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_exit_with_error_prints_prefixed_message_and_exits_nonzero(
|
|
241
|
+
capsys: pytest.CaptureFixture[str],
|
|
242
|
+
) -> None:
|
|
243
|
+
with pytest.raises(SystemExit) as raised_exit:
|
|
244
|
+
common.exit_with_error("boom")
|
|
245
|
+
assert raised_exit.value.code == 1
|
|
246
|
+
assert "Error: boom" in capsys.readouterr().err
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_build_permission_rules_builds_one_rule_per_tool() -> None:
|
|
250
|
+
built_rules = common.build_permission_rules("/proj", ("Edit", "Read"))
|
|
251
|
+
assert built_rules == ["Edit(/proj/.claude/**)", "Read(/proj/.claude/**)"]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_build_agent_config_deny_rule_embeds_tool_path_and_pattern() -> None:
|
|
255
|
+
built_rule = common.build_agent_config_deny_rule("Write", "/proj", "hooks/**")
|
|
256
|
+
assert built_rule == "Write(/proj/.claude/hooks/**)"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_remove_matching_entries_from_list_filters_in_place_and_counts() -> None:
|
|
260
|
+
all_entries: list[object] = ["keep", "drop", "drop", 7]
|
|
261
|
+
removed_count = common.remove_matching_entries_from_list(
|
|
262
|
+
all_entries, lambda each_entry: each_entry == "drop"
|
|
263
|
+
)
|
|
264
|
+
assert removed_count == 2
|
|
265
|
+
assert all_entries == ["keep", 7]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_load_settings_returns_empty_dict_when_file_missing(tmp_path: Path) -> None:
|
|
269
|
+
assert common.load_settings(tmp_path / "absent.json") == {}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_load_settings_parses_existing_json_object(tmp_path: Path) -> None:
|
|
273
|
+
settings_path = tmp_path / "settings.json"
|
|
274
|
+
settings_path.write_text('{"permissions": {"allow": []}}', encoding="utf-8")
|
|
275
|
+
assert common.load_settings(settings_path) == {"permissions": {"allow": []}}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_load_settings_exits_when_file_holds_invalid_json(
|
|
279
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
280
|
+
) -> None:
|
|
281
|
+
settings_path = tmp_path / "settings.json"
|
|
282
|
+
settings_path.write_text("not json", encoding="utf-8")
|
|
283
|
+
with pytest.raises(SystemExit):
|
|
284
|
+
common.load_settings(settings_path)
|
|
285
|
+
assert "not valid JSON" in capsys.readouterr().err
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_serialize_settings_to_json_text_sorts_keys() -> None:
|
|
289
|
+
serialized_text = common.serialize_settings_to_json_text({"b": 1, "a": 2})
|
|
290
|
+
assert serialized_text.index('"a"') < serialized_text.index('"b"')
|
|
291
|
+
assert json.loads(serialized_text) == {"a": 2, "b": 1}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_append_if_missing_appends_only_new_values() -> None:
|
|
295
|
+
all_entries: list[object] = ["existing"]
|
|
296
|
+
assert common.append_if_missing(all_entries, "fresh") is True
|
|
297
|
+
assert common.append_if_missing(all_entries, "fresh") is False
|
|
298
|
+
assert all_entries == ["existing", "fresh"]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_ensure_dict_section_creates_missing_and_returns_existing() -> None:
|
|
302
|
+
all_settings: dict[str, object] = {}
|
|
303
|
+
created_section = common.ensure_dict_section(all_settings, "permissions")
|
|
304
|
+
assert created_section == {}
|
|
305
|
+
assert all_settings["permissions"] is created_section
|
|
306
|
+
assert common.ensure_dict_section(all_settings, "permissions") is created_section
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_ensure_dict_section_exits_on_non_dict_value(
|
|
310
|
+
capsys: pytest.CaptureFixture[str],
|
|
311
|
+
) -> None:
|
|
312
|
+
all_settings: dict[str, object] = {"permissions": "oops"}
|
|
313
|
+
with pytest.raises(SystemExit):
|
|
314
|
+
common.ensure_dict_section(all_settings, "permissions")
|
|
315
|
+
assert "not a JSON object" in capsys.readouterr().err
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_ensure_list_entry_creates_missing_and_returns_existing() -> None:
|
|
319
|
+
permissions_section: dict[str, object] = {}
|
|
320
|
+
created_entry = common.ensure_list_entry(permissions_section, "allow")
|
|
321
|
+
assert created_entry == []
|
|
322
|
+
assert permissions_section["allow"] is created_entry
|
|
323
|
+
assert common.ensure_list_entry(permissions_section, "allow") is created_entry
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_ensure_list_entry_exits_on_non_list_value(
|
|
327
|
+
capsys: pytest.CaptureFixture[str],
|
|
328
|
+
) -> None:
|
|
329
|
+
permissions_section: dict[str, object] = {"allow": {}}
|
|
330
|
+
with pytest.raises(SystemExit):
|
|
331
|
+
common.ensure_list_entry(permissions_section, "allow")
|
|
332
|
+
assert "not a JSON array" in capsys.readouterr().err
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_prune_empty_list_then_empty_section_removes_empty_structures() -> None:
|
|
336
|
+
all_settings: dict[str, object] = {"permissions": {"allow": []}}
|
|
337
|
+
common.prune_empty_list_then_empty_section(all_settings, "permissions", "allow")
|
|
338
|
+
assert all_settings == {}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_prune_empty_list_then_empty_section_keeps_populated_structures() -> None:
|
|
342
|
+
all_settings: dict[str, object] = {
|
|
343
|
+
"permissions": {"allow": ["Edit(/proj/.claude/**)"], "deny": []}
|
|
344
|
+
}
|
|
345
|
+
common.prune_empty_list_then_empty_section(all_settings, "permissions", "deny")
|
|
346
|
+
assert all_settings == {"permissions": {"allow": ["Edit(/proj/.claude/**)"]}}
|
|
347
|
+
|
|
@@ -355,6 +355,24 @@ def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
|
|
|
355
355
|
)
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
def test_is_valid_project_root_shared_by_grant_and_revoke_modules(
|
|
359
|
+
tmp_path: Path,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Grant and revoke import the same helper, so both agree on the marker contract."""
|
|
362
|
+
grant_module = _load_grant_module()
|
|
363
|
+
revoke_module = _load_revoke_module()
|
|
364
|
+
git_marker_project_root = tmp_path / "git_project"
|
|
365
|
+
(git_marker_project_root / ".git").mkdir(parents=True)
|
|
366
|
+
claude_marker_project_root = tmp_path / "claude_project"
|
|
367
|
+
(claude_marker_project_root / ".claude").mkdir(parents=True)
|
|
368
|
+
bare_directory = tmp_path / "no_marker"
|
|
369
|
+
bare_directory.mkdir()
|
|
370
|
+
for each_module in (grant_module, revoke_module):
|
|
371
|
+
assert each_module.is_valid_project_root(git_marker_project_root) is True
|
|
372
|
+
assert each_module.is_valid_project_root(claude_marker_project_root) is True
|
|
373
|
+
assert each_module.is_valid_project_root(bare_directory) is False
|
|
374
|
+
|
|
375
|
+
|
|
358
376
|
def test_second_grant_is_idempotent_when_no_other_settings_changed(
|
|
359
377
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
360
378
|
) -> None:
|
|
@@ -160,6 +160,24 @@ def test_path_contains_glob_metacharacters_local_tuple_uses_all_collection_prefi
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def test_is_valid_project_root_detects_git_marker(tmp_path: Path) -> None:
|
|
164
|
+
git_project_root = tmp_path / "git_project"
|
|
165
|
+
(git_project_root / ".git").mkdir(parents=True)
|
|
166
|
+
assert common.is_valid_project_root(git_project_root) is True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_is_valid_project_root_detects_claude_marker(tmp_path: Path) -> None:
|
|
170
|
+
claude_project_root = tmp_path / "claude_project"
|
|
171
|
+
(claude_project_root / ".claude").mkdir(parents=True)
|
|
172
|
+
assert common.is_valid_project_root(claude_project_root) is True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_is_valid_project_root_rejects_unmarked_directory(tmp_path: Path) -> None:
|
|
176
|
+
unmarked_directory = tmp_path / "no_marker"
|
|
177
|
+
unmarked_directory.mkdir()
|
|
178
|
+
assert common.is_valid_project_root(unmarked_directory) is False
|
|
179
|
+
|
|
180
|
+
|
|
163
181
|
def test_is_valid_project_root_uses_extracted_directory_marker_constants() -> None:
|
|
164
182
|
"""is_valid_project_root must reference extracted constants, not inline string literals."""
|
|
165
183
|
source_text = inspect.getsource(common.is_valid_project_root)
|
|
@@ -9,6 +9,7 @@ Covers:
|
|
|
9
9
|
|
|
10
10
|
import importlib.util
|
|
11
11
|
import inspect
|
|
12
|
+
import os
|
|
12
13
|
import subprocess
|
|
13
14
|
import sys
|
|
14
15
|
import unittest.mock
|
|
@@ -93,6 +94,13 @@ def stage_file(repository_root: Path, relative_path: str) -> None:
|
|
|
93
94
|
run_git_in_repository(repository_root, "add", "--", relative_path)
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
@pytest.fixture(autouse=True)
|
|
98
|
+
def isolated_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
99
|
+
for each_variable_name in list(os.environ):
|
|
100
|
+
if each_variable_name.startswith("GIT_"):
|
|
101
|
+
monkeypatch.delenv(each_variable_name, raising=False)
|
|
102
|
+
|
|
103
|
+
|
|
96
104
|
@pytest.fixture()
|
|
97
105
|
def temporary_git_repository(tmp_path: Path) -> Path:
|
|
98
106
|
repository_root = tmp_path / "repository_under_test"
|
|
@@ -320,6 +328,35 @@ def test_main_staged_mode_exits_zero_when_nothing_staged(
|
|
|
320
328
|
assert exit_code == 0
|
|
321
329
|
|
|
322
330
|
|
|
331
|
+
def test_main_staged_mode_passes_when_only_terminology_findings_exist(
|
|
332
|
+
temporary_git_repository: Path,
|
|
333
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
334
|
+
) -> None:
|
|
335
|
+
write_file(temporary_git_repository / "quota.py", "first_count = 1\n")
|
|
336
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
337
|
+
write_file(
|
|
338
|
+
temporary_git_repository / "quota.py",
|
|
339
|
+
"first_count = 1\npremium_interactions = 1\n",
|
|
340
|
+
)
|
|
341
|
+
write_file(
|
|
342
|
+
temporary_git_repository / "README.md",
|
|
343
|
+
"The premium-request budget gates the run.\n",
|
|
344
|
+
)
|
|
345
|
+
stage_file(temporary_git_repository, "quota.py")
|
|
346
|
+
stage_file(temporary_git_repository, "README.md")
|
|
347
|
+
|
|
348
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
349
|
+
terminology_findings = gate_module.staged_terminology_findings(
|
|
350
|
+
temporary_git_repository
|
|
351
|
+
)
|
|
352
|
+
exit_code = gate_module.main(["--staged"])
|
|
353
|
+
|
|
354
|
+
assert any(
|
|
355
|
+
"premium-request" in each_finding for each_finding in terminology_findings
|
|
356
|
+
)
|
|
357
|
+
assert exit_code == 0
|
|
358
|
+
|
|
359
|
+
|
|
323
360
|
def test_added_lines_for_staged_file_returns_empty_for_modified_file_with_no_additions(
|
|
324
361
|
temporary_git_repository: Path,
|
|
325
362
|
) -> None:
|
|
@@ -120,3 +120,17 @@ def test_cli_main_supports_bugteam_reviewer(
|
|
|
120
120
|
) -> None:
|
|
121
121
|
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
|
|
122
122
|
assert reviews_disabled.main(["--reviewer", "bugteam"]) == 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cli_main_supports_copilot_reviewer(
|
|
126
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
127
|
+
) -> None:
|
|
128
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
|
|
129
|
+
assert reviews_disabled.main(["--reviewer", "copilot"]) == 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_cli_main_returns_one_for_copilot_when_only_bugbot_disabled(
|
|
133
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
134
|
+
) -> None:
|
|
135
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
136
|
+
assert reviews_disabled.main(["--reviewer", "copilot"]) == 1
|
package/agents/clean-coder.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clean-coder
|
|
3
3
|
description: "Use PROACTIVELY for ALL code generation — feature development, bug fixes, refactoring, hook creation, automation scripts, and any task that produces code. Internalizes CODE_RULES.md and the 8-dimension readability standard so thoroughly that /check finds zero issues. The definitive code-writing agent."
|
|
4
|
+
model: opus
|
|
4
5
|
tools: Read, Write, Edit, Bash, Grep, Glob, Task, Skill
|
|
5
6
|
color: green
|
|
6
7
|
---
|
|
@@ -436,6 +437,8 @@ This default is overridden by explicit user instruction such as "refactor this e
|
|
|
436
437
|
|
|
437
438
|
Docstrings on functions, methods, classes, and modules are encouraged for public APIs. The self-documenting-names gate inspects inline `#` and block `#` comments only; docstrings are exempt from that gate.
|
|
438
439
|
|
|
440
|
+
Write a behavior-illustrating docstring in the diagram-first shape: one summary line, then a `::` literal block or a doctest that shows a concrete input and its marked outcome (with `ok:` / `flag:` contrast lines where a pass-and-fail pair helps), then a couple of short neutral prose lines, then the `Args:` / `Returns:` sections. Two hooks back this: `check_docstring_runon_sentence` flags a run-on prose wall, and `check_docstring_prose_wall_without_illustration` flags a narrative that runs long with no diagram. See `../rules/plain-illustrative-docstrings.md` for the shape and a worked example.
|
|
441
|
+
|
|
439
442
|
## Audit Awareness
|
|
440
443
|
|
|
441
444
|
Code clean-coder writes will be audited later against the A–P bug categories from `code-quality-agent`. The hooks listed in this file enforce the Category J slice at write time, but A–I and K–P surface only in audit. For each category's full rubric, sub-bucket decomposition, and concrete checks, see `../audit-rubrics/category_rubrics/` (relative to this agent file). While generating code, anticipate the full A–P surface so the first write clears every audit category.
|
|
@@ -28,7 +28,7 @@ Decomposition is by the **kind of docstring claim** that needs to be cross-check
|
|
|
28
28
|
| O6 | Free-form `Args:`-adjacent claims | A docstring's `Returns:` / `Raises:` / `Note:` / `Example:` sections make claims (`returns shared-temp only`, `raises ValueError on missing key`). Verify each claim against the body. When a docstring enumerates the inputs a body counts (a "field counts as read when ..." list, a list of conditions treated as a match, a list of cases the body skips), list every union member and every suppressor the body applies (`read_names = a | b | c`, each early-return guard) and confirm each appears in the prose enumeration. A union member or suppressor the body applies but the prose omits is an O6 finding. When a docstring sentence excludes a named category of input from what the function flags (`X are not dispatch steps`, `Y is not a match`), confirm the axis the prose excludes on is the axis the body's branch condition actually keys on. A body that flags a call when it sits inside an `If.test` guard, paired with prose that excludes by the call's receiver shape (`method-on-local calls inside a branch are not dispatch steps`), is an O6 finding: a guarded method-on-local call is flagged even though the prose lists it as excluded — the exclusion is keyed to the wrong axis. The single-condition shared-fallback shape of this drift — a summary that scopes a fallback call to one condition while the body routes to that same call from two or more early-return guards — is gated deterministically at Write/Edit time by `check_docstring_fallback_branch_coverage`, so the audit lane focuses on the O6 shapes the gate cannot match. The exception-guard shape of this drift — a docstring that promises a malformed payload `resolves to None` while a payload subscript (`payload["key"]`, `float(payload["key"])`) sits outside the try/except whose handler returns None, so a present-but-malformed payload raises rather than resolving to None — is gated deterministically at Write/Edit time by `check_docstring_unguarded_malformed_payload_claim`, so the audit lane focuses on the wider Raises/None-on-failure claims the gate cannot match. A `Returns:` that names the mechanism, tool, or output format the function produces (`instructing a StructuredOutput summary`, `returns a YAML document`, `emits a JSON object`) matches the artifact the body actually builds: a prompt body that asks the agent to "Return strictly a JSON object" while the docstring claims it "instruct[s] a StructuredOutput" summary is an O6 finding, because the named tool appears nowhere in the emitted text. See `../../rules/docstring-prose-matches-implementation.md`. |
|
|
29
29
|
| O7 | Module-doc-vs-split-module after refactor | When a refactor moves a responsibility to a sibling module, the originating module's docstring and the receiving module's docstring both describe the home of that responsibility. A module docstring should describe only the responsibilities it owns. |
|
|
30
30
|
| O8 | Companion-doc ordering/content vs producer | When a PR changes a producer function's ordering or union, read that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact (a file path, a JSON key, a named list). A doc sentence that claims the artifact is `sorted` / `alphabetical` / `in sorted order`, or holds `just the at-risk names` / `only the current set`, while the producer merges stored names with new names and appends — preserving file order, not re-sorting the union — is an O8 finding on both counts (wrong order claim, hidden merged-in entries). The finding stands even when the PR diff never touched the `.md` file, because the behavior change orphaned the doc claim. See `../../rules/docstring-prose-matches-implementation.md`. |
|
|
31
|
-
| O9 | Python docstring plainness for a general developer | A changed module / class / public-function docstring's narrative prose — the summary and description before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section — reads plainly and paints a concrete scene a general developer follows on first read. Flag a narrative that stacks abstract machinery nouns into a wall (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`), that defines a thing by what it is not (`the non-promoter-specific machinery`), or that runs one sentence long while joining clauses with an em-dash or a semicolon. The deterministic run-on mark is gated at Write/Edit time by `check_docstring_runon_sentence` in `code_rules_docstrings.py`, so this lane carries the
|
|
31
|
+
| O9 | Python docstring plainness for a general developer | A changed module / class / public-function docstring's narrative prose — the summary and description before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section — reads plainly and paints a concrete scene a general developer follows on first read. Flag a narrative that stacks abstract machinery nouns into a wall (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`), that defines a thing by what it is not (`the non-promoter-specific machinery`), or that runs one sentence long while joining clauses with an em-dash or a semicolon. The diagram-first shape carries this best: a summary line, then a `::` example block or a doctest that shows a concrete input and its marked outcome, then a couple of short prose lines. The deterministic run-on mark is gated at Write/Edit time by `check_docstring_runon_sentence` in `code_rules_docstrings.py`, and a narrative that runs more than six prose lines with no such block is gated by `check_docstring_prose_wall_without_illustration` in the same module, so this lane carries the judgment the gates cannot: whether a stranger to the code pictures the moment, the input, and the outcome after one read, and whether the diagram truly illustrates — a real input, a marked outcome, an `ok:` / `flag:` contrast a reader learns from. See `../../rules/plain-illustrative-docstrings.md`. |
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
package/bin/install.mjs
CHANGED
|
@@ -294,10 +294,13 @@ function backupClaudeHubBeforeOverwrite(destPath, incomingPath) {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
297
|
+
* PreToolUse hook script paths the installer manages even though hooks.json
|
|
298
|
+
* carries no standalone entry for them. Most were folded into the PreToolUse
|
|
299
|
+
* dispatcher in Stage 1; md_to_html_blocker is a retired hook with no script on
|
|
300
|
+
* disk. Each path stays in this set so a reinstall from an older settings shape
|
|
301
|
+
* prunes its standalone entry — a folded hook would otherwise double-run
|
|
302
|
+
* alongside the dispatcher, and the retired hook's entry would point at a
|
|
303
|
+
* missing script.
|
|
301
304
|
*/
|
|
302
305
|
export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
303
306
|
'blocking/write_existing_file_blocker.py',
|
|
@@ -316,18 +319,23 @@ export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
316
319
|
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
317
320
|
'blocking/open_questions_in_plans_blocker.py',
|
|
318
321
|
'blocking/plain_language_blocker.py',
|
|
322
|
+
'blocking/md_to_html_blocker.py',
|
|
319
323
|
]);
|
|
320
324
|
|
|
321
325
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
+
* After-write hook script paths the installer manages even though hooks.json
|
|
327
|
+
* carries no standalone entry for them. mypy_validator and auto_formatter run
|
|
328
|
+
* hosted inside the PostToolUse dispatcher; doc_gist_auto_publish and
|
|
329
|
+
* md_to_html_companion are retired hooks with no script on disk. Each path stays
|
|
330
|
+
* in this set so a reinstall from an older settings shape prunes its standalone
|
|
331
|
+
* entry — a hosted hook would otherwise double-run alongside the dispatcher, and
|
|
332
|
+
* a retired hook's entry would point at a missing script.
|
|
326
333
|
*/
|
|
327
334
|
export const POST_FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
328
335
|
'validation/mypy_validator.py',
|
|
329
336
|
'workflow/auto_formatter.py',
|
|
330
337
|
'workflow/doc_gist_auto_publish.py',
|
|
338
|
+
'workflow/md_to_html_companion.py',
|
|
331
339
|
]);
|
|
332
340
|
|
|
333
341
|
/**
|