claude-dev-env 1.93.1 → 1.95.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/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +35 -27
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/audit-contract.md +1 -1
- package/_shared/pr-loop/gh-payloads.md +3 -3
- package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
- package/_shared/pr-loop/scripts/README.md +2 -2
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
- package/_shared/pr-loop/state-schema.md +3 -22
- package/bin/CLAUDE.md +11 -2
- package/bin/ever-shipped-skills.mjs +70 -0
- package/bin/install.mjs +138 -13
- package/bin/install.prune.test.mjs +457 -0
- package/bin/install.test.mjs +10 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_shared.py +82 -0
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/pii_commit_command.py +398 -0
- package/hooks/blocking/pii_payload_scan.py +133 -0
- package/hooks/blocking/pii_prevention_blocker.py +151 -449
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +9 -2
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/hooks/pyproject.toml +75 -4
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +10 -1
- package/scripts/Get-SessionAccount.ps1 +210 -0
- package/scripts/tests/CLAUDE.md +15 -1
- package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
- package/skills/CLAUDE.md +6 -27
- package/skills/anthropic-plan/SKILL.md +1 -13
- package/skills/autoconverge/CLAUDE.md +5 -2
- package/skills/autoconverge/SKILL.md +112 -387
- package/skills/autoconverge/reference/CLAUDE.md +4 -1
- package/skills/autoconverge/reference/closing-report.md +9 -2
- package/skills/autoconverge/reference/convergence.md +26 -22
- package/skills/autoconverge/reference/headless-safety.md +44 -0
- package/skills/autoconverge/reference/multi-pr.md +88 -0
- package/skills/autoconverge/reference/self-closing-loop.md +84 -0
- package/skills/autoconverge/reference/stop-conditions.md +21 -13
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
- package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +286 -96
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/CONSTRAINTS.md +3 -3
- package/skills/bugteam/PROMPTS.md +8 -19
- package/skills/bugteam/SKILL.md +13 -14
- package/skills/bugteam/reference/CLAUDE.md +1 -1
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
- package/skills/bugteam/reference/github-pr-reviews.md +56 -38
- package/skills/bugteam/reference/team-setup.md +11 -11
- package/skills/bugteam/scripts/CLAUDE.md +6 -6
- package/skills/bugteam/scripts/README.md +20 -8
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
- package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
- package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/copilot-finding-triage/SKILL.md +80 -28
- package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
- package/skills/copilot-finding-triage/templates/notification.md +8 -3
- package/skills/everything-search/SKILL.md +1 -2
- package/skills/grokify/SKILL.md +58 -0
- package/skills/grokify/templates/handoff-template.md +92 -0
- package/skills/orchestrator/SKILL.md +35 -25
- package/skills/orchestrator-refresh/SKILL.md +15 -11
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +132 -233
- package/skills/pr-converge/reference/convergence-gates.md +102 -66
- package/skills/pr-converge/reference/examples.md +37 -25
- package/skills/pr-converge/reference/fix-protocol.md +7 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
- package/skills/pr-converge/reference/per-tick.md +64 -41
- package/skills/pr-converge/reference/state-schema.md +27 -2
- package/skills/pr-converge/scripts/CLAUDE.md +8 -0
- package/skills/pr-converge/scripts/README.md +4 -3
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
- package/skills/pr-converge/scripts/check_convergence.py +189 -645
- package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
- package/skills/pr-converge/scripts/conftest.py +8 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
- package/skills/pr-fix-protocol/SKILL.md +20 -20
- package/skills/privacy-hygiene/SKILL.md +4 -3
- package/skills/reviewer-gates/SKILL.md +26 -8
- package/skills/session-log/SKILL.md +11 -12
- package/skills/session-tidy/SKILL.md +40 -30
- package/skills/skill-builder/CLAUDE.md +13 -10
- package/skills/skill-builder/SKILL.md +31 -31
- package/skills/skill-builder/references/CLAUDE.md +10 -6
- package/skills/skill-builder/references/delegation-map.md +13 -4
- package/skills/skill-builder/references/description-field.md +113 -0
- package/skills/skill-builder/references/self-audit-checklist.md +21 -4
- package/skills/skill-builder/references/skill-modularity.md +113 -0
- package/skills/skill-builder/references/skill-types.md +4 -0
- package/skills/skill-builder/templates/CLAUDE.md +4 -4
- package/skills/skill-builder/templates/gap-analysis.md +25 -1
- package/skills/skill-builder/workflows/CLAUDE.md +6 -4
- package/skills/skill-builder/workflows/improve-skill.md +17 -16
- package/skills/skill-builder/workflows/new-skill.md +34 -10
- package/skills/skill-builder/workflows/polish-skill.md +59 -43
- package/skills/team-advisor/SKILL.md +15 -11
- package/skills/usage-pause/SKILL.md +10 -14
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
- package/skills/bdd-protocol/CLAUDE.md +0 -26
- package/skills/bdd-protocol/SKILL.md +0 -30
- package/skills/bdd-protocol/references/CLAUDE.md +0 -10
- package/skills/bdd-protocol/references/anti-patterns.md +0 -26
- package/skills/bdd-protocol/references/example-mapping.md +0 -23
- package/skills/bg-agent/CLAUDE.md +0 -17
- package/skills/bg-agent/SKILL.md +0 -69
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
- package/skills/caveman/CLAUDE.md +0 -15
- package/skills/caveman/SKILL.md +0 -38
- package/skills/code/CLAUDE.md +0 -17
- package/skills/code/SKILL.md +0 -178
- package/skills/deep-research/CLAUDE.md +0 -17
- package/skills/deep-research/SKILL.md +0 -80
- package/skills/findbugs/CLAUDE.md +0 -20
- package/skills/findbugs/SKILL.md +0 -241
- package/skills/fixbugs/CLAUDE.md +0 -19
- package/skills/fixbugs/SKILL.md +0 -142
- package/skills/gh-paginate/CLAUDE.md +0 -18
- package/skills/gh-paginate/SKILL.md +0 -84
- package/skills/gotcha/CLAUDE.md +0 -33
- package/skills/gotcha/SKILL.md +0 -73
- package/skills/implement/CLAUDE.md +0 -27
- package/skills/implement/SKILL.md +0 -66
- package/skills/implement/scripts/CLAUDE.md +0 -22
- package/skills/implement/scripts/append_note.py +0 -133
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
- package/skills/implement/scripts/test_append_note.py +0 -191
- package/skills/log-audit/CLAUDE.md +0 -20
- package/skills/log-audit/SKILL.md +0 -66
- package/skills/log-audit/reference/CLAUDE.md +0 -9
- package/skills/log-audit/reference/charter.md +0 -52
- package/skills/log-audit/scripts/CLAUDE.md +0 -27
- package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
- package/skills/log-audit/scripts/collect_log_window.py +0 -199
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
- package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
- package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
- package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
- package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
- package/skills/logifix/CLAUDE.md +0 -36
- package/skills/logifix/SKILL.md +0 -69
- package/skills/logifix/scripts/CLAUDE.md +0 -16
- package/skills/logifix/scripts/logifix.ps1 +0 -205
- package/skills/monitor-open-prs/CLAUDE.md +0 -34
- package/skills/monitor-open-prs/SKILL.md +0 -88
- package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
- package/skills/monitor-open-prs/test_skill_contract.py +0 -27
- package/skills/post-audit-findings/SKILL.md +0 -80
- package/skills/pr-consistency-audit/CLAUDE.md +0 -34
- package/skills/pr-consistency-audit/SKILL.md +0 -116
- package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
- package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
- package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
- package/skills/pr-review-responder/CLAUDE.md +0 -35
- package/skills/pr-review-responder/EXAMPLES.md +0 -590
- package/skills/pr-review-responder/PRINCIPLES.md +0 -533
- package/skills/pr-review-responder/README.md +0 -168
- package/skills/pr-review-responder/SKILL.md +0 -203
- package/skills/pr-review-responder/TESTING.md +0 -350
- package/skills/pr-scope-resolve/SKILL.md +0 -45
- package/skills/pre-compact/CLAUDE.md +0 -24
- package/skills/pre-compact/SKILL.md +0 -134
- package/skills/qbug/CLAUDE.md +0 -40
- package/skills/qbug/SKILL.md +0 -387
- package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
- package/skills/refine/CLAUDE.md +0 -44
- package/skills/refine/SKILL.md +0 -262
- package/skills/refine/templates/CLAUDE.md +0 -17
- package/skills/refine/templates/implementation-notes-template.html +0 -56
- package/skills/refine/templates/plan-template.md +0 -60
- package/skills/research-mode/CLAUDE.md +0 -35
- package/skills/research-mode/SKILL.md +0 -53
- package/skills/structure-prompt/CLAUDE.md +0 -42
- package/skills/structure-prompt/SKILL.md +0 -44
- package/skills/structure-prompt/reference/CLAUDE.md +0 -28
- package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
- package/skills/structure-prompt/reference/block-classification.md +0 -27
- package/skills/structure-prompt/reference/canonical-case.md +0 -48
- package/skills/structure-prompt/reference/citation-depth.md +0 -70
- package/skills/structure-prompt/reference/cleanup.md +0 -33
- package/skills/structure-prompt/reference/constraints.md +0 -33
- package/skills/structure-prompt/reference/directives.md +0 -37
- package/skills/structure-prompt/reference/examples.md +0 -72
- package/skills/structure-prompt/reference/instantiation.md +0 -51
- package/skills/structure-prompt/reference/output-contract.md +0 -72
- package/skills/structure-prompt/reference/per-category.md +0 -23
- package/skills/structure-prompt/reference/persona.md +0 -38
- package/skills/structure-prompt/reference/research.md +0 -33
- package/skills/structure-prompt/reference/structure.md +0 -28
- package/skills/verified-build/CLAUDE.md +0 -33
- package/skills/verified-build/SKILL.md +0 -33
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""Read the recent window of the hook block log into structured records.
|
|
2
|
-
|
|
3
|
-
Tails the JSON-lines hook block log every blocking hook appends to, and hands
|
|
4
|
-
back the block events inside a time window as plain records. Picture the audit
|
|
5
|
-
agent waking up: it asks this script for the last few hours of blocks, and gets
|
|
6
|
-
one record per block carrying when it happened, which hook fired, and why. Those
|
|
7
|
-
records feed cluster_recurrences, which groups them by a normalized signature.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
collect_log_window.py --hours 24
|
|
11
|
-
collect_log_window.py --hours 6 --log-path /path/to/hook-blocks.log
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import json
|
|
18
|
-
import sys
|
|
19
|
-
from collections.abc import Iterable
|
|
20
|
-
from dataclasses import dataclass
|
|
21
|
-
from datetime import datetime, timedelta
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
_script_directory = str(Path(__file__).resolve().parent)
|
|
25
|
-
if _script_directory not in sys.path:
|
|
26
|
-
sys.path.insert(0, _script_directory)
|
|
27
|
-
|
|
28
|
-
from log_audit_constants.collect_log_window_constants import ( # noqa: E402
|
|
29
|
-
BLOCK_LEVEL_LABEL,
|
|
30
|
-
DEFAULT_WINDOW_HOURS,
|
|
31
|
-
HOOK_BLOCKS_LOG_RELATIVE_PATH,
|
|
32
|
-
LOG_HOOK_KEY,
|
|
33
|
-
LOG_REASON_KEY,
|
|
34
|
-
LOG_TIMESTAMP_KEY,
|
|
35
|
-
RECORD_LEVEL_KEY,
|
|
36
|
-
RECORD_MESSAGE_KEY,
|
|
37
|
-
RECORD_SOURCE_KEY,
|
|
38
|
-
RECORD_TIMESTAMP_KEY,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass(frozen=True)
|
|
43
|
-
class LogRecord:
|
|
44
|
-
"""One block event read from the hook block log.
|
|
45
|
-
|
|
46
|
-
Attributes:
|
|
47
|
-
timestamp: When the block happened, parsed from the record's ISO string.
|
|
48
|
-
source: The hook that blocked, taken from the record's hook field.
|
|
49
|
-
level: The severity label; every hook-block record reads as a block.
|
|
50
|
-
message: The human-readable block reason.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
timestamp: datetime
|
|
54
|
-
source: str
|
|
55
|
-
level: str
|
|
56
|
-
message: str
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def parse_log_line(raw_line: str) -> LogRecord | None:
|
|
60
|
-
"""Parse one hook-block-log line into a LogRecord.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
raw_line: A single line of the JSON-lines hook block log.
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
The parsed record, or None when the line is blank, is not a JSON object,
|
|
67
|
-
or omits the timestamp, hook, or reason field, or carries a timestamp
|
|
68
|
-
that does not parse as an ISO datetime.
|
|
69
|
-
"""
|
|
70
|
-
stripped_line = raw_line.strip()
|
|
71
|
-
if not stripped_line:
|
|
72
|
-
return None
|
|
73
|
-
try:
|
|
74
|
-
record_fields = json.loads(stripped_line)
|
|
75
|
-
except json.JSONDecodeError:
|
|
76
|
-
return None
|
|
77
|
-
if not isinstance(record_fields, dict):
|
|
78
|
-
return None
|
|
79
|
-
timestamp_text = record_fields.get(LOG_TIMESTAMP_KEY)
|
|
80
|
-
source_text = record_fields.get(LOG_HOOK_KEY)
|
|
81
|
-
message_text = record_fields.get(LOG_REASON_KEY)
|
|
82
|
-
if not isinstance(timestamp_text, str):
|
|
83
|
-
return None
|
|
84
|
-
if not isinstance(source_text, str) or not isinstance(message_text, str):
|
|
85
|
-
return None
|
|
86
|
-
try:
|
|
87
|
-
parsed_timestamp = datetime.fromisoformat(timestamp_text)
|
|
88
|
-
except ValueError:
|
|
89
|
-
return None
|
|
90
|
-
return LogRecord(
|
|
91
|
-
timestamp=parsed_timestamp,
|
|
92
|
-
source=source_text,
|
|
93
|
-
level=BLOCK_LEVEL_LABEL,
|
|
94
|
-
message=message_text,
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _records_within_window(
|
|
99
|
-
all_lines: Iterable[str], window_start: datetime
|
|
100
|
-
) -> list[LogRecord]:
|
|
101
|
-
"""Parse each line and keep the records at or after the window start.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
all_lines: The hook-block-log lines to parse, in file order.
|
|
105
|
-
window_start: The earliest timestamp a kept record may carry.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
The in-window records in the order their lines appear.
|
|
109
|
-
"""
|
|
110
|
-
kept_records: list[LogRecord] = []
|
|
111
|
-
for each_line in all_lines:
|
|
112
|
-
parsed_record = parse_log_line(each_line)
|
|
113
|
-
if parsed_record is None:
|
|
114
|
-
continue
|
|
115
|
-
if parsed_record.timestamp >= window_start:
|
|
116
|
-
kept_records.append(parsed_record)
|
|
117
|
-
return kept_records
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def collect_records(log_text: str, window_start: datetime) -> list[LogRecord]:
|
|
121
|
-
"""Parse log text and keep records at or after the window start.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
log_text: The full text of the hook block log.
|
|
125
|
-
window_start: The earliest timestamp a kept record may carry.
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
The in-window records in the order they appear in the log.
|
|
129
|
-
"""
|
|
130
|
-
return _records_within_window(log_text.splitlines(), window_start)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def read_log_window(
|
|
134
|
-
log_path: Path, window_hours: int, now: datetime
|
|
135
|
-
) -> list[LogRecord]:
|
|
136
|
-
"""Read the hook block log and return records within the window.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
log_path: Path to the JSON-lines hook block log.
|
|
140
|
-
window_hours: How many hours back from now to keep.
|
|
141
|
-
now: The current time the window is measured back from.
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
The in-window records, or an empty list when the log file is absent.
|
|
145
|
-
"""
|
|
146
|
-
window_start = now - timedelta(hours=window_hours)
|
|
147
|
-
try:
|
|
148
|
-
with log_path.open(encoding="utf-8") as log_file:
|
|
149
|
-
return _records_within_window(log_file, window_start)
|
|
150
|
-
except FileNotFoundError:
|
|
151
|
-
return []
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _default_log_path() -> Path:
|
|
155
|
-
"""Return the home-relative path of the hook block log."""
|
|
156
|
-
return Path.home() / HOOK_BLOCKS_LOG_RELATIVE_PATH
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _record_as_json_dict(record: LogRecord) -> dict[str, str]:
|
|
160
|
-
"""Render a record as a JSON-ready dict with an ISO timestamp string.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
record: The record to render.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
A dict of string values keyed by the record's field names.
|
|
167
|
-
"""
|
|
168
|
-
return {
|
|
169
|
-
RECORD_TIMESTAMP_KEY: record.timestamp.isoformat(),
|
|
170
|
-
RECORD_SOURCE_KEY: record.source,
|
|
171
|
-
RECORD_LEVEL_KEY: record.level,
|
|
172
|
-
RECORD_MESSAGE_KEY: record.message,
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def main() -> int:
|
|
177
|
-
"""Print the recent hook-block records as JSON to stdout.
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
The process exit code; zero on success.
|
|
181
|
-
"""
|
|
182
|
-
parser = argparse.ArgumentParser(description="Tail the hook block log window.")
|
|
183
|
-
parser.add_argument("--hours", type=int, default=DEFAULT_WINDOW_HOURS)
|
|
184
|
-
parser.add_argument("--log-path", type=str, default=None)
|
|
185
|
-
parsed_arguments = parser.parse_args()
|
|
186
|
-
log_path = (
|
|
187
|
-
Path(parsed_arguments.log_path).expanduser().resolve()
|
|
188
|
-
if parsed_arguments.log_path is not None
|
|
189
|
-
else _default_log_path()
|
|
190
|
-
)
|
|
191
|
-
records = read_log_window(log_path, parsed_arguments.hours, datetime.now())
|
|
192
|
-
json.dump(
|
|
193
|
-
[_record_as_json_dict(each_record) for each_record in records], sys.stdout
|
|
194
|
-
)
|
|
195
|
-
return 0
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if __name__ == "__main__":
|
|
199
|
-
raise SystemExit(main())
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# log-audit/scripts/log_audit_constants
|
|
2
|
-
|
|
3
|
-
Named constants imported by the `log-audit` scripts. Each module holds the tunables for one script, keeping magic values out of the scripts.
|
|
4
|
-
|
|
5
|
-
## Files
|
|
6
|
-
|
|
7
|
-
| File | What it holds |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `__init__.py` | Package marker. |
|
|
10
|
-
| `collect_log_window_constants.py` | The hook-block-log path, default window, block level label, and the input and output record key names for `collect_log_window.py`. |
|
|
11
|
-
| `cluster_recurrences_constants.py` | The signature-stripping regexes, recency decay, and timing-regression thresholds for `cluster_recurrences.py`. |
|
|
12
|
-
| `mine_copilot_findings_constants.py` | The reviewer-bot logins, the recent-pulls and per-pull comments endpoint templates, the recent-pull count, and the defect-class keyword and proposal maps for `mine_copilot_findings.py`. |
|
|
File without changes
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Constants for the cluster_recurrences script.
|
|
2
|
-
|
|
3
|
-
PATH_PATTERN, HASH_PATTERN, DIGIT_PATTERN: regexes stripped from a message, in this
|
|
4
|
-
order, so records that differ only in a path, hash, or number share one signature.
|
|
5
|
-
SIGNATURE_PLACEHOLDER: the token each stripped span collapses to.
|
|
6
|
-
SECONDS_PER_HOUR: seconds in one hour, dividing a record's age into hours.
|
|
7
|
-
RECENCY_DECAY_BASE: the weight a record carries once it is one half-life old.
|
|
8
|
-
RECENCY_HALF_LIFE_HOURS: age in hours at which a record's weight reaches the decay base.
|
|
9
|
-
TIMING_REGRESSION_RATIO: recent-over-baseline duration ratio that flags a regression.
|
|
10
|
-
MIN_TIMING_SAMPLES_PER_HALF: samples required in the baseline half and the recent half.
|
|
11
|
-
SAMPLE_HALVES: the number of halves a timing series splits into before it can be judged.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
PATH_PATTERN = r"(?:[A-Za-z]:)?(?:[\\/][\w.\-]+)+"
|
|
15
|
-
HASH_PATTERN = r"\b[0-9a-f]{7,40}\b"
|
|
16
|
-
DIGIT_PATTERN = r"\d+"
|
|
17
|
-
SIGNATURE_PLACEHOLDER = "*"
|
|
18
|
-
SECONDS_PER_HOUR = 3600
|
|
19
|
-
RECENCY_DECAY_BASE = 0.5
|
|
20
|
-
RECENCY_HALF_LIFE_HOURS = 24.0
|
|
21
|
-
TIMING_REGRESSION_RATIO = 1.5
|
|
22
|
-
MIN_TIMING_SAMPLES_PER_HALF = 3
|
|
23
|
-
SAMPLE_HALVES = 2
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"""Constants for the collect_log_window script.
|
|
2
|
-
|
|
3
|
-
HOOK_BLOCKS_LOG_RELATIVE_PATH: home-relative path of the JSON-lines hook block log.
|
|
4
|
-
DEFAULT_WINDOW_HOURS: how many hours back a collection reads when no window is given.
|
|
5
|
-
BLOCK_LEVEL_LABEL: the level stamped on every record read from the hook block log.
|
|
6
|
-
LOG_TIMESTAMP_KEY: hook-block-log JSON key holding the ISO-8601 block time.
|
|
7
|
-
LOG_HOOK_KEY: hook-block-log JSON key holding the blocking hook's name.
|
|
8
|
-
LOG_REASON_KEY: hook-block-log JSON key holding the human-readable block reason.
|
|
9
|
-
RECORD_TIMESTAMP_KEY: output-record key holding the ISO-8601 block time.
|
|
10
|
-
RECORD_SOURCE_KEY: output-record key holding the blocking hook's name.
|
|
11
|
-
RECORD_LEVEL_KEY: output-record key holding the severity label.
|
|
12
|
-
RECORD_MESSAGE_KEY: output-record key holding the block reason.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
HOOK_BLOCKS_LOG_RELATIVE_PATH = ".claude/logs/hook-blocks.log"
|
|
16
|
-
DEFAULT_WINDOW_HOURS = 24
|
|
17
|
-
BLOCK_LEVEL_LABEL = "block"
|
|
18
|
-
LOG_TIMESTAMP_KEY = "timestamp"
|
|
19
|
-
LOG_HOOK_KEY = "hook"
|
|
20
|
-
LOG_REASON_KEY = "reason"
|
|
21
|
-
RECORD_TIMESTAMP_KEY = "timestamp"
|
|
22
|
-
RECORD_SOURCE_KEY = "source"
|
|
23
|
-
RECORD_LEVEL_KEY = "level"
|
|
24
|
-
RECORD_MESSAGE_KEY = "message"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""Constants for the mine_copilot_findings script.
|
|
2
|
-
|
|
3
|
-
ALL_REVIEWER_BOT_LOGINS: the GitHub logins whose review comments count as defects.
|
|
4
|
-
RECENT_PULLS_ENDPOINT_TEMPLATE: the gh api path for a repo's most recent pulls.
|
|
5
|
-
PULL_COMMENTS_ENDPOINT_TEMPLATE: the gh api path for one pull request's comments.
|
|
6
|
-
RECENT_PULL_COUNT: how many recent pull requests a mining run reads.
|
|
7
|
-
MAX_EXAMPLES_PER_CLUSTER: example comment bodies kept per defect class.
|
|
8
|
-
KEYWORDS_BY_DEFECT_CLASS: lowercased substrings that sort a comment into a defect class.
|
|
9
|
-
PROPOSAL_BY_DEFECT_CLASS: the concrete skill-definition edit proposed for each class.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
ALL_REVIEWER_BOT_LOGINS = ("copilot-pull-request-reviewer[bot]", "cursor[bot]")
|
|
13
|
-
RECENT_PULLS_ENDPOINT_TEMPLATE = (
|
|
14
|
-
"repos/{repo}/pulls?state=all&sort=updated&direction=desc&per_page={pull_count}"
|
|
15
|
-
)
|
|
16
|
-
PULL_COMMENTS_ENDPOINT_TEMPLATE = (
|
|
17
|
-
"repos/{repo}/pulls/{pull_number}/comments?per_page=100"
|
|
18
|
-
)
|
|
19
|
-
RECENT_PULL_COUNT = 20
|
|
20
|
-
MAX_EXAMPLES_PER_CLUSTER = 3
|
|
21
|
-
KEYWORDS_BY_DEFECT_CLASS = {
|
|
22
|
-
"missing-type-hint": ("type hint", "missing annotation", "untyped", "no return type"),
|
|
23
|
-
"broad-except": ("broad except", "bare except", "except exception", "catch-all except"),
|
|
24
|
-
"magic-value": ("magic number", "magic value", "hardcoded", "hard-coded"),
|
|
25
|
-
"unclear-name": ("abbreviation", "unclear name", "rename this", "single-letter"),
|
|
26
|
-
"missing-docstring": ("missing docstring", "no docstring", "undocumented"),
|
|
27
|
-
}
|
|
28
|
-
PROPOSAL_BY_DEFECT_CLASS = {
|
|
29
|
-
"missing-type-hint": (
|
|
30
|
-
"Tighten the code skill's type-hint rule to flag the untyped shape this "
|
|
31
|
-
"class keeps hitting."
|
|
32
|
-
),
|
|
33
|
-
"broad-except": (
|
|
34
|
-
"Extend the no-broad-except enforcer pattern to the except shape this "
|
|
35
|
-
"class keeps hitting."
|
|
36
|
-
),
|
|
37
|
-
"magic-value": (
|
|
38
|
-
"Extend the magic-value enforcer to the literal shape this class keeps "
|
|
39
|
-
"hitting."
|
|
40
|
-
),
|
|
41
|
-
"unclear-name": (
|
|
42
|
-
"Add the abbreviation this class keeps flagging to the banned-identifier "
|
|
43
|
-
"list."
|
|
44
|
-
),
|
|
45
|
-
"missing-docstring": (
|
|
46
|
-
"Tighten the public-docstring rule to the surface this class keeps "
|
|
47
|
-
"hitting."
|
|
48
|
-
),
|
|
49
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
"""Turn the defects reviewers keep catching into upstream skill-edit proposals.
|
|
2
|
-
|
|
3
|
-
Copilot and Bugbot flag the same kinds of defect over and over — an untyped
|
|
4
|
-
return here, a broad except there. Each catch is a defect that reached review
|
|
5
|
-
because nothing blocked it at write time. This script reads recent reviewer
|
|
6
|
-
comments, sorts them into defect classes, and for each class names a concrete
|
|
7
|
-
skill or rule edit that would block that class upstream. The proposals are for a
|
|
8
|
-
human to apply through review, not for the agent to commit on its own.
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
mine_copilot_findings.py --repo owner/name
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import json
|
|
18
|
-
import subprocess
|
|
19
|
-
import sys
|
|
20
|
-
from collections import defaultdict
|
|
21
|
-
from dataclasses import dataclass
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
_script_directory = str(Path(__file__).resolve().parent)
|
|
25
|
-
if _script_directory not in sys.path:
|
|
26
|
-
sys.path.insert(0, _script_directory)
|
|
27
|
-
|
|
28
|
-
from log_audit_constants.mine_copilot_findings_constants import ( # noqa: E402
|
|
29
|
-
ALL_REVIEWER_BOT_LOGINS,
|
|
30
|
-
KEYWORDS_BY_DEFECT_CLASS,
|
|
31
|
-
MAX_EXAMPLES_PER_CLUSTER,
|
|
32
|
-
PROPOSAL_BY_DEFECT_CLASS,
|
|
33
|
-
PULL_COMMENTS_ENDPOINT_TEMPLATE,
|
|
34
|
-
RECENT_PULL_COUNT,
|
|
35
|
-
RECENT_PULLS_ENDPOINT_TEMPLATE,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class GitHubCommandError(RuntimeError):
|
|
40
|
-
"""Raised when a gh api call fails or returns output that is not JSON."""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@dataclass(frozen=True)
|
|
44
|
-
class ReviewerComment:
|
|
45
|
-
"""One review comment left by a reviewer bot.
|
|
46
|
-
|
|
47
|
-
Attributes:
|
|
48
|
-
pull_number: The pull request the comment was left on.
|
|
49
|
-
author: The reviewer bot's login.
|
|
50
|
-
body: The comment text.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
pull_number: int
|
|
54
|
-
author: str
|
|
55
|
-
body: str
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclass(frozen=True)
|
|
59
|
-
class DefectCluster:
|
|
60
|
-
"""The reviewer comments that fall into one defect class.
|
|
61
|
-
|
|
62
|
-
Attributes:
|
|
63
|
-
defect_class: The class name the comments were sorted into.
|
|
64
|
-
count: How many comments fell into the class.
|
|
65
|
-
example_bodies: A capped sample of the comment bodies in the class.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
defect_class: str
|
|
69
|
-
count: int
|
|
70
|
-
example_bodies: tuple[str, ...]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@dataclass(frozen=True)
|
|
74
|
-
class SkillProposal:
|
|
75
|
-
"""A proposed skill-definition edit that blocks one defect class upstream.
|
|
76
|
-
|
|
77
|
-
Attributes:
|
|
78
|
-
defect_class: The defect class the edit would block.
|
|
79
|
-
proposal: The concrete edit to make.
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
defect_class: str
|
|
83
|
-
proposal: str
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def classify_defect(comment_body: str) -> str | None:
|
|
87
|
-
"""Sort a review comment into a defect class by its keywords.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
comment_body: The review comment text.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
The first defect class whose keywords appear in the comment, or None
|
|
94
|
-
when no class matches.
|
|
95
|
-
"""
|
|
96
|
-
lowered_body = comment_body.lower()
|
|
97
|
-
for each_defect_class, each_keywords in KEYWORDS_BY_DEFECT_CLASS.items():
|
|
98
|
-
if any(each_keyword in lowered_body for each_keyword in each_keywords):
|
|
99
|
-
return each_defect_class
|
|
100
|
-
return None
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def cluster_defects(all_comments: list[ReviewerComment]) -> list[DefectCluster]:
|
|
104
|
-
"""Group review comments by defect class and rank by how often each recurs.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
all_comments: The reviewer comments to sort.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
One cluster per defect class present, most frequent first, each with a
|
|
111
|
-
capped sample of its comment bodies.
|
|
112
|
-
"""
|
|
113
|
-
bodies_by_class: dict[str, list[str]] = defaultdict(list)
|
|
114
|
-
for each_comment in all_comments:
|
|
115
|
-
defect_class = classify_defect(each_comment.body)
|
|
116
|
-
if defect_class is None:
|
|
117
|
-
continue
|
|
118
|
-
bodies_by_class[defect_class].append(each_comment.body)
|
|
119
|
-
clusters: list[DefectCluster] = []
|
|
120
|
-
for each_defect_class, each_bodies in bodies_by_class.items():
|
|
121
|
-
clusters.append(
|
|
122
|
-
DefectCluster(
|
|
123
|
-
defect_class=each_defect_class,
|
|
124
|
-
count=len(each_bodies),
|
|
125
|
-
example_bodies=tuple(each_bodies[:MAX_EXAMPLES_PER_CLUSTER]),
|
|
126
|
-
)
|
|
127
|
-
)
|
|
128
|
-
return sorted(clusters, key=lambda cluster: cluster.count, reverse=True)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def proposal_for_defect_class(defect_class: str) -> SkillProposal:
|
|
132
|
-
"""Return the skill-edit proposal that blocks a defect class upstream.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
defect_class: The defect class to propose an edit for.
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
The proposal mapped to the class.
|
|
139
|
-
"""
|
|
140
|
-
return SkillProposal(
|
|
141
|
-
defect_class=defect_class, proposal=PROPOSAL_BY_DEFECT_CLASS[defect_class]
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _pull_number_from_url(pull_request_url: str) -> int | None:
|
|
146
|
-
"""Read the trailing pull number from a GitHub pull-request URL."""
|
|
147
|
-
last_segment = pull_request_url.rsplit("/", 1)[-1]
|
|
148
|
-
try:
|
|
149
|
-
return int(last_segment)
|
|
150
|
-
except ValueError:
|
|
151
|
-
return None
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _reviewer_comment_from_payload(payload: object) -> ReviewerComment | None:
|
|
155
|
-
"""Build a ReviewerComment from one gh comment payload, or None to skip it."""
|
|
156
|
-
if not isinstance(payload, dict):
|
|
157
|
-
return None
|
|
158
|
-
user_field = payload.get("user")
|
|
159
|
-
login = user_field.get("login") if isinstance(user_field, dict) else None
|
|
160
|
-
body = payload.get("body")
|
|
161
|
-
pull_request_url = payload.get("pull_request_url")
|
|
162
|
-
if not isinstance(login, str) or login not in ALL_REVIEWER_BOT_LOGINS:
|
|
163
|
-
return None
|
|
164
|
-
if not isinstance(body, str) or not isinstance(pull_request_url, str):
|
|
165
|
-
return None
|
|
166
|
-
pull_number = _pull_number_from_url(pull_request_url)
|
|
167
|
-
if pull_number is None:
|
|
168
|
-
return None
|
|
169
|
-
return ReviewerComment(pull_number=pull_number, author=login, body=body)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _flatten_pages(slurped_payload: object) -> list[object]:
|
|
173
|
-
"""Flatten the per-page arrays a gh --slurp response holds into one list.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
slurped_payload: The parsed output of a ``gh api --paginate --slurp``
|
|
177
|
-
call: a list whose entries are the individual page responses.
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
Every entry across the pages in page order. A page that is itself a list
|
|
181
|
-
contributes its entries; any other page contributes itself.
|
|
182
|
-
"""
|
|
183
|
-
flattened_entries: list[object] = []
|
|
184
|
-
if isinstance(slurped_payload, list):
|
|
185
|
-
for each_page in slurped_payload:
|
|
186
|
-
if isinstance(each_page, list):
|
|
187
|
-
flattened_entries.extend(each_page)
|
|
188
|
-
else:
|
|
189
|
-
flattened_entries.append(each_page)
|
|
190
|
-
return flattened_entries
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _run_gh_json(endpoint: str, *, should_paginate: bool = False) -> object:
|
|
194
|
-
"""Run one gh api call for an endpoint and return its parsed JSON payload.
|
|
195
|
-
|
|
196
|
-
Args:
|
|
197
|
-
endpoint: The gh api endpoint path to request.
|
|
198
|
-
should_paginate: When True, follow pagination with ``--paginate
|
|
199
|
-
--slurp`` and flatten the per-page arrays into one list, so a list
|
|
200
|
-
endpoint with more than one page returns every entry rather than
|
|
201
|
-
only the first page.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
The parsed JSON payload: the flattened entry list when should_paginate
|
|
205
|
-
is True, otherwise the single response parsed as-is.
|
|
206
|
-
|
|
207
|
-
Raises:
|
|
208
|
-
GitHubCommandError: When gh is not installed, the gh api call fails, or
|
|
209
|
-
its output is not JSON.
|
|
210
|
-
"""
|
|
211
|
-
gh_command = ["gh", "api", endpoint]
|
|
212
|
-
if should_paginate:
|
|
213
|
-
gh_command += ["--paginate", "--slurp"]
|
|
214
|
-
try:
|
|
215
|
-
completed_process = subprocess.run(
|
|
216
|
-
gh_command,
|
|
217
|
-
capture_output=True,
|
|
218
|
-
text=True,
|
|
219
|
-
check=True,
|
|
220
|
-
)
|
|
221
|
-
except FileNotFoundError as missing_gh_error:
|
|
222
|
-
raise GitHubCommandError(
|
|
223
|
-
"gh command not found; install the GitHub CLI to mine reviewer comments"
|
|
224
|
-
) from missing_gh_error
|
|
225
|
-
except subprocess.CalledProcessError as gh_failure_error:
|
|
226
|
-
raise GitHubCommandError(
|
|
227
|
-
f"gh api {endpoint} failed: {gh_failure_error.stderr.strip()}"
|
|
228
|
-
) from gh_failure_error
|
|
229
|
-
try:
|
|
230
|
-
parsed_payload = json.loads(completed_process.stdout)
|
|
231
|
-
except json.JSONDecodeError as decode_error:
|
|
232
|
-
raise GitHubCommandError(
|
|
233
|
-
f"gh api {endpoint} returned output that is not JSON"
|
|
234
|
-
) from decode_error
|
|
235
|
-
if should_paginate:
|
|
236
|
-
return _flatten_pages(parsed_payload)
|
|
237
|
-
return parsed_payload
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _fetch_recent_pull_numbers(repo: str, pull_count: int) -> list[int]:
|
|
241
|
-
"""Fetch the most recently updated pull numbers, capped at pull_count."""
|
|
242
|
-
pulls_payload = _run_gh_json(
|
|
243
|
-
RECENT_PULLS_ENDPOINT_TEMPLATE.format(repo=repo, pull_count=pull_count)
|
|
244
|
-
)
|
|
245
|
-
recent_pull_numbers: list[int] = []
|
|
246
|
-
if isinstance(pulls_payload, list):
|
|
247
|
-
for each_pull in pulls_payload:
|
|
248
|
-
if isinstance(each_pull, dict):
|
|
249
|
-
pull_number = each_pull.get("number")
|
|
250
|
-
if isinstance(pull_number, int):
|
|
251
|
-
recent_pull_numbers.append(pull_number)
|
|
252
|
-
return recent_pull_numbers
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _fetch_pull_comments(repo: str, pull_number: int) -> list[object]:
|
|
256
|
-
"""Fetch one pull request's review comments through gh."""
|
|
257
|
-
comments_payload = _run_gh_json(
|
|
258
|
-
PULL_COMMENTS_ENDPOINT_TEMPLATE.format(repo=repo, pull_number=pull_number),
|
|
259
|
-
should_paginate=True,
|
|
260
|
-
)
|
|
261
|
-
if isinstance(comments_payload, list):
|
|
262
|
-
return list(comments_payload)
|
|
263
|
-
return []
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _fetch_reviewer_comments(repo: str) -> list[ReviewerComment]:
|
|
267
|
-
"""Fetch reviewer-bot comments from the repo's most recent pull requests."""
|
|
268
|
-
recent_pull_numbers = _fetch_recent_pull_numbers(repo, RECENT_PULL_COUNT)
|
|
269
|
-
reviewer_comments: list[ReviewerComment] = []
|
|
270
|
-
for each_pull_number in recent_pull_numbers:
|
|
271
|
-
for each_payload in _fetch_pull_comments(repo, each_pull_number):
|
|
272
|
-
parsed_comment = _reviewer_comment_from_payload(each_payload)
|
|
273
|
-
if parsed_comment is not None:
|
|
274
|
-
reviewer_comments.append(parsed_comment)
|
|
275
|
-
return reviewer_comments
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def main() -> int:
|
|
279
|
-
"""Print one skill-edit proposal per recurring reviewer defect class.
|
|
280
|
-
|
|
281
|
-
Returns:
|
|
282
|
-
The process exit code; zero on success.
|
|
283
|
-
"""
|
|
284
|
-
parser = argparse.ArgumentParser(
|
|
285
|
-
description="Mine reviewer defect patterns into skill-edit proposals."
|
|
286
|
-
)
|
|
287
|
-
parser.add_argument("--repo", required=True)
|
|
288
|
-
parsed_arguments = parser.parse_args()
|
|
289
|
-
try:
|
|
290
|
-
comments = _fetch_reviewer_comments(parsed_arguments.repo)
|
|
291
|
-
except GitHubCommandError as gh_error:
|
|
292
|
-
print(str(gh_error), file=sys.stderr)
|
|
293
|
-
return 1
|
|
294
|
-
clusters = cluster_defects(comments)
|
|
295
|
-
for each_cluster in clusters:
|
|
296
|
-
proposal = proposal_for_defect_class(each_cluster.defect_class)
|
|
297
|
-
print(f"{each_cluster.count}\t{each_cluster.defect_class}\t{proposal.proposal}")
|
|
298
|
-
return 0
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if __name__ == "__main__":
|
|
302
|
-
raise SystemExit(main())
|