claude-dev-env 1.93.0 → 1.94.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/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/code_rules_gate.py +127 -13
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -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 +1 -1
- package/bin/install.mjs +2 -7
- package/bin/install.test.mjs +10 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- 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/pii_scanner.py +26 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_pii_scanner.py +27 -2
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +20 -8
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/package.json +1 -1
- 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 +3 -25
- 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 +284 -94
- 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/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/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 +12 -8
- package/skills/orchestrator-refresh/SKILL.md +3 -3
- 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/privacy-hygiene/SKILL.md +8 -7
- 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/usage-pause/SKILL.md +10 -14
- 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-fix-protocol/SKILL.md +0 -65
- 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,261 +0,0 @@
|
|
|
1
|
-
"""Group hook-block records by a normalized signature and rank the clusters.
|
|
2
|
-
|
|
3
|
-
Two failures can read differently only because one names line 12 and the other
|
|
4
|
-
line 348. They are the same failure. This script strips the varying parts down
|
|
5
|
-
to one signature, so it counts how often each real failure recurs. It ranks the
|
|
6
|
-
loudest first and weights recent blocks over stale ones. It also reads timing
|
|
7
|
-
samples for one operation and flags that operation when its recent runs have
|
|
8
|
-
grown slower than its earlier runs.
|
|
9
|
-
|
|
10
|
-
Reads the JSON record list collect_log_window prints, on stdin.
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
collect_log_window.py --hours 24 | cluster_recurrences.py
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import re
|
|
20
|
-
import sys
|
|
21
|
-
from collections import defaultdict
|
|
22
|
-
from dataclasses import dataclass
|
|
23
|
-
from datetime import datetime
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
|
|
26
|
-
_script_directory = str(Path(__file__).resolve().parent)
|
|
27
|
-
if _script_directory not in sys.path:
|
|
28
|
-
sys.path.insert(0, _script_directory)
|
|
29
|
-
|
|
30
|
-
from log_audit_constants.cluster_recurrences_constants import ( # noqa: E402
|
|
31
|
-
DIGIT_PATTERN,
|
|
32
|
-
HASH_PATTERN,
|
|
33
|
-
MIN_TIMING_SAMPLES_PER_HALF,
|
|
34
|
-
PATH_PATTERN,
|
|
35
|
-
RECENCY_DECAY_BASE,
|
|
36
|
-
RECENCY_HALF_LIFE_HOURS,
|
|
37
|
-
SAMPLE_HALVES,
|
|
38
|
-
SECONDS_PER_HOUR,
|
|
39
|
-
SIGNATURE_PLACEHOLDER,
|
|
40
|
-
TIMING_REGRESSION_RATIO,
|
|
41
|
-
)
|
|
42
|
-
from log_audit_constants.collect_log_window_constants import ( # noqa: E402
|
|
43
|
-
RECORD_LEVEL_KEY,
|
|
44
|
-
RECORD_MESSAGE_KEY,
|
|
45
|
-
RECORD_SOURCE_KEY,
|
|
46
|
-
RECORD_TIMESTAMP_KEY,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
from collect_log_window import LogRecord # noqa: E402
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass(frozen=True)
|
|
53
|
-
class SignatureCluster:
|
|
54
|
-
"""A group of records that share one normalized message signature.
|
|
55
|
-
|
|
56
|
-
Attributes:
|
|
57
|
-
signature: The normalized message the grouped records share.
|
|
58
|
-
count: How many records fell into the cluster.
|
|
59
|
-
latest_timestamp: The most recent record's timestamp in the cluster.
|
|
60
|
-
score: The recency-weighted sum of the cluster's records.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
signature: str
|
|
64
|
-
count: int
|
|
65
|
-
latest_timestamp: datetime
|
|
66
|
-
score: float
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@dataclass(frozen=True)
|
|
70
|
-
class TimingSample:
|
|
71
|
-
"""One measured run of a repeated operation.
|
|
72
|
-
|
|
73
|
-
Attributes:
|
|
74
|
-
operation: The operation's name.
|
|
75
|
-
timestamp: When the run happened.
|
|
76
|
-
duration_ms: How long the run took, in milliseconds.
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
operation: str
|
|
80
|
-
timestamp: datetime
|
|
81
|
-
duration_ms: float
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@dataclass(frozen=True)
|
|
85
|
-
class TimingRegression:
|
|
86
|
-
"""A repeated operation whose recent runs have grown slower.
|
|
87
|
-
|
|
88
|
-
Attributes:
|
|
89
|
-
operation: The operation's name.
|
|
90
|
-
baseline_ms: The mean duration of the earliest runs.
|
|
91
|
-
recent_ms: The mean duration of the latest runs.
|
|
92
|
-
ratio: The recent mean divided by the baseline mean.
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
operation: str
|
|
96
|
-
baseline_ms: float
|
|
97
|
-
recent_ms: float
|
|
98
|
-
ratio: float
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def normalize_signature(message: str) -> str:
|
|
102
|
-
"""Reduce a message to a signature shared by its recurring variants.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
message: A raw block-reason message.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
The message with paths, hashes, and numbers replaced by a placeholder
|
|
109
|
-
and runs of whitespace collapsed to single spaces.
|
|
110
|
-
"""
|
|
111
|
-
without_paths = re.sub(PATH_PATTERN, SIGNATURE_PLACEHOLDER, message)
|
|
112
|
-
without_hashes = re.sub(HASH_PATTERN, SIGNATURE_PLACEHOLDER, without_paths)
|
|
113
|
-
without_numbers = re.sub(DIGIT_PATTERN, SIGNATURE_PLACEHOLDER, without_hashes)
|
|
114
|
-
return re.sub(r"\s+", " ", without_numbers).strip()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def recency_weight(record_timestamp: datetime, now: datetime) -> float:
|
|
118
|
-
"""Weight a record by how recent it is, halving each half-life of age.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
record_timestamp: When the record was written.
|
|
122
|
-
now: The current time age is measured against.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
A weight near one for a fresh record, falling toward zero as it ages.
|
|
126
|
-
"""
|
|
127
|
-
age_hours = (now - record_timestamp).total_seconds() / SECONDS_PER_HOUR
|
|
128
|
-
non_negative_age_hours = max(age_hours, 0.0)
|
|
129
|
-
return RECENCY_DECAY_BASE ** (non_negative_age_hours / RECENCY_HALF_LIFE_HOURS)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def rank_signature_clusters(
|
|
133
|
-
all_records: list[LogRecord], now: datetime
|
|
134
|
-
) -> list[SignatureCluster]:
|
|
135
|
-
"""Group records by signature and rank them by recency-weighted count.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
all_records: The block records to group.
|
|
139
|
-
now: The current time used to weight each record.
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
One cluster per distinct signature, highest score first.
|
|
143
|
-
"""
|
|
144
|
-
records_by_signature: dict[str, list[LogRecord]] = defaultdict(list)
|
|
145
|
-
for each_record in all_records:
|
|
146
|
-
records_by_signature[normalize_signature(each_record.message)].append(
|
|
147
|
-
each_record
|
|
148
|
-
)
|
|
149
|
-
clusters: list[SignatureCluster] = []
|
|
150
|
-
for each_signature, each_signature_records in records_by_signature.items():
|
|
151
|
-
score = sum(
|
|
152
|
-
recency_weight(each_grouped.timestamp, now)
|
|
153
|
-
for each_grouped in each_signature_records
|
|
154
|
-
)
|
|
155
|
-
latest_timestamp = max(
|
|
156
|
-
each_grouped.timestamp for each_grouped in each_signature_records
|
|
157
|
-
)
|
|
158
|
-
clusters.append(
|
|
159
|
-
SignatureCluster(
|
|
160
|
-
signature=each_signature,
|
|
161
|
-
count=len(each_signature_records),
|
|
162
|
-
latest_timestamp=latest_timestamp,
|
|
163
|
-
score=score,
|
|
164
|
-
)
|
|
165
|
-
)
|
|
166
|
-
return sorted(
|
|
167
|
-
clusters,
|
|
168
|
-
key=lambda cluster: (cluster.score, cluster.count),
|
|
169
|
-
reverse=True,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _mean_duration(all_samples: list[TimingSample]) -> float:
|
|
174
|
-
"""Return the mean duration in milliseconds of the given samples."""
|
|
175
|
-
return sum(each_sample.duration_ms for each_sample in all_samples) / len(
|
|
176
|
-
all_samples
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def detect_timing_regressions(
|
|
181
|
-
all_samples: list[TimingSample],
|
|
182
|
-
) -> list[TimingRegression]:
|
|
183
|
-
"""Flag operations whose recent runs are slower than their earliest runs.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
all_samples: Timing samples across one or more operations.
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
One regression per operation whose recent-to-baseline duration ratio
|
|
190
|
-
reaches the regression threshold, highest ratio first.
|
|
191
|
-
"""
|
|
192
|
-
samples_by_operation: dict[str, list[TimingSample]] = defaultdict(list)
|
|
193
|
-
for each_sample in all_samples:
|
|
194
|
-
samples_by_operation[each_sample.operation].append(each_sample)
|
|
195
|
-
regressions: list[TimingRegression] = []
|
|
196
|
-
for each_operation, each_operation_samples in samples_by_operation.items():
|
|
197
|
-
if len(each_operation_samples) < MIN_TIMING_SAMPLES_PER_HALF * SAMPLE_HALVES:
|
|
198
|
-
continue
|
|
199
|
-
ordered_samples = sorted(
|
|
200
|
-
each_operation_samples, key=lambda sample: sample.timestamp
|
|
201
|
-
)
|
|
202
|
-
baseline_ms = _mean_duration(ordered_samples[:MIN_TIMING_SAMPLES_PER_HALF])
|
|
203
|
-
recent_ms = _mean_duration(ordered_samples[-MIN_TIMING_SAMPLES_PER_HALF:])
|
|
204
|
-
if baseline_ms <= 0:
|
|
205
|
-
continue
|
|
206
|
-
ratio = recent_ms / baseline_ms
|
|
207
|
-
if ratio >= TIMING_REGRESSION_RATIO:
|
|
208
|
-
regressions.append(
|
|
209
|
-
TimingRegression(
|
|
210
|
-
operation=each_operation,
|
|
211
|
-
baseline_ms=baseline_ms,
|
|
212
|
-
recent_ms=recent_ms,
|
|
213
|
-
ratio=ratio,
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
return sorted(regressions, key=lambda regression: regression.ratio, reverse=True)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _record_from_json_dict(all_record_fields: dict[str, str]) -> LogRecord:
|
|
220
|
-
"""Rebuild a LogRecord from one collect_log_window JSON dict."""
|
|
221
|
-
return LogRecord(
|
|
222
|
-
timestamp=datetime.fromisoformat(all_record_fields[RECORD_TIMESTAMP_KEY]),
|
|
223
|
-
source=all_record_fields[RECORD_SOURCE_KEY],
|
|
224
|
-
level=all_record_fields[RECORD_LEVEL_KEY],
|
|
225
|
-
message=all_record_fields[RECORD_MESSAGE_KEY],
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def main() -> int:
|
|
230
|
-
"""Read records on stdin and print ranked signature clusters.
|
|
231
|
-
|
|
232
|
-
Returns:
|
|
233
|
-
The process exit code; zero on success, one when stdin is not a JSON
|
|
234
|
-
list of well-formed records.
|
|
235
|
-
"""
|
|
236
|
-
try:
|
|
237
|
-
records_payload = json.load(sys.stdin)
|
|
238
|
-
except json.JSONDecodeError as decode_error:
|
|
239
|
-
print(f"cluster_recurrences: stdin is not valid JSON: {decode_error}", file=sys.stderr)
|
|
240
|
-
return 1
|
|
241
|
-
if not isinstance(records_payload, list):
|
|
242
|
-
print("cluster_recurrences: stdin JSON must be a list of records", file=sys.stderr)
|
|
243
|
-
return 1
|
|
244
|
-
try:
|
|
245
|
-
records = [
|
|
246
|
-
_record_from_json_dict(each_record_fields)
|
|
247
|
-
for each_record_fields in records_payload
|
|
248
|
-
]
|
|
249
|
-
except (KeyError, TypeError, ValueError) as record_error:
|
|
250
|
-
print(f"cluster_recurrences: malformed record in stdin: {record_error}", file=sys.stderr)
|
|
251
|
-
return 1
|
|
252
|
-
clusters = rank_signature_clusters(records, datetime.now())
|
|
253
|
-
for each_cluster in clusters:
|
|
254
|
-
print(
|
|
255
|
-
f"{each_cluster.count}\t{each_cluster.score:.2f}\t{each_cluster.signature}"
|
|
256
|
-
)
|
|
257
|
-
return 0
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if __name__ == "__main__":
|
|
261
|
-
raise SystemExit(main())
|
|
@@ -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
|
-
}
|