claude-dev-env 8.72.2 → 8.73.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/.agents/skills/build-eval/SKILL.md +4 -8
- package/.agents/skills/build-eval/reference/context-budget.md +49 -0
- package/.agents/skills/orchestrator/reference/oversee-delegated-work.md +1 -0
- package/.agents/skills/pr-lifecycle/SKILL.md +3 -10
- package/bin/codex-trimmed-sol.mjs +109 -0
- package/bin/codex-trimmed-sol.test.mjs +226 -0
- package/bin/install.mjs +18 -0
- package/bin/install.question-presentation.test.mjs +1 -1
- package/docs/codex-accounts.md +26 -0
- package/hooks/blocking/context_budget_blocker.py +177 -0
- package/hooks/blocking/test_context_budget_blocker.py +189 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +3 -1
- package/hooks/context_budget/__init__.py +1 -0
- package/hooks/context_budget/baseline.py +78 -0
- package/hooks/context_budget/findings.py +251 -0
- package/hooks/context_budget/hook_measure.py +178 -0
- package/hooks/context_budget/model.py +134 -0
- package/hooks/context_budget/policy_changes.py +214 -0
- package/hooks/context_budget/policy_file.py +222 -0
- package/hooks/context_budget/sections.py +213 -0
- package/hooks/context_budget/tests/budget_support.py +71 -0
- package/hooks/context_budget/tests/test_baseline.py +43 -0
- package/hooks/context_budget/tests/test_findings.py +120 -0
- package/hooks/context_budget/tests/test_hook_measure.py +57 -0
- package/hooks/context_budget/tests/test_policy_changes.py +175 -0
- package/hooks/context_budget/tests/test_policy_file.py +62 -0
- package/hooks/context_budget/tests/test_sections.py +112 -0
- package/hooks/features/write-edit-blocking.md +5 -1
- package/hooks/hooks_constants/context_budget_constants.py +154 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -1
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -2
- package/hooks/test_context_budget_hooks.py +79 -0
- package/hooks/test_files/context_budget/advisor_config/settings.json +3 -0
- package/hooks/test_files/context_budget/empty_transcript.jsonl +0 -0
- package/hooks/test_files/context_budget/orchestrator_skill_3f2c3ef.txt +170 -0
- package/hooks/test_files/context_budget/orchestrator_skill_98f45b1.txt +158 -0
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +7 -12
- package/scripts/context_budget_check.py +243 -0
- package/scripts/dev_env_scripts_constants/context_budget_check_constants.py +26 -0
- package/scripts/policy_lint/adapter_context_budget.py +158 -0
- package/scripts/policy_lint/adapters.py +73 -0
- package/scripts/policy_lint/config/constants.py +17 -1
- package/scripts/policy_lint/registry.py +17 -0
- package/scripts/tests/test_context_budget_check.py +270 -0
- package/scripts/tests/test_policy_lint_rules_context_budget.py +218 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +26 -0
- package/system-prompts/codex-sol.md +170 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Answer "is this context file thin enough?" and keep the over-budget list current.
|
|
3
|
+
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
python context_budget_check.py path/to/SKILL.md check files, prior = HEAD
|
|
7
|
+
python context_budget_check.py --hooks measure the policy's hooks
|
|
8
|
+
python context_budget_check.py --write-baseline rebuild the over-budget list
|
|
9
|
+
|
|
10
|
+
Run it from inside the repository whose ``.claude/context-budget.json`` applies.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from pathlib import Path, PurePosixPath
|
|
19
|
+
from typing import TextIO
|
|
20
|
+
|
|
21
|
+
from dev_env_scripts_constants.context_budget_check_constants import (
|
|
22
|
+
ALL_GIT_ROOT_ARGUMENTS,
|
|
23
|
+
ALL_LS_FILES_ARGUMENTS,
|
|
24
|
+
BASELINE_WRITTEN_MESSAGE,
|
|
25
|
+
CLEAN_MESSAGE,
|
|
26
|
+
COMMAND_DESCRIPTION,
|
|
27
|
+
FAILURE_EXIT_CODE,
|
|
28
|
+
HEAD_REVISION,
|
|
29
|
+
HOOK_REPORT_TEMPLATE,
|
|
30
|
+
HOOKS_FLAG,
|
|
31
|
+
HOOKS_HELP,
|
|
32
|
+
MARKDOWN_SUFFIX,
|
|
33
|
+
NO_BASELINE_TEXT,
|
|
34
|
+
NO_POLICY_MESSAGE,
|
|
35
|
+
PATHS_HELP,
|
|
36
|
+
POLICY_RELATIVE_PATH,
|
|
37
|
+
SUCCESS_EXIT_CODE,
|
|
38
|
+
UTF8_ENCODING,
|
|
39
|
+
WRITE_BASELINE_FLAG,
|
|
40
|
+
WRITE_BASELINE_HELP,
|
|
41
|
+
)
|
|
42
|
+
from policy_lint import adapters
|
|
43
|
+
from policy_lint.model import ContentOrigin, Diagnostic, Document
|
|
44
|
+
from policy_lint.selection_git import git_bytes_for, read_blob, split_nul_tokens
|
|
45
|
+
|
|
46
|
+
_hooks_directory = str(Path(__file__).resolve().parents[1] / "hooks")
|
|
47
|
+
if _hooks_directory not in sys.path:
|
|
48
|
+
sys.path.insert(0, _hooks_directory)
|
|
49
|
+
|
|
50
|
+
from context_budget.baseline import build_baseline, policy_text_with_baseline
|
|
51
|
+
from context_budget.hook_measure import (
|
|
52
|
+
HookMeasurementError,
|
|
53
|
+
hook_findings,
|
|
54
|
+
injected_character_count,
|
|
55
|
+
run_hook_command,
|
|
56
|
+
)
|
|
57
|
+
from context_budget.model import BudgetPolicy, ContextBudgetPolicyError
|
|
58
|
+
from context_budget.policy_file import read_policy_file
|
|
59
|
+
from context_budget.sections import context_kind_for_path
|
|
60
|
+
from hooks_constants.context_budget_constants import BASELINE_FILES_KEY, BASELINE_HOOKS_KEY
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
64
|
+
"""Return the command's argument parser.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The parser for paths, ``--hooks``, and ``--write-baseline``.
|
|
68
|
+
"""
|
|
69
|
+
parser = argparse.ArgumentParser(description=COMMAND_DESCRIPTION)
|
|
70
|
+
parser.add_argument("paths", nargs="*", help=PATHS_HELP)
|
|
71
|
+
parser.add_argument(HOOKS_FLAG, action="store_true", help=HOOKS_HELP)
|
|
72
|
+
parser.add_argument(WRITE_BASELINE_FLAG, action="store_true", help=WRITE_BASELINE_HELP)
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _repository_root(starting_directory: Path) -> Path:
|
|
77
|
+
root_bytes = git_bytes_for(starting_directory, ALL_GIT_ROOT_ARGUMENTS)
|
|
78
|
+
return Path(root_bytes.decode(UTF8_ENCODING).strip()).resolve()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _document_for(repository_root: Path, file_path: Path) -> Document:
|
|
82
|
+
relative_path = file_path.resolve().relative_to(repository_root).as_posix()
|
|
83
|
+
return Document(
|
|
84
|
+
PurePosixPath(relative_path),
|
|
85
|
+
file_path.read_text(encoding=UTF8_ENCODING),
|
|
86
|
+
read_blob(repository_root, HEAD_REVISION, relative_path),
|
|
87
|
+
None,
|
|
88
|
+
ContentOrigin.WORKTREE,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _diagnostics_for(repository_root: Path, file_path: Path) -> tuple[Diagnostic, ...]:
|
|
93
|
+
document = _document_for(repository_root, file_path)
|
|
94
|
+
if document.path.as_posix() == POLICY_RELATIVE_PATH:
|
|
95
|
+
return adapters.context_budget_policy_diagnostics(document, repository_root)
|
|
96
|
+
if document.path.suffix == MARKDOWN_SUFFIX:
|
|
97
|
+
return adapters.context_budget_diagnostics(document, repository_root)
|
|
98
|
+
return ()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_paths(repository_root: Path, all_paths: Sequence[str], stdout: TextIO) -> int:
|
|
102
|
+
"""Check explicit files with each file's HEAD version as its prior state.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
repository_root: Repository root holding the policy.
|
|
106
|
+
all_paths: Paths given on the command line.
|
|
107
|
+
stdout: Report stream.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Zero when every file is within budget, else one.
|
|
111
|
+
"""
|
|
112
|
+
all_messages = [
|
|
113
|
+
each_diagnostic.message
|
|
114
|
+
for each_path in all_paths
|
|
115
|
+
for each_diagnostic in _diagnostics_for(repository_root, Path(each_path))
|
|
116
|
+
]
|
|
117
|
+
for each_message in all_messages:
|
|
118
|
+
stdout.write(each_message + "\n")
|
|
119
|
+
if all_messages:
|
|
120
|
+
return FAILURE_EXIT_CODE
|
|
121
|
+
stdout.write(CLEAN_MESSAGE.format(count=len(all_paths)) + "\n")
|
|
122
|
+
return SUCCESS_EXIT_CODE
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _measure_hooks(policy: BudgetPolicy, repository_root: Path) -> dict[str, int]:
|
|
126
|
+
"""Run every hook the policy lists and count the characters each injects.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
policy: Budget policy listing the hooks.
|
|
130
|
+
repository_root: Directory each hook runs from.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Injected characters by hook name.
|
|
134
|
+
"""
|
|
135
|
+
return {
|
|
136
|
+
each_hook.name: injected_character_count(run_hook_command(each_hook, repository_root))
|
|
137
|
+
for each_hook in policy.all_hooks
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_hooks(policy: BudgetPolicy, repository_root: Path, stdout: TextIO) -> int:
|
|
142
|
+
"""Report each hook's injected characters and fail on any over budget.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
policy: Budget policy listing the hooks.
|
|
146
|
+
repository_root: Directory each hook runs from.
|
|
147
|
+
stdout: Report stream.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Zero when every hook is within budget, else one.
|
|
151
|
+
"""
|
|
152
|
+
count_by_name = _measure_hooks(policy, repository_root)
|
|
153
|
+
all_messages: list[str] = []
|
|
154
|
+
for each_hook in policy.all_hooks:
|
|
155
|
+
count = count_by_name[each_hook.name]
|
|
156
|
+
baseline = policy.baseline_characters_by_hook_name.get(each_hook.name, NO_BASELINE_TEXT)
|
|
157
|
+
stdout.write(
|
|
158
|
+
HOOK_REPORT_TEMPLATE.format(
|
|
159
|
+
name=each_hook.name, count=count, limit=each_hook.char_limit, baseline=baseline
|
|
160
|
+
)
|
|
161
|
+
+ "\n"
|
|
162
|
+
)
|
|
163
|
+
all_messages.extend(hook_findings(policy, each_hook, count))
|
|
164
|
+
for each_message in all_messages:
|
|
165
|
+
stdout.write(each_message + "\n")
|
|
166
|
+
return FAILURE_EXIT_CODE if all_messages else SUCCESS_EXIT_CODE
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def current_baseline(policy: BudgetPolicy, repository_root: Path) -> dict[str, dict]:
|
|
170
|
+
"""Build the baseline the tracked tree and the policy's hooks need today.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
policy: Current budget policy.
|
|
174
|
+
repository_root: Repository root holding the policy.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The ``baseline`` object.
|
|
178
|
+
"""
|
|
179
|
+
all_tracked_paths = split_nul_tokens(git_bytes_for(repository_root, ALL_LS_FILES_ARGUMENTS))
|
|
180
|
+
text_by_path = {
|
|
181
|
+
each_path: (repository_root / each_path).read_text(encoding=UTF8_ENCODING)
|
|
182
|
+
for each_path in all_tracked_paths
|
|
183
|
+
if context_kind_for_path(policy, each_path) is not None
|
|
184
|
+
and (repository_root / each_path).is_file()
|
|
185
|
+
}
|
|
186
|
+
return build_baseline(policy, text_by_path, _measure_hooks(policy, repository_root))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _write_baseline(policy: BudgetPolicy, repository_root: Path, stdout: TextIO) -> int:
|
|
190
|
+
"""Rebuild the policy file's baseline from the tracked tree and its hooks.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
policy: Current budget policy.
|
|
194
|
+
repository_root: Repository root holding the policy.
|
|
195
|
+
stdout: Report stream.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Zero after the file is written.
|
|
199
|
+
"""
|
|
200
|
+
baseline = current_baseline(policy, repository_root)
|
|
201
|
+
policy_path = repository_root / POLICY_RELATIVE_PATH
|
|
202
|
+
policy_text = policy_path.read_text(encoding=UTF8_ENCODING)
|
|
203
|
+
policy_path.write_text(policy_text_with_baseline(policy_text, baseline), encoding=UTF8_ENCODING)
|
|
204
|
+
file_count = len(baseline[BASELINE_FILES_KEY])
|
|
205
|
+
hook_count = len(baseline[BASELINE_HOOKS_KEY])
|
|
206
|
+
stdout.write(BASELINE_WRITTEN_MESSAGE.format(files=file_count, hooks=hook_count) + "\n")
|
|
207
|
+
return SUCCESS_EXIT_CODE
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _run_action(namespace: argparse.Namespace, repository_root: Path, stdout: TextIO) -> int:
|
|
211
|
+
policy = read_policy_file(repository_root)
|
|
212
|
+
if policy is None:
|
|
213
|
+
stdout.write(NO_POLICY_MESSAGE + "\n")
|
|
214
|
+
return FAILURE_EXIT_CODE
|
|
215
|
+
if namespace.write_baseline:
|
|
216
|
+
return _write_baseline(policy, repository_root, stdout)
|
|
217
|
+
if namespace.hooks:
|
|
218
|
+
return _check_hooks(policy, repository_root, stdout)
|
|
219
|
+
return _check_paths(repository_root, namespace.paths, stdout)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main(all_arguments: Sequence[str], starting_directory: Path, stdout: TextIO) -> int:
|
|
223
|
+
"""Run the command.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
all_arguments: Command-line arguments.
|
|
227
|
+
starting_directory: Directory inside the repository to check.
|
|
228
|
+
stdout: Report stream.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
The exit code of the requested action.
|
|
232
|
+
"""
|
|
233
|
+
namespace = build_parser().parse_args(list(all_arguments))
|
|
234
|
+
repository_root = _repository_root(starting_directory)
|
|
235
|
+
try:
|
|
236
|
+
return _run_action(namespace, repository_root, stdout)
|
|
237
|
+
except (ContextBudgetPolicyError, HookMeasurementError) as error:
|
|
238
|
+
stdout.write(str(error) + "\n")
|
|
239
|
+
return FAILURE_EXIT_CODE
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
raise SystemExit(main(sys.argv[1:], Path.cwd(), sys.stdout))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Constants for the context budget command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
COMMAND_DESCRIPTION = (
|
|
6
|
+
"Check context files against .claude/context-budget.json, measure its hooks, "
|
|
7
|
+
"or rebuild its over-budget list from the current tree."
|
|
8
|
+
)
|
|
9
|
+
PATHS_HELP = "Context files to check; the prior state is each file's HEAD version."
|
|
10
|
+
HOOKS_FLAG = "--hooks"
|
|
11
|
+
HOOKS_HELP = "Run each hook the policy lists and hold it to its character budget."
|
|
12
|
+
WRITE_BASELINE_FLAG = "--write-baseline"
|
|
13
|
+
WRITE_BASELINE_HELP = "Rebuild the policy's baseline from the tracked tree and its hooks."
|
|
14
|
+
HEAD_REVISION = "HEAD"
|
|
15
|
+
ALL_LS_FILES_ARGUMENTS: tuple[str, ...] = ("ls-files", "-z")
|
|
16
|
+
ALL_GIT_ROOT_ARGUMENTS: tuple[str, ...] = ("rev-parse", "--show-toplevel")
|
|
17
|
+
POLICY_RELATIVE_PATH = ".claude/context-budget.json"
|
|
18
|
+
MARKDOWN_SUFFIX = ".md"
|
|
19
|
+
UTF8_ENCODING = "utf-8"
|
|
20
|
+
NO_POLICY_MESSAGE = "context-budget: no " + POLICY_RELATIVE_PATH + " in this repository."
|
|
21
|
+
CLEAN_MESSAGE = "context-budget: OK ({count} checked)"
|
|
22
|
+
HOOK_REPORT_TEMPLATE = "{name}: {count} characters (limit {limit}, baseline {baseline})"
|
|
23
|
+
BASELINE_WRITTEN_MESSAGE = "context-budget: baseline written ({files} files, {hooks} hooks)"
|
|
24
|
+
NO_BASELINE_TEXT = "none"
|
|
25
|
+
FAILURE_EXIT_CODE = 1
|
|
26
|
+
SUCCESS_EXIT_CODE = 0
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Hold changed agent context files and the over-budget list to the context budget.
|
|
2
|
+
|
|
3
|
+
The budget core lives in ``hooks/context_budget``, shared with the write hook.
|
|
4
|
+
The policy is ``.claude/context-budget.json`` at the repository root; a
|
|
5
|
+
repository without one is not checked.
|
|
6
|
+
|
|
7
|
+
::
|
|
8
|
+
|
|
9
|
+
context-budget SKILL.md over its limit grew -> finding
|
|
10
|
+
context-budget-policy baseline entry raised -> finding
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
|
|
18
|
+
from .adapter_support import HookModuleLoader
|
|
19
|
+
from .config import constants
|
|
20
|
+
from .model import Diagnostic, Document, DocumentSet, Severity
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def accepts_context_markdown(document: Document) -> bool:
|
|
24
|
+
"""Return whether the document is Markdown the kind table may select.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
document: Candidate document.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True for Markdown documents.
|
|
31
|
+
"""
|
|
32
|
+
return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def accepts_context_budget_policy(document: Document) -> bool:
|
|
36
|
+
"""Return whether the document is the repository's budget policy file.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
document: Candidate document.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True for ``.claude/context-budget.json``.
|
|
43
|
+
"""
|
|
44
|
+
return document.path.as_posix() == constants.CONTEXT_BUDGET_POLICY_PATH
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _file_diagnostic(rule_id: str, message: str) -> Diagnostic:
|
|
48
|
+
return Diagnostic(rule_id, Severity.ERROR, message)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def context_budget_diagnostics(
|
|
52
|
+
document: Document, repository_root: Path, load_module: HookModuleLoader
|
|
53
|
+
) -> tuple[Diagnostic, ...]:
|
|
54
|
+
"""Report the budget findings of one changed context file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
document: Current text and the prior text the ratchet compares.
|
|
58
|
+
repository_root: Repository root holding the policy file.
|
|
59
|
+
load_module: Hook module loader.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
One diagnostic per finding; empty without a readable policy file.
|
|
63
|
+
"""
|
|
64
|
+
policy_file = load_module(constants.CONTEXT_BUDGET_POLICY_FILE_MODULE)
|
|
65
|
+
model = load_module(constants.CONTEXT_BUDGET_MODEL_MODULE)
|
|
66
|
+
try:
|
|
67
|
+
policy = policy_file.read_policy_file(repository_root)
|
|
68
|
+
except model.ContextBudgetPolicyError:
|
|
69
|
+
return ()
|
|
70
|
+
if policy is None:
|
|
71
|
+
return ()
|
|
72
|
+
findings = load_module(constants.CONTEXT_BUDGET_FINDINGS_MODULE)
|
|
73
|
+
all_findings = findings.file_findings(
|
|
74
|
+
policy, document.path.as_posix(), document.text, document.prior_text
|
|
75
|
+
)
|
|
76
|
+
return tuple(
|
|
77
|
+
_file_diagnostic(constants.CONTEXT_BUDGET_RULE_ID, each.message)
|
|
78
|
+
for each in all_findings
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _prior_policy(prior_text: str | None, policy_file: ModuleType, model: ModuleType) -> object:
|
|
83
|
+
if prior_text is None:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return policy_file.parse_policy(prior_text)
|
|
87
|
+
except model.ContextBudgetPolicyError:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _policy_messages(
|
|
92
|
+
document: Document, repository_root: Path, load_module: HookModuleLoader
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
policy_file = load_module(constants.CONTEXT_BUDGET_POLICY_FILE_MODULE)
|
|
95
|
+
model = load_module(constants.CONTEXT_BUDGET_MODEL_MODULE)
|
|
96
|
+
policy_path = document.path.as_posix()
|
|
97
|
+
try:
|
|
98
|
+
current_policy = policy_file.parse_policy(document.text)
|
|
99
|
+
except model.ContextBudgetPolicyError as error:
|
|
100
|
+
all_messages = [
|
|
101
|
+
constants.CONTEXT_BUDGET_UNREADABLE_MESSAGE.format(path=policy_path, error=error)
|
|
102
|
+
]
|
|
103
|
+
else:
|
|
104
|
+
prior_policy = _prior_policy(document.prior_text, policy_file, model)
|
|
105
|
+
policy_changes = load_module(constants.CONTEXT_BUDGET_POLICY_CHANGES_MODULE)
|
|
106
|
+
all_messages = [
|
|
107
|
+
each.message
|
|
108
|
+
for each in policy_changes.policy_shrink_findings(
|
|
109
|
+
policy_path, prior_policy, current_policy, repository_root
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
return all_messages
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def context_budget_policy_diagnostics(
|
|
116
|
+
document: Document, repository_root: Path, load_module: HookModuleLoader
|
|
117
|
+
) -> tuple[Diagnostic, ...]:
|
|
118
|
+
"""Report each change to the policy file that loosens the budget.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
document: Current policy text and its prior text.
|
|
122
|
+
repository_root: Working tree that holds each hook's command script.
|
|
123
|
+
load_module: Hook module loader.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
One diagnostic per loosening change, or one for an unparseable file.
|
|
127
|
+
"""
|
|
128
|
+
rule_id = constants.CONTEXT_BUDGET_POLICY_RULE_ID
|
|
129
|
+
all_messages = _policy_messages(document, repository_root, load_module)
|
|
130
|
+
return tuple(_file_diagnostic(rule_id, each) for each in all_messages)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def context_budget_policy_change_set_diagnostics(
|
|
134
|
+
document_set: DocumentSet,
|
|
135
|
+
) -> tuple[Diagnostic, ...]:
|
|
136
|
+
"""Reject removal or movement of the policy path in a Git selection.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
document_set: Changed documents and removed or renamed paths.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
One finding when the selection removes the policy path.
|
|
143
|
+
"""
|
|
144
|
+
policy_path = PurePosixPath(constants.CONTEXT_BUDGET_POLICY_PATH)
|
|
145
|
+
removed_paths = set(document_set.deleted_paths)
|
|
146
|
+
removed_paths.update(
|
|
147
|
+
old_path for old_path, new_path in document_set.renamed_paths if old_path != new_path
|
|
148
|
+
)
|
|
149
|
+
if policy_path not in removed_paths:
|
|
150
|
+
return ()
|
|
151
|
+
return (
|
|
152
|
+
Diagnostic(
|
|
153
|
+
constants.CONTEXT_BUDGET_POLICY_RULE_ID,
|
|
154
|
+
Severity.ERROR,
|
|
155
|
+
constants.CONTEXT_BUDGET_POLICY_REMOVED_MESSAGE.format(path=policy_path),
|
|
156
|
+
check_id=constants.CONTEXT_BUDGET_POLICY_REMOVAL_RULE_ID,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
@@ -11,6 +11,7 @@ from shared_tree_paths import resolve_shared_scripts_directory
|
|
|
11
11
|
from . import (
|
|
12
12
|
adapter_callers,
|
|
13
13
|
adapter_configuration,
|
|
14
|
+
adapter_context_budget,
|
|
14
15
|
adapter_contrast_framing,
|
|
15
16
|
adapter_detectors,
|
|
16
17
|
adapter_pairing,
|
|
@@ -498,3 +499,75 @@ def retired_hook_prose_diagnostics(
|
|
|
498
499
|
return adapter_retired_hook_prose.retired_hook_prose_diagnostics(
|
|
499
500
|
document, repository_root
|
|
500
501
|
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def accepts_context_markdown(document: Document) -> bool:
|
|
505
|
+
"""Return whether the document is Markdown the context budget may select.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
document: Candidate document.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
True for Markdown documents.
|
|
512
|
+
"""
|
|
513
|
+
return adapter_context_budget.accepts_context_markdown(document)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def accepts_context_budget_policy(document: Document) -> bool:
|
|
517
|
+
"""Return whether the document is the context budget policy file.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
document: Candidate document.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True for ``.claude/context-budget.json``.
|
|
524
|
+
"""
|
|
525
|
+
return adapter_context_budget.accepts_context_budget_policy(document)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def context_budget_diagnostics(
|
|
529
|
+
document: Document, repository_root: Path
|
|
530
|
+
) -> tuple[Diagnostic, ...]:
|
|
531
|
+
"""Hold one changed context file to its kind limits and the over-budget list.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
document: Current Markdown text and its prior text.
|
|
535
|
+
repository_root: Repository root holding the policy file.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Context budget diagnostics.
|
|
539
|
+
"""
|
|
540
|
+
return adapter_context_budget.context_budget_diagnostics(
|
|
541
|
+
document, repository_root, _hooks_module
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def context_budget_policy_diagnostics(
|
|
546
|
+
document: Document, repository_root: Path
|
|
547
|
+
) -> tuple[Diagnostic, ...]:
|
|
548
|
+
"""Report a policy file change that loosens the context budget.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
document: Current policy text and its prior text.
|
|
552
|
+
repository_root: Request repository root.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Shrink-only diagnostics.
|
|
556
|
+
"""
|
|
557
|
+
return adapter_context_budget.context_budget_policy_diagnostics(
|
|
558
|
+
document, repository_root, _hooks_module
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def context_budget_policy_change_set_diagnostics(
|
|
563
|
+
document_set: DocumentSet,
|
|
564
|
+
) -> tuple[Diagnostic, ...]:
|
|
565
|
+
"""Reject a Git selection that removes the context budget policy path.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
document_set: Changed documents and removed or renamed paths.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Policy removal diagnostics.
|
|
572
|
+
"""
|
|
573
|
+
return adapter_context_budget.context_budget_policy_change_set_diagnostics(document_set)
|
|
@@ -77,7 +77,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
77
77
|
)
|
|
78
78
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
79
79
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
80
|
-
ALL_TEST_FILE_SUFFIXES = ("_test.py",)
|
|
80
|
+
ALL_TEST_FILE_SUFFIXES = ("_test.py", "_test_support.py")
|
|
81
81
|
POLICY_LINT_DIRECTORY_NAME = "policy_lint"
|
|
82
82
|
POLICY_LINT_RULES_TEST_PREFIX = "test_policy_lint_rules"
|
|
83
83
|
POLICY_LINT_SELECTION_TEST_PREFIX = "test_policy_lint_selection"
|
|
@@ -200,3 +200,19 @@ ALL_INVENTORY_FILE_NAMES = frozenset(
|
|
|
200
200
|
)
|
|
201
201
|
ALL_UNCALLED_EXEMPT_FILE_NAMES = frozenset({"__init__.py", "conftest.py"})
|
|
202
202
|
CALLER_NAME_PATTERN_TEMPLATE = r"(?<![\w-]){name}(?![\w-])"
|
|
203
|
+
|
|
204
|
+
CONTEXT_BUDGET_RULE_ID = "context-budget"
|
|
205
|
+
CONTEXT_BUDGET_POLICY_RULE_ID = "context-budget-policy"
|
|
206
|
+
CONTEXT_BUDGET_POLICY_REMOVAL_RULE_ID = "context-budget-policy-removal"
|
|
207
|
+
CONTEXT_BUDGET_POLICY_PATH = ".claude/context-budget.json"
|
|
208
|
+
CONTEXT_BUDGET_POLICY_REMOVED_MESSAGE = (
|
|
209
|
+
"{path}: context-budget: the policy file is removed. Restore it to preserve the budget."
|
|
210
|
+
)
|
|
211
|
+
CONTEXT_BUDGET_POLICY_FILE_MODULE = "context_budget.policy_file"
|
|
212
|
+
CONTEXT_BUDGET_FINDINGS_MODULE = "context_budget.findings"
|
|
213
|
+
CONTEXT_BUDGET_POLICY_CHANGES_MODULE = "context_budget.policy_changes"
|
|
214
|
+
CONTEXT_BUDGET_MODEL_MODULE = "context_budget.model"
|
|
215
|
+
CONTEXT_BUDGET_UNREADABLE_MESSAGE = (
|
|
216
|
+
"{path}: context-budget: the policy file does not parse ({error}). Fix it to"
|
|
217
|
+
" the documented shape."
|
|
218
|
+
)
|
|
@@ -4,6 +4,7 @@ from collections.abc import Callable
|
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
5
|
|
|
6
6
|
from . import adapters, model
|
|
7
|
+
from .config import constants
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def build_document_rule(
|
|
@@ -114,6 +115,16 @@ def _text_document_rules() -> tuple[model.Rule, ...]:
|
|
|
114
115
|
adapters.accepts_instruction_markdown,
|
|
115
116
|
adapters.retired_hook_prose_diagnostics,
|
|
116
117
|
),
|
|
118
|
+
build_document_rule(
|
|
119
|
+
"context-budget",
|
|
120
|
+
adapters.accepts_context_markdown,
|
|
121
|
+
adapters.context_budget_diagnostics,
|
|
122
|
+
),
|
|
123
|
+
build_document_rule(
|
|
124
|
+
"context-budget-policy",
|
|
125
|
+
adapters.accepts_context_budget_policy,
|
|
126
|
+
adapters.context_budget_policy_diagnostics,
|
|
127
|
+
),
|
|
117
128
|
)
|
|
118
129
|
|
|
119
130
|
|
|
@@ -142,6 +153,12 @@ def default_registry() -> tuple[model.Rule, ...]:
|
|
|
142
153
|
*_python_document_rules(),
|
|
143
154
|
*_text_document_rules(),
|
|
144
155
|
*_configuration_document_rules(),
|
|
156
|
+
model.ChangeSetRule(
|
|
157
|
+
constants.CONTEXT_BUDGET_POLICY_REMOVAL_RULE_ID,
|
|
158
|
+
frozenset({"changed"}),
|
|
159
|
+
frozenset({model.SelectionKind.STAGED, model.SelectionKind.BASE}),
|
|
160
|
+
adapters.context_budget_policy_change_set_diagnostics,
|
|
161
|
+
),
|
|
145
162
|
model.ChangeSetRule(
|
|
146
163
|
"test-pairing",
|
|
147
164
|
frozenset({"changed"}),
|