claude-dev-env 8.73.1 → 8.73.2
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/agents/test_agent_frontmatter.py +23 -23
- package/.agents/agents-archived/clean-coder.md +5 -5
- package/.agents/skills/correction/playbooks/fix.md +1 -1
- package/.agents/skills/eli5/SKILL.md +3 -3
- package/.agents/skills/pr-lifecycle/SKILL.md +1 -1
- package/.agents/skills/recovering-codex-startup/SKILL.md +1 -1
- package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
- package/_shared/advisor/reference/third-party-bind.md +1 -1
- package/bin/install.mjs +1 -0
- package/bin/install.policy-lint.test.mjs +1 -0
- package/bin/install.profile-root.test.mjs +1 -1
- package/bin/install.test.mjs +1 -0
- package/commands/fix-followups.md +1 -1
- package/commands/sr-loop.md +1 -1
- package/docs/references/prose-style-enforcement.md +1 -1
- package/docs/references/working-style.md +1 -1
- package/docs/rule-guides/asd-ste100-language.md +9 -1
- package/docs/rule-guides/cleanup-temp-files.md +8 -2
- package/docs/rule-guides/destructive-commands.md +13 -3
- package/docs/rule-guides/explore-thoroughly.md +9 -1
- package/docs/rule-guides/features-start-with-an-eval.md +8 -2
- package/docs/rule-guides/filesystem-search.md +7 -1
- package/docs/rule-guides/long-horizon-autonomy.md +1 -1
- package/docs/rule-guides/memory-stores-durable-facts.md +9 -3
- package/docs/rule-guides/no-contrast-framing.md +10 -4
- package/docs/rule-guides/prefer-existing-tools.md +2 -2
- package/docs/rule-guides/proof-before-pull-request.md +5 -1
- package/docs/rule-guides/request-scope.md +5 -1
- package/docs/rule-guides/research-mode.md +9 -1
- package/docs/rule-guides/session-title.md +7 -1
- package/docs/rule-guides/shell-invocation.md +9 -1
- package/docs/rule-guides/verify-before-asking.md +7 -1
- package/docs/rule-guides/verify-runtime-state.md +7 -1
- package/hooks/blocking/code_rules_enforcer.py +16 -6
- package/hooks/blocking/code_rules_shared.py +23 -4
- package/hooks/blocking/reply_length_gate.py +74 -14
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +54 -1
- package/hooks/blocking/test_code_rules_shared.py +33 -0
- package/hooks/blocking/test_reply_length_gate.py +184 -5
- package/hooks/blocking/test_state_description_blocker.py +1 -1
- package/hooks/features/conduct-gates.md +3 -6
- package/hooks/hooks.json +1 -6
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/reply_length_gate_constants.py +1 -0
- package/hooks/hooks_constants/session_env_cleanup_constants.py +0 -2
- package/hooks/hooks_constants/test_visual_reply_rules_constants.py +15 -0
- package/hooks/hooks_constants/test_windows_filesystem.py +57 -0
- package/hooks/hooks_constants/visual_reply_rules_constants.py +63 -0
- package/hooks/hooks_constants/windows_filesystem.py +51 -0
- package/hooks/session/session_env_cleanup.py +4 -30
- package/hooks/test_visual_reply_rules.py +164 -0
- package/hooks/visual_reply_rules.py +268 -0
- package/hooks/workflow/test_auto_formatter.py +4 -25
- package/package.json +1 -1
- package/rules/index.md +30 -0
- package/rules/visual-reply-rules.json +41 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/conftest.py +13 -5
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +2 -2
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +12 -0
- package/scripts/policy_lint/adapter_configuration.py +0 -5
- package/scripts/policy_lint/config/constants.py +0 -1
- package/scripts/test_active_capability_references.py +0 -5
- package/scripts/test_broker_state_isolation.py +3 -3
- package/scripts/test_digest_check.py +0 -5
- package/scripts/test_durable_post_lint.py +1 -5
- package/scripts/test_followup_cli.py +0 -6
- package/scripts/test_resolve_worker_spawn.py +5 -10
- package/scripts/test_setup_project_paths.py +0 -12
- package/scripts/test_shared_tree_paths.py +2 -6
- package/scripts/test_validate_instruction_pairs.py +0 -7
- package/scripts/test_verify_installable_package.py +0 -5
- package/scripts/tests/test_contrast_framing.py +3 -3
- package/scripts/tests/test_rule_index_shape.py +59 -5
- package/scripts/tests/test_rule_load_scopes.py +1 -16
- package/system-prompts/software-engineer.xml +4 -4
- package/hooks/blocking/artifact_look_gate.py +0 -223
- package/hooks/blocking/test_artifact_look_gate.py +0 -153
- package/hooks/hooks_constants/artifact_look_gate_constants.py +0 -32
- package/rules/asd-ste100-language.md +0 -9
- package/rules/cleanup-temp-files.md +0 -9
- package/rules/destructive-commands.md +0 -11
- package/rules/explore-thoroughly.md +0 -9
- package/rules/features-start-with-an-eval.md +0 -9
- package/rules/filesystem-search.md +0 -9
- package/rules/memory-stores-durable-facts.md +0 -9
- package/rules/no-contrast-framing.md +0 -9
- package/rules/proof-before-pull-request.md +0 -7
- package/rules/request-scope.md +0 -7
- package/rules/research-mode.md +0 -9
- package/rules/session-title.md +0 -9
- package/rules/shell-invocation.md +0 -9
- package/rules/skill-pointers.md +0 -3
- package/rules/verify-before-asking.md +0 -9
- package/rules/verify-runtime-state.md +0 -9
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""The visual reply mode rule list and the checks the reply length gate runs from it.
|
|
2
|
+
|
|
3
|
+
One JSON file, ``rules/visual-reply-rules.json``, holds the rules. The gate,
|
|
4
|
+
the reminder and the evaluation read the same file, so all three name each
|
|
5
|
+
rule with the same label and the same reminder text.
|
|
6
|
+
|
|
7
|
+
The mode is off unless ``~/.claude/visual-reply-mode.json`` holds
|
|
8
|
+
``{"enabled": true}``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from json_file_reader import read_json_object
|
|
20
|
+
from hooks_constants.visual_reply_rules_constants import (
|
|
21
|
+
ABBREVIATION_MESSAGE,
|
|
22
|
+
ABBREVIATIONS_RULE_KEY,
|
|
23
|
+
ALL_REPLY_TOOL_NAME_SUFFIXES,
|
|
24
|
+
ALL_VISUAL_TOOL_NAME_SUFFIXES,
|
|
25
|
+
ALLOWED_CAPITALIZED_WORDS_KEY,
|
|
26
|
+
ASSISTANT_ENTRY_TYPE,
|
|
27
|
+
BLOCK_ID_KEY,
|
|
28
|
+
BLOCK_NAME_KEY,
|
|
29
|
+
BLOCK_TYPE_KEY,
|
|
30
|
+
CAPITALIZED_ABBREVIATION_PATTERN,
|
|
31
|
+
CONTENT_KEY,
|
|
32
|
+
ENTRY_TYPE_KEY,
|
|
33
|
+
IS_ERROR_KEY,
|
|
34
|
+
IS_META_KEY,
|
|
35
|
+
LOWERCASE_ABBREVIATION_TEMPLATE,
|
|
36
|
+
LOWERCASE_ABBREVIATIONS_KEY,
|
|
37
|
+
MAXIMUM_REPLIES_PER_TURN,
|
|
38
|
+
MAXIMUM_SENTENCES_WITHOUT_VISUAL,
|
|
39
|
+
MESSAGE_KEY,
|
|
40
|
+
MODE_SWITCH_ENABLED_KEY,
|
|
41
|
+
MODE_SWITCH_FILE_NAME,
|
|
42
|
+
NO_PICTURE_RULE_KEY,
|
|
43
|
+
NO_VISUAL_MESSAGE,
|
|
44
|
+
ONE_ITEM_RULE_KEY,
|
|
45
|
+
RULE_KEY_FIELD,
|
|
46
|
+
RULE_LABEL_FIELD,
|
|
47
|
+
RULE_REMINDER_FIELD,
|
|
48
|
+
RULES_FILE_ENCODING,
|
|
49
|
+
RULES_FILE_PATH,
|
|
50
|
+
RULES_LIST_KEY,
|
|
51
|
+
SECOND_REPLY_MESSAGE,
|
|
52
|
+
TEXT_BLOCK_TYPE,
|
|
53
|
+
TOOL_RESULT_BLOCK_TYPE,
|
|
54
|
+
TOOL_USE_BLOCK_TYPE,
|
|
55
|
+
TOOL_USE_ID_KEY,
|
|
56
|
+
TRACKER_NUMBER_MESSAGE,
|
|
57
|
+
TRACKER_NUMBER_PATTERN,
|
|
58
|
+
TRANSCRIPT_ENCODING,
|
|
59
|
+
USER_ENTRY_TYPE,
|
|
60
|
+
WIDGET_ANCHOR_MESSAGE,
|
|
61
|
+
WIDGET_ANCHOR_PATTERN,
|
|
62
|
+
WIDGET_TOOL_NAME_SUFFIX,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class ReplyRule:
|
|
68
|
+
"""One rule of the mode: its key, the label a flag shows, and the reminder text."""
|
|
69
|
+
|
|
70
|
+
key: str
|
|
71
|
+
label: str
|
|
72
|
+
reminder: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class VisualReplyRules:
|
|
77
|
+
"""The parsed rule file."""
|
|
78
|
+
|
|
79
|
+
all_rules: tuple[ReplyRule, ...]
|
|
80
|
+
all_allowed_capitalized_words: frozenset[str]
|
|
81
|
+
all_lowercase_abbreviations: tuple[str, ...]
|
|
82
|
+
|
|
83
|
+
def reminder(self, rule_key: str) -> str:
|
|
84
|
+
"""Return the reminder text for one rule key."""
|
|
85
|
+
return next(each.reminder for each in self.all_rules if each.key == rule_key)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _string_list(field_value: object) -> list[object]:
|
|
89
|
+
return field_value if isinstance(field_value, list) else []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_rules(rules_path: Path = RULES_FILE_PATH) -> VisualReplyRules | None:
|
|
93
|
+
"""Parse the rule file, or return None when it is missing or malformed."""
|
|
94
|
+
document = read_json_object(rules_path, RULES_FILE_ENCODING)
|
|
95
|
+
if document is None:
|
|
96
|
+
return None
|
|
97
|
+
all_rule_entries = document.get(RULES_LIST_KEY)
|
|
98
|
+
if not isinstance(all_rule_entries, list):
|
|
99
|
+
return None
|
|
100
|
+
all_rules = tuple(
|
|
101
|
+
ReplyRule(
|
|
102
|
+
key=str(each[RULE_KEY_FIELD]),
|
|
103
|
+
label=str(each[RULE_LABEL_FIELD]),
|
|
104
|
+
reminder=str(each[RULE_REMINDER_FIELD]),
|
|
105
|
+
)
|
|
106
|
+
for each in all_rule_entries
|
|
107
|
+
if isinstance(each, dict)
|
|
108
|
+
and all(field in each for field in (RULE_KEY_FIELD, RULE_LABEL_FIELD, RULE_REMINDER_FIELD))
|
|
109
|
+
)
|
|
110
|
+
all_needed_keys = {ONE_ITEM_RULE_KEY, NO_PICTURE_RULE_KEY, ABBREVIATIONS_RULE_KEY}
|
|
111
|
+
if not all_needed_keys <= {each.key for each in all_rules}:
|
|
112
|
+
return None
|
|
113
|
+
return VisualReplyRules(
|
|
114
|
+
all_rules=all_rules,
|
|
115
|
+
all_allowed_capitalized_words=frozenset(
|
|
116
|
+
str(each) for each in _string_list(document.get(ALLOWED_CAPITALIZED_WORDS_KEY))
|
|
117
|
+
),
|
|
118
|
+
all_lowercase_abbreviations=tuple(
|
|
119
|
+
str(each) for each in _string_list(document.get(LOWERCASE_ABBREVIATIONS_KEY))
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def mode_enabled(claude_home: Path) -> bool:
|
|
125
|
+
"""Return True only when the switch file turns the mode on."""
|
|
126
|
+
switch_document = read_json_object(claude_home / MODE_SWITCH_FILE_NAME, RULES_FILE_ENCODING)
|
|
127
|
+
if switch_document is None:
|
|
128
|
+
return False
|
|
129
|
+
return switch_document.get(MODE_SWITCH_ENABLED_KEY) is True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def abbreviation_violation(prose_text: str, rules: VisualReplyRules) -> str | None:
|
|
133
|
+
"""Return the deny reason for the first abbreviation in prose with links and code removed."""
|
|
134
|
+
reminder = rules.reminder(ABBREVIATIONS_RULE_KEY)
|
|
135
|
+
for each_match in CAPITALIZED_ABBREVIATION_PATTERN.finditer(prose_text):
|
|
136
|
+
if each_match.group(0) not in rules.all_allowed_capitalized_words:
|
|
137
|
+
return ABBREVIATION_MESSAGE.format(abbreviation=each_match.group(0), reminder=reminder)
|
|
138
|
+
for each_abbreviation in rules.all_lowercase_abbreviations:
|
|
139
|
+
pattern = LOWERCASE_ABBREVIATION_TEMPLATE.format(abbreviation=re.escape(each_abbreviation))
|
|
140
|
+
if re.search(pattern, prose_text, re.IGNORECASE):
|
|
141
|
+
return ABBREVIATION_MESSAGE.format(abbreviation=each_abbreviation, reminder=reminder)
|
|
142
|
+
tracker_match = TRACKER_NUMBER_PATTERN.search(prose_text)
|
|
143
|
+
if tracker_match is not None:
|
|
144
|
+
return TRACKER_NUMBER_MESSAGE.format(reference=tracker_match.group(0), reminder=reminder)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def widget_anchor_violation(tool_name: str, all_tool_input: dict[str, object]) -> str | None:
|
|
149
|
+
"""Return the deny reason for an anchor link inside widget code."""
|
|
150
|
+
if not tool_name.endswith(WIDGET_TOOL_NAME_SUFFIX):
|
|
151
|
+
return None
|
|
152
|
+
serialized_input = json.dumps(all_tool_input)
|
|
153
|
+
return WIDGET_ANCHOR_MESSAGE if WIDGET_ANCHOR_PATTERN.search(serialized_input) else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _parsed_entries(all_transcript_lines: Iterable[str]) -> list[dict[str, object]]:
|
|
157
|
+
all_entries = []
|
|
158
|
+
for each_line in all_transcript_lines:
|
|
159
|
+
try:
|
|
160
|
+
parsed_entry = json.loads(each_line)
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
continue
|
|
163
|
+
if isinstance(parsed_entry, dict):
|
|
164
|
+
all_entries.append(parsed_entry)
|
|
165
|
+
return all_entries
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_prompt_entry(all_entry_fields: dict[str, object]) -> bool:
|
|
169
|
+
if all_entry_fields.get(ENTRY_TYPE_KEY) != USER_ENTRY_TYPE or all_entry_fields.get(IS_META_KEY):
|
|
170
|
+
return False
|
|
171
|
+
message = all_entry_fields.get(MESSAGE_KEY)
|
|
172
|
+
content = message.get(CONTENT_KEY) if isinstance(message, dict) else None
|
|
173
|
+
if isinstance(content, str):
|
|
174
|
+
return bool(content.strip())
|
|
175
|
+
return isinstance(content, list) and any(
|
|
176
|
+
isinstance(each_block, dict) and each_block.get(BLOCK_TYPE_KEY) == TEXT_BLOCK_TYPE
|
|
177
|
+
for each_block in content
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _content_blocks(all_entry_fields: dict[str, object], entry_type: str) -> list[dict[str, object]]:
|
|
182
|
+
if all_entry_fields.get(ENTRY_TYPE_KEY) != entry_type:
|
|
183
|
+
return []
|
|
184
|
+
message = all_entry_fields.get(MESSAGE_KEY)
|
|
185
|
+
content = message.get(CONTENT_KEY) if isinstance(message, dict) else None
|
|
186
|
+
return [each for each in (content if isinstance(content, list) else []) if isinstance(each, dict)]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _tool_uses(all_entry_fields: dict[str, object]) -> list[dict[str, object]]:
|
|
190
|
+
return [
|
|
191
|
+
each_block
|
|
192
|
+
for each_block in _content_blocks(all_entry_fields, ASSISTANT_ENTRY_TYPE)
|
|
193
|
+
if each_block.get(BLOCK_TYPE_KEY) == TOOL_USE_BLOCK_TYPE
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _entries_this_turn(all_transcript_lines: Iterable[str]) -> list[dict[str, object]]:
|
|
198
|
+
all_entries = _parsed_entries(all_transcript_lines)
|
|
199
|
+
last_prompt_index = max(
|
|
200
|
+
(index for index, each in enumerate(all_entries) if _is_prompt_entry(each)), default=-1
|
|
201
|
+
)
|
|
202
|
+
return all_entries[last_prompt_index + 1 :]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _read_transcript_lines(transcript_path: object) -> list[str] | None:
|
|
206
|
+
if not isinstance(transcript_path, str):
|
|
207
|
+
return None
|
|
208
|
+
try:
|
|
209
|
+
return Path(transcript_path).read_text(encoding=TRANSCRIPT_ENCODING).splitlines()
|
|
210
|
+
except OSError:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _result_ids(all_entries: list[dict[str, object]], is_error: bool) -> set[object]:
|
|
215
|
+
return {
|
|
216
|
+
each_block.get(TOOL_USE_ID_KEY)
|
|
217
|
+
for each_entry in all_entries
|
|
218
|
+
for each_block in _content_blocks(each_entry, USER_ENTRY_TYPE)
|
|
219
|
+
if each_block.get(BLOCK_TYPE_KEY) == TOOL_RESULT_BLOCK_TYPE
|
|
220
|
+
and bool(each_block.get(IS_ERROR_KEY)) is is_error
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def visual_shown_this_turn(all_transcript_lines: Iterable[str]) -> bool:
|
|
225
|
+
"""Return True when a widget or page call after the last prompt did not come back with an error."""
|
|
226
|
+
all_entries = _entries_this_turn(all_transcript_lines)
|
|
227
|
+
all_errored_ids = _result_ids(all_entries, is_error=True)
|
|
228
|
+
return any(
|
|
229
|
+
str(each_use.get(BLOCK_NAME_KEY)).endswith(ALL_VISUAL_TOOL_NAME_SUFFIXES)
|
|
230
|
+
and each_use.get(BLOCK_ID_KEY) not in all_errored_ids
|
|
231
|
+
for each_entry in all_entries
|
|
232
|
+
for each_use in _tool_uses(each_entry)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def replies_sent_this_turn(all_transcript_lines: Iterable[str]) -> int:
|
|
237
|
+
"""Count the reply calls after the last prompt that came back without an error."""
|
|
238
|
+
all_entries = _entries_this_turn(all_transcript_lines)
|
|
239
|
+
all_delivered_ids = _result_ids(all_entries, is_error=False)
|
|
240
|
+
return sum(
|
|
241
|
+
1
|
|
242
|
+
for each_entry in all_entries
|
|
243
|
+
for each_use in _tool_uses(each_entry)
|
|
244
|
+
if str(each_use.get(BLOCK_NAME_KEY)).endswith(ALL_REPLY_TOOL_NAME_SUFFIXES)
|
|
245
|
+
and each_use.get(BLOCK_ID_KEY) in all_delivered_ids
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def second_reply_violation(transcript_path: object, rules: VisualReplyRules) -> str | None:
|
|
250
|
+
"""Return the deny reason for a reply when this turn already delivered one."""
|
|
251
|
+
all_lines = _read_transcript_lines(transcript_path)
|
|
252
|
+
if all_lines is None or replies_sent_this_turn(all_lines) < MAXIMUM_REPLIES_PER_TURN:
|
|
253
|
+
return None
|
|
254
|
+
return SECOND_REPLY_MESSAGE.format(reminder=rules.reminder(ONE_ITEM_RULE_KEY))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def visual_violation(
|
|
258
|
+
sentence_count: int, transcript_path: object, rules: VisualReplyRules
|
|
259
|
+
) -> str | None:
|
|
260
|
+
"""Return the deny reason for a reply of several sentences with no visual this turn."""
|
|
261
|
+
if sentence_count <= MAXIMUM_SENTENCES_WITHOUT_VISUAL:
|
|
262
|
+
return None
|
|
263
|
+
all_lines = _read_transcript_lines(transcript_path)
|
|
264
|
+
if all_lines is None or visual_shown_this_turn(all_lines):
|
|
265
|
+
return None
|
|
266
|
+
return NO_VISUAL_MESSAGE.format(
|
|
267
|
+
sentence_count=sentence_count, reminder=rules.reminder(NO_PICTURE_RULE_KEY)
|
|
268
|
+
)
|
|
@@ -9,14 +9,10 @@ The sandbox is rooted under the user's home directory via ``tempfile.mkdtemp``
|
|
|
9
9
|
rather than the OS temp directory, matching the sibling workflow-hook tests.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import contextlib
|
|
13
12
|
import functools
|
|
14
13
|
import importlib.util
|
|
15
|
-
import inspect
|
|
16
14
|
import json
|
|
17
15
|
import os
|
|
18
|
-
import shutil
|
|
19
|
-
import stat
|
|
20
16
|
import subprocess
|
|
21
17
|
import sys
|
|
22
18
|
import tempfile
|
|
@@ -26,6 +22,8 @@ from types import ModuleType
|
|
|
26
22
|
|
|
27
23
|
import pytest
|
|
28
24
|
|
|
25
|
+
from hooks_constants.windows_filesystem import force_rmtree
|
|
26
|
+
|
|
29
27
|
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "auto_formatter.py")
|
|
30
28
|
HOOKS_DIRECTORY_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
31
29
|
POST_TOOL_USE_DISPATCHER_PATH = os.path.join(
|
|
@@ -52,25 +50,6 @@ def build_dispatcher_payload(tool_name: str, file_path: Path) -> str:
|
|
|
52
50
|
return json.dumps({"tool_name": tool_name, "tool_input": {"file_path": str(file_path)}})
|
|
53
51
|
|
|
54
52
|
|
|
55
|
-
def _strip_read_only_and_retry(removal_function, target_path, *_exc_info):
|
|
56
|
-
try:
|
|
57
|
-
os.chmod(target_path, stat.S_IWRITE)
|
|
58
|
-
removal_function(target_path)
|
|
59
|
-
except OSError:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
_rmtree_supports_onexc = "onexc" in inspect.signature(shutil.rmtree).parameters
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _force_rmtree(target_path: str) -> None:
|
|
67
|
-
with contextlib.suppress(OSError):
|
|
68
|
-
if _rmtree_supports_onexc:
|
|
69
|
-
shutil.rmtree(target_path, onexc=_strip_read_only_and_retry)
|
|
70
|
-
else:
|
|
71
|
-
shutil.rmtree(target_path, onerror=_strip_read_only_and_retry)
|
|
72
|
-
|
|
73
|
-
|
|
74
53
|
@functools.lru_cache(maxsize=1)
|
|
75
54
|
def _get_sandbox_parent_directory() -> str:
|
|
76
55
|
return tempfile.mkdtemp(prefix="pytest_auto_formatter_", dir=str(Path.home()))
|
|
@@ -80,7 +59,7 @@ def _get_sandbox_parent_directory() -> str:
|
|
|
80
59
|
def _cleanup_sandbox_parent_directory() -> Generator[None]:
|
|
81
60
|
yield
|
|
82
61
|
if _get_sandbox_parent_directory.cache_info().currsize:
|
|
83
|
-
|
|
62
|
+
force_rmtree(_get_sandbox_parent_directory())
|
|
84
63
|
_get_sandbox_parent_directory.cache_clear()
|
|
85
64
|
|
|
86
65
|
|
|
@@ -95,7 +74,7 @@ def git_repository() -> Generator[Path]:
|
|
|
95
74
|
env=build_fixture_git_environment(),
|
|
96
75
|
)
|
|
97
76
|
yield repository_path
|
|
98
|
-
|
|
77
|
+
force_rmtree(str(repository_path))
|
|
99
78
|
|
|
100
79
|
|
|
101
80
|
def _run_hook(tool_name: str, file_path: Path) -> subprocess.CompletedProcess[str]:
|
package/package.json
CHANGED
package/rules/index.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Rules
|
|
2
|
+
Each line gives a rule in brief. When its trigger matches your task, open the guide and follow it in full. Correction lens and question presentation load as their own files.
|
|
3
|
+
|
|
4
|
+
**Writing**
|
|
5
|
+
- **Plain language** ([guide](../docs/rule-guides/asd-ste100-language.md)). Writing chat, narration, or repository prose. Short complete sentences on one topic, active voice, one action per step, each fact said once.
|
|
6
|
+
- **No contrast framing** ([guide](../docs/rule-guides/no-contrast-framing.md)). Any sentence a person reads. State the chosen point and leave out the discarded reading. Remove the six forms: `trailing-comma-not`, `corrective-it-is-not`, `substitution-rather-than`, `additive-not-just`, `comparative-ranking`, `substitution-as-opposed-to`.
|
|
7
|
+
|
|
8
|
+
**Evidence**
|
|
9
|
+
- **Research mode** ([guide](../docs/rule-guides/research-mode.md)). Any claim, recommendation, or advice. Cite a source for each claim, and run the tool that settles a fact before you state it.
|
|
10
|
+
- **Verify before asking** ([guide](../docs/rule-guides/verify-before-asking.md)). Before a clarifying question. Look in files, config, environment, and tools first; ask only for a judgment or a preference.
|
|
11
|
+
- **Verify runtime state** ([guide](../docs/rule-guides/verify-runtime-state.md)). Before you say a component works, is healthy, or is not the cause. Get a live signal this session.
|
|
12
|
+
|
|
13
|
+
**Before building**
|
|
14
|
+
- **Explore thoroughly** ([guide](../docs/rule-guides/explore-thoroughly.md)). Before you log, plan, delegate, or build an ask. Search for existing work; when something exists, stop and report it in four points with the `search-before-acting` skill.
|
|
15
|
+
- **Features start with an eval** ([guide](../docs/rule-guides/features-start-with-an-eval.md)). A new feature or a change in behavior. Your first action is the Skill tool with skill `claude-api` and args `build-eval`.
|
|
16
|
+
- **Request scope** ([guide](../docs/rule-guides/request-scope.md)). Asked whether part of a block is still needed, or asked to change one. Propose removing every line a shipped gate already enforces.
|
|
17
|
+
|
|
18
|
+
**Shell and files**
|
|
19
|
+
- **Shell invocation** ([guide](../docs/rule-guides/shell-invocation.md)). Any Bash command or permission rule. Use `pwsh` on Windows; keep `$(...)`, backticks, and process substitution out of commands.
|
|
20
|
+
- **Destructive commands** ([guide](../docs/rule-guides/destructive-commands.md)). Removing files. Keep `rm` literals out of commands, use absolute targets, use `git rm` for tracked files, and copy the guide's no-rm line into subagent prompts.
|
|
21
|
+
- **Filesystem search** ([guide](../docs/rule-guides/filesystem-search.md)). Finding files by name, path, size, or date. Scope the search to a project, worktree, or filter, never a root.
|
|
22
|
+
- **Clean up temporary files** ([guide](../docs/rule-guides/cleanup-temp-files.md)). Creating scratch files or debug dumps. Track them and remove them when the task ends.
|
|
23
|
+
|
|
24
|
+
**Shipping**
|
|
25
|
+
- **Pull request lifecycle**. Before a commit, push, pull request action, or merge. Invoke the `pr-lifecycle` skill; a hook denies those commands until it loads.
|
|
26
|
+
- **Proof before a pull request** ([guide](../docs/rule-guides/proof-before-pull-request.md)). Before you open a pull request. Run the change with and without it, and quote both under "Proof in practice".
|
|
27
|
+
- **Session title** ([guide](../docs/rule-guides/session-title.md)). The session has a tool whose name ends in `__set_session_title`. Set `<emoji> <name>` before each turn ends.
|
|
28
|
+
|
|
29
|
+
**Memory**
|
|
30
|
+
- **Durable facts only** ([guide](../docs/rule-guides/memory-stores-durable-facts.md)). Writing auto-memory or a `MEMORY.md` entry. Keep only a fact that helps a fresh session weeks later.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"key": "one_item",
|
|
5
|
+
"label": "More than one item",
|
|
6
|
+
"reminder": "Show one item per turn: one visual of that item and one short reply beside it. Put progress in the status line, and send the next item after the user answers this one."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"key": "no_picture",
|
|
10
|
+
"label": "No picture",
|
|
11
|
+
"reminder": "Show the item as one visual first, a widget or a published page. Then send one reply of one to three short sentences that states the outcome and each fact the user needs, and links the page."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"key": "too_many_words",
|
|
15
|
+
"label": "Too many words",
|
|
16
|
+
"reminder": "Cut the reply to three short sentences or fewer. Keep the outcome and each fact the user needs in the reply; the visual adds detail around them."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"key": "abbreviations",
|
|
20
|
+
"label": "Abbreviations",
|
|
21
|
+
"reminder": "Write each name in plain descriptive words. Spell out every abbreviation and drop tracker and item numbers. A pull request number stays only inside its link."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "wall_of_text",
|
|
25
|
+
"label": "Wall of text",
|
|
26
|
+
"reminder": "Put each point, and each step the reader takes, on its own line."
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"allowed_capitalized_words": [
|
|
30
|
+
"I",
|
|
31
|
+
"OK"
|
|
32
|
+
],
|
|
33
|
+
"lowercase_abbreviations": [
|
|
34
|
+
"e.g.",
|
|
35
|
+
"i.e.",
|
|
36
|
+
"etc.",
|
|
37
|
+
"vs.",
|
|
38
|
+
"approx.",
|
|
39
|
+
"w/"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
settings.local.json, and settings.local.json.template, and rewrites permission
|
|
10
10
|
rule strings that invoke powershell / powershell.exe / bash -c / cmd /c
|
|
11
11
|
into their pwsh equivalents per the migration mapping in
|
|
12
|
-
|
|
12
|
+
docs/rule-guides/shell-invocation.md. Defaults to dry-run; pass -Apply to write
|
|
13
13
|
changes. Prints exactly one summary line.
|
|
14
14
|
|
|
15
15
|
.PARAMETER Roots
|
package/scripts/conftest.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""Put the scripts
|
|
1
|
+
"""Put the scripts and hooks directories on ``sys.path`` for every test here.
|
|
2
2
|
|
|
3
3
|
The code-review invoker and its constants package live beside these tests
|
|
4
4
|
rather than on the default pytest path, so each test module can import them
|
|
5
|
-
by bare name once this directory is registered.
|
|
5
|
+
by bare name once this directory is registered. The hooks directory and its
|
|
6
|
+
session package are registered too, so a test imports ``followup_ledger``,
|
|
7
|
+
``hooks_constants``, or ``untracked_repo_detector`` by bare name.
|
|
6
8
|
|
|
7
9
|
::
|
|
8
10
|
|
|
@@ -21,9 +23,15 @@ from pathlib import Path
|
|
|
21
23
|
|
|
22
24
|
import pytest
|
|
23
25
|
|
|
24
|
-
_scripts_directory =
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
_scripts_directory = Path(__file__).resolve().parent
|
|
27
|
+
_hooks_directory = _scripts_directory.parent / "hooks"
|
|
28
|
+
for each_import_directory in (
|
|
29
|
+
_hooks_directory / "session",
|
|
30
|
+
_hooks_directory,
|
|
31
|
+
_scripts_directory,
|
|
32
|
+
):
|
|
33
|
+
if str(each_import_directory) not in sys.path:
|
|
34
|
+
sys.path.insert(0, str(each_import_directory))
|
|
27
35
|
|
|
28
36
|
_live_broker_state_resolver = importlib.import_module("account_broker_support").broker_state_path
|
|
29
37
|
|
|
@@ -8,7 +8,7 @@ against a rejected reading::
|
|
|
8
8
|
"precision matters more than coverage" -> comparative-ranking
|
|
9
9
|
ok: "the function runs more than 30 lines"
|
|
10
10
|
|
|
11
|
-
The rule
|
|
11
|
+
The rule guide ``docs/rule-guides/no-contrast-framing.md`` names each form here in
|
|
12
12
|
backticks, and a test holds the two in step.
|
|
13
13
|
"""
|
|
14
14
|
|
|
@@ -79,4 +79,4 @@ CONTRAST_FRAMING_QUOTED_LINE_LIMIT: int = 120
|
|
|
79
79
|
|
|
80
80
|
CONTRAST_FRAMING_FINDING_CODE: str = "contrast-framing"
|
|
81
81
|
|
|
82
|
-
CONTRAST_FRAMING_RULE_DOCUMENT: str = "
|
|
82
|
+
CONTRAST_FRAMING_RULE_DOCUMENT: str = "docs/rule-guides/no-contrast-framing.md"
|
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
5
7
|
from dev_env_scripts_constants.contrast_framing_constants import (
|
|
6
8
|
ALL_CONTRAST_FRAMING_FORMS,
|
|
7
9
|
CONTRAST_FRAMING_MESSAGE_TEMPLATE,
|
|
10
|
+
CONTRAST_FRAMING_RULE_DOCUMENT,
|
|
8
11
|
)
|
|
9
12
|
|
|
13
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_the_rule_document_is_a_rule_guide_that_exists() -> None:
|
|
17
|
+
rule_document = PACKAGE_ROOT / CONTRAST_FRAMING_RULE_DOCUMENT
|
|
18
|
+
|
|
19
|
+
assert rule_document.parent.name == "rule-guides"
|
|
20
|
+
assert rule_document.is_file()
|
|
21
|
+
|
|
10
22
|
|
|
11
23
|
def test_every_form_carries_a_distinct_name() -> None:
|
|
12
24
|
all_names = [each_form.name for each_form in ALL_CONTRAST_FRAMING_FORMS]
|
|
@@ -136,7 +136,6 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
136
136
|
hooks/blocking/session_title_format_gate.py -> exempt
|
|
137
137
|
hooks/blocking/session_title_stop_gate.py -> exempt
|
|
138
138
|
hooks/blocking/artifact_dark_mode_gate.py -> exempt
|
|
139
|
-
hooks/blocking/artifact_look_gate.py -> exempt
|
|
140
139
|
hooks/blocking/some_new_blocker.py -> flagged
|
|
141
140
|
|
|
142
141
|
The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
|
|
@@ -161,10 +160,6 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
161
160
|
The verify-before-acting hook runs after a mutating call has finished. It
|
|
162
161
|
asks the model to check a hedged claim, and the call it reads stays in place.
|
|
163
162
|
|
|
164
|
-
The artifact look gate denies an Artifact publish of a page until a
|
|
165
|
-
screenshot of it was read. It decides when a page ships and no code or
|
|
166
|
-
safety policy.
|
|
167
|
-
|
|
168
163
|
The session title gates keep the session title in its status format. One
|
|
169
164
|
denies a malformed title, and the other asks for a title before a turn
|
|
170
165
|
ends. They decide the shape of a title and no code or safety policy.
|
|
@@ -73,7 +73,6 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
73
73
|
"blocking/session_title_stop_gate.py",
|
|
74
74
|
"blocking/image_read_size_gate.py",
|
|
75
75
|
"blocking/artifact_dark_mode_gate.py",
|
|
76
|
-
"blocking/artifact_look_gate.py",
|
|
77
76
|
)
|
|
78
77
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
79
78
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import sys
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
|
-
_SCRIPTS = Path(__file__).resolve().parent
|
|
9
|
-
if str(_SCRIPTS) not in sys.path:
|
|
10
|
-
sys.path.insert(0, str(_SCRIPTS))
|
|
11
|
-
|
|
12
7
|
from active_capability_references import (
|
|
13
8
|
build_capability_inventory,
|
|
14
9
|
classify_capability_reference,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import subprocess
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
@@ -43,6 +44,5 @@ def test_meter_cache_write_lands_outside_the_home_directory(
|
|
|
43
44
|
|
|
44
45
|
home_state_path = fake_home / ".claude" / "account-broker" / "state.json"
|
|
45
46
|
assert not home_state_path.exists()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
47
|
+
state = json.loads(isolated_broker_state_path.read_text(encoding="utf-8"))
|
|
48
|
+
assert f"claude:only:{account.home}" in state["meters"]
|
|
@@ -2,15 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import sys
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
import pytest
|
|
9
8
|
|
|
10
|
-
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
-
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
12
|
-
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
13
|
-
|
|
14
9
|
import digest_check
|
|
15
10
|
from dev_env_scripts_constants.digest_check_constants import (
|
|
16
11
|
CARD_WORD_BUDGET,
|
|
@@ -9,14 +9,10 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
13
|
-
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
14
|
-
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
15
|
-
|
|
16
12
|
import durable_post_lint
|
|
17
13
|
from dev_env_scripts_constants.private_term_constants import PrivateTermDigest
|
|
18
14
|
|
|
19
|
-
SCRIPT_PATH =
|
|
15
|
+
SCRIPT_PATH = Path(__file__).resolve().parent / "durable_post_lint.py"
|
|
20
16
|
VALID_PR_BODY = """## Why
|
|
21
17
|
|
|
22
18
|
Why text.
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
6
|
import json
|
|
7
|
-
import sys
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
|
|
10
9
|
from dev_env_scripts_constants.followup_constants import (
|
|
@@ -14,11 +13,6 @@ from dev_env_scripts_constants.followup_constants import (
|
|
|
14
13
|
FOLLOWUP_BACKLOG_THRESHOLD,
|
|
15
14
|
)
|
|
16
15
|
from followup_cli import main
|
|
17
|
-
|
|
18
|
-
_hooks_directory = str(Path(__file__).resolve().parents[1] / "hooks")
|
|
19
|
-
if _hooks_directory not in sys.path:
|
|
20
|
-
sys.path.insert(0, _hooks_directory)
|
|
21
|
-
|
|
22
16
|
from followup_ledger import (
|
|
23
17
|
FollowupFinding,
|
|
24
18
|
all_recorded_findings,
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import subprocess
|
|
7
|
-
import sys
|
|
8
7
|
import threading
|
|
9
8
|
from dataclasses import dataclass, field
|
|
10
9
|
from datetime import datetime, timezone
|
|
@@ -12,13 +11,9 @@ from pathlib import Path
|
|
|
12
11
|
|
|
13
12
|
import pytest
|
|
14
13
|
|
|
15
|
-
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
16
|
-
if str(_SCRIPTS_DIR) not in sys.path:
|
|
17
|
-
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
18
|
-
|
|
19
14
|
import account_broker
|
|
20
15
|
import invoke_code_review as invoker
|
|
21
|
-
import resolve_worker_spawn as dispatcher
|
|
16
|
+
import resolve_worker_spawn as dispatcher
|
|
22
17
|
from dev_env_scripts_constants.account_broker_constants import (
|
|
23
18
|
Account,
|
|
24
19
|
BrokerConfigurationError,
|
|
@@ -28,7 +23,7 @@ from dev_env_scripts_constants.account_broker_constants import (
|
|
|
28
23
|
ProductAdapter,
|
|
29
24
|
WAIT_EXIT_CODE,
|
|
30
25
|
)
|
|
31
|
-
from dev_env_scripts_constants.grok_worker_constants import (
|
|
26
|
+
from dev_env_scripts_constants.grok_worker_constants import (
|
|
32
27
|
AGENT_FLAG,
|
|
33
28
|
CLI_AGENT_FLAG,
|
|
34
29
|
ALL_AGENT_FILENAMES_BY_ROLE,
|
|
@@ -67,12 +62,12 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
67
62
|
TIER_CLAUDE_HEADLESS,
|
|
68
63
|
TIER_GROK,
|
|
69
64
|
)
|
|
70
|
-
import grok_headless_runner
|
|
71
|
-
from grok_headless_runner import (
|
|
65
|
+
import grok_headless_runner
|
|
66
|
+
from grok_headless_runner import (
|
|
72
67
|
GrokRunnerOutcome,
|
|
73
68
|
run_headless_worker,
|
|
74
69
|
)
|
|
75
|
-
from grok_worker_preflight import PreflightOutcome
|
|
70
|
+
from grok_worker_preflight import PreflightOutcome
|
|
76
71
|
|
|
77
72
|
HOST_PROFILE_CLAUDE = "Claude"
|
|
78
73
|
HOST_PROFILE_THIRD_PARTY = "ThirdParty"
|