claude-dev-env 8.73.1 → 8.74.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/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/cde.mjs +1 -0
- package/bin/cde.test.mjs +1 -0
- 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.settings-defaults.test.mjs +10 -2
- package/bin/install.test.mjs +1 -0
- package/bin/merge_settings_defaults.mjs +34 -1
- package/bin/merge_settings_defaults.test.mjs +53 -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/flag-non-breaking-findings.md +3 -0
- 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/haiku_spawn_slim_gate.py +96 -0
- 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_haiku_spawn_slim_gate.py +65 -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/features/spawn-routing.md +5 -1
- package/hooks/followup_ledger.py +52 -2
- package/hooks/hooks.json +11 -6
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/haiku_spawn_slim_gate_constants.py +29 -0
- 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_followup_ledger.py +58 -0
- 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/followup_constants.py +4 -1
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +12 -0
- package/scripts/followup_cli.py +25 -1
- package/scripts/policy_lint/adapter_configuration.py +0 -5
- package/scripts/policy_lint/config/constants.py +1 -1
- package/scripts/repository_checks/config/constants.py +6 -0
- package/scripts/repository_checks/followup_ledger_duplicates.py +55 -0
- package/scripts/repository_checks/runner.py +7 -0
- 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 +18 -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_followup_ledger_duplicates.py +58 -0
- package/scripts/tests/test_repository_policy.py +2 -0
- package/scripts/tests/test_rule_index_shape.py +59 -5
- package/scripts/tests/test_rule_load_scopes.py +1 -16
- package/settings.json +9 -0
- 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
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: refuse to publish an artifact page nobody has looked at.
|
|
3
|
-
|
|
4
|
-
An agent that writes a page and publishes it without rendering it cannot know
|
|
5
|
-
the page matches the ask. The hook walks the session transcript and allows the
|
|
6
|
-
publish only when a Read opened an image file whose modification time is at
|
|
7
|
-
least the page file's::
|
|
8
|
-
|
|
9
|
-
Write page.html -> shoot shot.png -> Read shot.png -> publish -> allowed
|
|
10
|
-
Write page.html -> publish -> refused
|
|
11
|
-
Read shot.png -> change page.html in any tool -> publish -> refused
|
|
12
|
-
Read an image older than the page -> publish -> refused
|
|
13
|
-
|
|
14
|
-
A Read that returned an error, or whose result came before the page's last
|
|
15
|
-
change, does not count. A non-HTML file, an asset
|
|
16
|
-
upload, a non-publish action and an unreadable transcript all pass, so the
|
|
17
|
-
hook never fails closed.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import json
|
|
23
|
-
import sys
|
|
24
|
-
from datetime import datetime
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
28
|
-
if hooks_root_directory not in sys.path:
|
|
29
|
-
sys.path.insert(0, hooks_root_directory)
|
|
30
|
-
|
|
31
|
-
from hooks_constants.artifact_look_gate_constants import (
|
|
32
|
-
ALL_IMAGE_FILE_SUFFIXES,
|
|
33
|
-
ALL_PAGE_FILE_SUFFIXES,
|
|
34
|
-
ARTIFACT_ACTION_INPUT_KEY,
|
|
35
|
-
ARTIFACT_ASSET_INPUT_KEY,
|
|
36
|
-
ARTIFACT_PUBLISH_ACTION,
|
|
37
|
-
ARTIFACT_TOOL_NAME,
|
|
38
|
-
ASSISTANT_ENTRY_TYPE,
|
|
39
|
-
DENY_DECISION,
|
|
40
|
-
ENTRY_TIMESTAMP_KEY,
|
|
41
|
-
FILE_PATH_INPUT_KEY,
|
|
42
|
-
IMAGE_READING_TOOL_NAME,
|
|
43
|
-
PRE_TOOL_USE_EVENT_NAME,
|
|
44
|
-
TOOL_INPUT_KEY,
|
|
45
|
-
TOOL_NAME_KEY,
|
|
46
|
-
TOOL_RESULT_BLOCK_TYPE,
|
|
47
|
-
TOOL_USE_BLOCK_TYPE,
|
|
48
|
-
TRANSCRIPT_DECODE_ERRORS,
|
|
49
|
-
TRANSCRIPT_ENCODING,
|
|
50
|
-
TRANSCRIPT_PATH_KEY,
|
|
51
|
-
UNSEEN_PAGE_REASON_TEMPLATE,
|
|
52
|
-
USER_ENTRY_TYPE,
|
|
53
|
-
)
|
|
54
|
-
from hooks_constants.hook_specific_output_keys import (
|
|
55
|
-
HOOK_EVENT_NAME_KEY,
|
|
56
|
-
HOOK_SPECIFIC_OUTPUT_KEY,
|
|
57
|
-
PERMISSION_DECISION_KEY,
|
|
58
|
-
)
|
|
59
|
-
from hooks_constants.pr_lifecycle_skill_gate_constants import (
|
|
60
|
-
PERMISSION_DECISION_REASON_KEY,
|
|
61
|
-
)
|
|
62
|
-
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _content_blocks(
|
|
66
|
-
all_entry_fields: dict[str, object], entry_type: str
|
|
67
|
-
) -> list[dict[str, object]]:
|
|
68
|
-
if all_entry_fields.get("type") != entry_type:
|
|
69
|
-
return []
|
|
70
|
-
message = all_entry_fields.get("message")
|
|
71
|
-
content = message.get("content") if isinstance(message, dict) else None
|
|
72
|
-
if not isinstance(content, list):
|
|
73
|
-
return []
|
|
74
|
-
return [each_block for each_block in content if isinstance(each_block, dict)]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _parsed_entry(transcript_line: str) -> dict[str, object]:
|
|
78
|
-
try:
|
|
79
|
-
parsed_line = json.loads(transcript_line)
|
|
80
|
-
except json.JSONDecodeError:
|
|
81
|
-
return {}
|
|
82
|
-
return parsed_line if isinstance(parsed_line, dict) else {}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _entry_epoch(all_entry_fields: dict[str, object]) -> float | None:
|
|
86
|
-
timestamp_text = all_entry_fields.get(ENTRY_TIMESTAMP_KEY)
|
|
87
|
-
if not isinstance(timestamp_text, str):
|
|
88
|
-
return None
|
|
89
|
-
try:
|
|
90
|
-
return datetime.fromisoformat(timestamp_text.replace("Z", "+00:00")).timestamp()
|
|
91
|
-
except ValueError:
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _successful_result_epochs(all_entries: list[dict[str, object]]) -> dict[str, float]:
|
|
96
|
-
return {
|
|
97
|
-
str(each_block.get("tool_use_id")): result_epoch
|
|
98
|
-
for each_entry in all_entries
|
|
99
|
-
if (result_epoch := _entry_epoch(each_entry)) is not None
|
|
100
|
-
for each_block in _content_blocks(each_entry, USER_ENTRY_TYPE)
|
|
101
|
-
if each_block.get("type") == TOOL_RESULT_BLOCK_TYPE and not each_block.get("is_error")
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _tool_calls(all_entries: list[dict[str, object]]) -> list[tuple[str, object, object]]:
|
|
106
|
-
return [
|
|
107
|
-
(str(each_block.get("id")), each_block.get("name"), tool_input.get(FILE_PATH_INPUT_KEY))
|
|
108
|
-
for each_entry in all_entries
|
|
109
|
-
for each_block in _content_blocks(each_entry, ASSISTANT_ENTRY_TYPE)
|
|
110
|
-
if each_block.get("type") == TOOL_USE_BLOCK_TYPE
|
|
111
|
-
and isinstance(tool_input := each_block.get("input"), dict)
|
|
112
|
-
]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _is_image_read(tool_name: object, file_path: object) -> bool:
|
|
116
|
-
return (
|
|
117
|
-
tool_name == IMAGE_READING_TOOL_NAME
|
|
118
|
-
and isinstance(file_path, str)
|
|
119
|
-
and file_path.lower().endswith(ALL_IMAGE_FILE_SUFFIXES)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _modified_time(file_path: str) -> float | None:
|
|
124
|
-
try:
|
|
125
|
-
return Path(file_path).stat().st_mtime
|
|
126
|
-
except OSError:
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def page_was_looked_at(all_transcript_lines: list[str], page_path: str) -> bool:
|
|
131
|
-
"""Return True when a screenshot made after the page's last change was read.
|
|
132
|
-
|
|
133
|
-
The page's modification time covers every way it can change, including a
|
|
134
|
-
Bash command or a subagent. A Read counts only when it succeeded, its
|
|
135
|
-
result arrived after the page last changed, and its image file is at
|
|
136
|
-
least as new as the page.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
all_transcript_lines: The session transcript, one JSON record per line.
|
|
140
|
-
page_path: The file the Artifact call publishes.
|
|
141
|
-
"""
|
|
142
|
-
page_modified_time = _modified_time(page_path)
|
|
143
|
-
if page_modified_time is None:
|
|
144
|
-
return False
|
|
145
|
-
all_entries = [_parsed_entry(each_line) for each_line in all_transcript_lines]
|
|
146
|
-
result_epoch_by_id = _successful_result_epochs(all_entries)
|
|
147
|
-
for each_id, each_tool_name, each_file_path in _tool_calls(all_entries):
|
|
148
|
-
result_epoch = result_epoch_by_id.get(each_id)
|
|
149
|
-
if not _is_image_read(each_tool_name, each_file_path) or result_epoch is None:
|
|
150
|
-
continue
|
|
151
|
-
image_modified_time = _modified_time(str(each_file_path))
|
|
152
|
-
if (
|
|
153
|
-
image_modified_time is not None
|
|
154
|
-
and image_modified_time >= page_modified_time
|
|
155
|
-
and result_epoch >= page_modified_time
|
|
156
|
-
):
|
|
157
|
-
return True
|
|
158
|
-
return False
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _published_page_path(all_hook_fields: dict[str, object]) -> str | None:
|
|
162
|
-
tool_input = all_hook_fields.get(TOOL_INPUT_KEY)
|
|
163
|
-
if all_hook_fields.get(TOOL_NAME_KEY) != ARTIFACT_TOOL_NAME or not isinstance(tool_input, dict):
|
|
164
|
-
return None
|
|
165
|
-
page_path = tool_input.get(FILE_PATH_INPUT_KEY)
|
|
166
|
-
is_page_publish = (
|
|
167
|
-
tool_input.get(ARTIFACT_ACTION_INPUT_KEY, ARTIFACT_PUBLISH_ACTION) == ARTIFACT_PUBLISH_ACTION
|
|
168
|
-
and tool_input.get(ARTIFACT_ASSET_INPUT_KEY) is not True
|
|
169
|
-
and isinstance(page_path, str)
|
|
170
|
-
and page_path.lower().endswith(ALL_PAGE_FILE_SUFFIXES)
|
|
171
|
-
)
|
|
172
|
-
return page_path if is_page_publish else None
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def unseen_page_reason(all_hook_fields: dict[str, object]) -> str | None:
|
|
176
|
-
"""Return the refusal for an unseen page publish, or None to allow it.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
all_hook_fields: The parsed PreToolUse payload.
|
|
180
|
-
"""
|
|
181
|
-
page_path = _published_page_path(all_hook_fields)
|
|
182
|
-
transcript_path = all_hook_fields.get(TRANSCRIPT_PATH_KEY)
|
|
183
|
-
if page_path is None or not isinstance(transcript_path, str):
|
|
184
|
-
return None
|
|
185
|
-
try:
|
|
186
|
-
all_transcript_lines = Path(transcript_path).read_text(
|
|
187
|
-
encoding=TRANSCRIPT_ENCODING, errors=TRANSCRIPT_DECODE_ERRORS
|
|
188
|
-
).splitlines()
|
|
189
|
-
except OSError:
|
|
190
|
-
return None
|
|
191
|
-
if page_was_looked_at(all_transcript_lines, page_path):
|
|
192
|
-
return None
|
|
193
|
-
return UNSEEN_PAGE_REASON_TEMPLATE.format(page_path=page_path)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def main() -> int:
|
|
197
|
-
"""Read the PreToolUse payload and print a deny decision for an unseen page.
|
|
198
|
-
|
|
199
|
-
Returns:
|
|
200
|
-
0 in every case; the decision travels in the JSON output.
|
|
201
|
-
"""
|
|
202
|
-
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
203
|
-
if hook_payload is None:
|
|
204
|
-
return 0
|
|
205
|
-
refusal_reason = unseen_page_reason(hook_payload)
|
|
206
|
-
if refusal_reason is not None:
|
|
207
|
-
sys.stdout.write(
|
|
208
|
-
json.dumps(
|
|
209
|
-
{
|
|
210
|
-
HOOK_SPECIFIC_OUTPUT_KEY: {
|
|
211
|
-
HOOK_EVENT_NAME_KEY: PRE_TOOL_USE_EVENT_NAME,
|
|
212
|
-
PERMISSION_DECISION_KEY: DENY_DECISION,
|
|
213
|
-
PERMISSION_DECISION_REASON_KEY: refusal_reason,
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
)
|
|
217
|
-
)
|
|
218
|
-
sys.stdout.flush()
|
|
219
|
-
return 0
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if __name__ == "__main__":
|
|
223
|
-
sys.exit(main())
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the artifact look gate.
|
|
2
|
-
|
|
3
|
-
The refused case replays a session that wrote a page and published it with no
|
|
4
|
-
render in between, the way a text-card page reached the person who asked for
|
|
5
|
-
a diagram.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import os
|
|
10
|
-
import subprocess
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
_BLOCKING_DIR = Path(__file__).resolve().parent
|
|
15
|
-
_HOOKS_ROOT = _BLOCKING_DIR.parent
|
|
16
|
-
for each_sys_path_entry in (str(_BLOCKING_DIR), str(_HOOKS_ROOT)):
|
|
17
|
-
if each_sys_path_entry not in sys.path:
|
|
18
|
-
sys.path.insert(0, each_sys_path_entry)
|
|
19
|
-
|
|
20
|
-
import artifact_look_gate
|
|
21
|
-
|
|
22
|
-
HOOK_SCRIPT = _BLOCKING_DIR / "artifact_look_gate.py"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _tool_use(call_id: str, tool_name: str, file_path: str) -> dict[str, object]:
|
|
26
|
-
return {
|
|
27
|
-
"type": "assistant",
|
|
28
|
-
"message": {
|
|
29
|
-
"content": [
|
|
30
|
-
{
|
|
31
|
-
"type": "tool_use",
|
|
32
|
-
"id": call_id,
|
|
33
|
-
"name": tool_name,
|
|
34
|
-
"input": {"file_path": file_path},
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _tool_result(
|
|
42
|
-
call_id: str, is_error: bool = False, timestamp: str = "2026-01-01T00:00:00Z"
|
|
43
|
-
) -> dict[str, object]:
|
|
44
|
-
return {
|
|
45
|
-
"type": "user",
|
|
46
|
-
"timestamp": timestamp,
|
|
47
|
-
"message": {
|
|
48
|
-
"content": [
|
|
49
|
-
{
|
|
50
|
-
"type": "tool_result",
|
|
51
|
-
"tool_use_id": call_id,
|
|
52
|
-
"content": "ok",
|
|
53
|
-
"is_error": is_error,
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _run_hook(tmp_path: Path, all_records: list[dict[str, object]], page_path: Path) -> str:
|
|
61
|
-
transcript_path = tmp_path / "transcript.jsonl"
|
|
62
|
-
transcript_path.write_text(
|
|
63
|
-
"".join(json.dumps(each_record) + "\n" for each_record in all_records), encoding="utf-8"
|
|
64
|
-
)
|
|
65
|
-
completed = subprocess.run(
|
|
66
|
-
[sys.executable, str(HOOK_SCRIPT)],
|
|
67
|
-
input=json.dumps(
|
|
68
|
-
{
|
|
69
|
-
"hook_event_name": "PreToolUse",
|
|
70
|
-
"tool_name": "Artifact",
|
|
71
|
-
"tool_input": {"file_path": str(page_path), "icon": "map"},
|
|
72
|
-
"transcript_path": str(transcript_path),
|
|
73
|
-
}
|
|
74
|
-
),
|
|
75
|
-
capture_output=True,
|
|
76
|
-
text=True,
|
|
77
|
-
check=True,
|
|
78
|
-
)
|
|
79
|
-
return completed.stdout
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _file_at(file_path: Path, modified_time: float) -> Path:
|
|
83
|
-
file_path.write_text("x", encoding="utf-8")
|
|
84
|
-
os.utime(file_path, (modified_time, modified_time))
|
|
85
|
-
return file_path
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _read_records(
|
|
89
|
-
image_path: Path, is_error: bool = False, timestamp: str = "2026-01-01T00:00:00Z"
|
|
90
|
-
) -> list[dict[str, object]]:
|
|
91
|
-
return [_tool_use("r1", "Read", str(image_path)), _tool_result("r1", is_error, timestamp)]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_should_refuse_a_page_published_without_a_look(tmp_path: Path) -> None:
|
|
95
|
-
page_path = _file_at(tmp_path / "page.html", 1000)
|
|
96
|
-
hook_output = json.loads(
|
|
97
|
-
_run_hook(
|
|
98
|
-
tmp_path, [_tool_use("w1", "Write", str(page_path)), _tool_result("w1")], page_path
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
specific_output = hook_output["hookSpecificOutput"]
|
|
102
|
-
assert specific_output["permissionDecision"] == "deny"
|
|
103
|
-
assert "--screenshot" in specific_output["permissionDecisionReason"]
|
|
104
|
-
assert "Attach" not in specific_output["permissionDecisionReason"]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_should_allow_a_page_whose_newer_screenshot_was_read(tmp_path: Path) -> None:
|
|
108
|
-
page_path = _file_at(tmp_path / "page.html", 1000)
|
|
109
|
-
image_path = _file_at(tmp_path / "page.png", 1001)
|
|
110
|
-
assert _run_hook(tmp_path, _read_records(image_path), page_path) == ""
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def test_should_refuse_when_the_page_changed_after_the_screenshot(tmp_path: Path) -> None:
|
|
114
|
-
image_path = _file_at(tmp_path / "page.png", 1000)
|
|
115
|
-
page_path = _file_at(tmp_path / "page.html", 1001)
|
|
116
|
-
assert "deny" in _run_hook(tmp_path, _read_records(image_path), page_path)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_should_refuse_when_the_screenshot_read_failed(tmp_path: Path) -> None:
|
|
120
|
-
page_path = _file_at(tmp_path / "page.html", 1000)
|
|
121
|
-
image_path = _file_at(tmp_path / "page.png", 1001)
|
|
122
|
-
assert "deny" in _run_hook(tmp_path, _read_records(image_path, is_error=True), page_path)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def test_should_refuse_a_screenshot_overwritten_after_its_read(tmp_path: Path) -> None:
|
|
126
|
-
page_path = _file_at(tmp_path / "page.html", 1000)
|
|
127
|
-
image_path = _file_at(tmp_path / "page.png", 1001)
|
|
128
|
-
read_before_the_page_changed = "1970-01-01T00:16:39Z"
|
|
129
|
-
assert "deny" in _run_hook(
|
|
130
|
-
tmp_path, _read_records(image_path, timestamp=read_before_the_page_changed), page_path
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def test_page_was_looked_at_ignores_reads_of_other_files(tmp_path: Path) -> None:
|
|
135
|
-
page_path = _file_at(tmp_path / "page.html", 1000)
|
|
136
|
-
notes_path = _file_at(tmp_path / "notes.md", 1001)
|
|
137
|
-
all_lines = [json.dumps(each_record) for each_record in _read_records(notes_path)]
|
|
138
|
-
assert not artifact_look_gate.page_was_looked_at(all_lines, str(page_path))
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def test_unseen_page_reason_passes_non_publish_calls() -> None:
|
|
142
|
-
assert (
|
|
143
|
-
artifact_look_gate.unseen_page_reason(
|
|
144
|
-
{"tool_name": "Artifact", "tool_input": {"action": "read", "url": "x"}}
|
|
145
|
-
)
|
|
146
|
-
is None
|
|
147
|
-
)
|
|
148
|
-
assert (
|
|
149
|
-
artifact_look_gate.unseen_page_reason(
|
|
150
|
-
{"tool_name": "Artifact", "tool_input": {"file_path": "image.png", "asset": True}}
|
|
151
|
-
)
|
|
152
|
-
is None
|
|
153
|
-
)
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Names and refusal text for the artifact_look_gate PreToolUse hook."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
ARTIFACT_TOOL_NAME = "Artifact"
|
|
6
|
-
ARTIFACT_ACTION_INPUT_KEY = "action"
|
|
7
|
-
ARTIFACT_PUBLISH_ACTION = "publish"
|
|
8
|
-
ARTIFACT_ASSET_INPUT_KEY = "asset"
|
|
9
|
-
FILE_PATH_INPUT_KEY = "file_path"
|
|
10
|
-
TOOL_NAME_KEY = "tool_name"
|
|
11
|
-
TOOL_INPUT_KEY = "tool_input"
|
|
12
|
-
TRANSCRIPT_PATH_KEY = "transcript_path"
|
|
13
|
-
IMAGE_READING_TOOL_NAME = "Read"
|
|
14
|
-
ALL_IMAGE_FILE_SUFFIXES = (".png", ".jpg", ".jpeg", ".webp", ".gif")
|
|
15
|
-
ALL_PAGE_FILE_SUFFIXES = (".html", ".htm")
|
|
16
|
-
TRANSCRIPT_ENCODING = "utf-8"
|
|
17
|
-
TRANSCRIPT_DECODE_ERRORS = "replace"
|
|
18
|
-
ASSISTANT_ENTRY_TYPE = "assistant"
|
|
19
|
-
USER_ENTRY_TYPE = "user"
|
|
20
|
-
TOOL_USE_BLOCK_TYPE = "tool_use"
|
|
21
|
-
TOOL_RESULT_BLOCK_TYPE = "tool_result"
|
|
22
|
-
ENTRY_TIMESTAMP_KEY = "timestamp"
|
|
23
|
-
PRE_TOOL_USE_EVENT_NAME = "PreToolUse"
|
|
24
|
-
DENY_DECISION = "deny"
|
|
25
|
-
UNSEEN_PAGE_REASON_TEMPLATE = (
|
|
26
|
-
"This page has not been looked at since it was last written. Render it "
|
|
27
|
-
"and look at the picture before you publish: take a screenshot with a "
|
|
28
|
-
"headless browser, for example `chromium --headless --screenshot=page.png "
|
|
29
|
-
"--window-size=800,900 file://{page_path}`, then open page.png with the "
|
|
30
|
-
"Read tool and check it against the ask. Fix what the picture shows, take "
|
|
31
|
-
"a new screenshot, and publish again."
|
|
32
|
-
)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# ASD-STE100 language policy
|
|
2
|
-
|
|
3
|
-
**When:** Writing chat, tool narration, or repository prose.
|
|
4
|
-
|
|
5
|
-
Use the sole general language rule. Write short, complete sentences on one topic. Use active voice; lead with conditions; one action per step. Use plain, precise words, stable terms. Expand abbreviations and contractions; name unclear pronouns. Be inclusive; punctuate clearly. Preserve exact labels, identifiers, formulas, titles, interface text. Say each fact once. Send a result, blocker, or question. Mark injury or death `WARNING`, equipment damage `CAUTION`; state condition then result. Aim for 20 words per step, 25 per description. Treat checks as aids; have a human verify accuracy, terms, safety, confidentiality, meaning.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/asd-ste100-language.md). Read it for sources.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Clean up temporary files
|
|
2
|
-
|
|
3
|
-
**When:** Creating scratch files, debug dumps, or one-off helpers during a task.
|
|
4
|
-
|
|
5
|
-
Prefer memory to scratch files and track any temporary files you create. At completion, remove those files and leave user-requested files in place. Files under the OS temporary root or `$CLAUDE_JOB_DIR` need no explicit removal; a parent handles child-agent scratch.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/cleanup-temp-files.md). Read it to classify a file or choose cleanup.
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Destructive commands in Bash
|
|
2
|
-
|
|
3
|
-
**When:** Removing files or writing a destructive command string.
|
|
4
|
-
|
|
5
|
-
Keep destructive literals out of command strings, including data. Use literal absolute targets; never target a bare temporary root. Use `git rm` for tracked files. Pass bodies by file and test hooks through the test suite. Copy this line into every subagent prompt:
|
|
6
|
-
|
|
7
|
-
> Never use bash rm in any form. Delete scratch/probe files with the PowerShell tool (Remove-Item -Recurse -Force -Confirm:$false <absolute path>), or leave them in the OS temp dir; remove worktrees only via git worktree remove --force.
|
|
8
|
-
|
|
9
|
-
**Enforcement:** none; a harness permission prompt can stall unattended work.
|
|
10
|
-
|
|
11
|
-
**Full text:** [guide](../docs/rule-guides/destructive-commands.md). Read it before any removal.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Explore thoroughly
|
|
2
|
-
|
|
3
|
-
**When:** Before you log, plan, delegate, or build an ask, or choose an approach.
|
|
4
|
-
|
|
5
|
-
Search for what already does the ask, in full or in part. When something exists, stop and report it to the user in four points; run the `search-before-acting` skill. Then read files, patterns, and constraints, scaled to risk. Once the files, constraints, and success condition are known, act without repeating settled research.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** the pull request gate denies a body with no "Existing work" section.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/explore-thoroughly.md). Read it for search scope and depth.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Features start with an eval
|
|
2
|
-
|
|
3
|
-
**When:** A user asks for a new feature or a change in behavior.
|
|
4
|
-
|
|
5
|
-
Your first action is the Skill tool with skill `claude-api` and args `build-eval`. Build the feature against that eval. When the work touches an existing feature with no eval, start a separate session to build that eval in parallel. An Agent-tool subagent does not count. You own its result.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** `hooks/blocking/pr_lifecycle_skill_gate.py` denies a `feat` pull request with no "Eval" section or no build-eval call.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/features-start-with-an-eval.md).
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Filesystem search
|
|
2
|
-
|
|
3
|
-
**When:** Searching for files by name, path, extension, size, or date.
|
|
4
|
-
|
|
5
|
-
Scope every search to a project, worktree, package, or narrowing filter. Never start at a filesystem root, drive root, bare home, or share root. Read a known path directly; use scoped `es.exe`, Glob, or Grep for discovery. After `es.exe` Error 8, retry twice before falling back within the same scope. Run one large shell walk at a time.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/filesystem-search.md). Read it for tool selection and search examples.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Memory stores durable facts
|
|
2
|
-
|
|
3
|
-
**When:** Writing auto-memory or adding a `MEMORY.md` entry.
|
|
4
|
-
|
|
5
|
-
Keep only a fact that helps a fresh session weeks later. Remove dates, task identifiers, commits, and paths from a draft; if the remainder is empty or false, put it in the task tracker or handoff instead. Store standing user preferences, stable setup traps, and useful resource pointers. Delete stale memory and its index line in the same run.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/memory-stores-durable-facts.md). Read it when deciding whether a fact belongs in memory.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# No contrast framing
|
|
2
|
-
|
|
3
|
-
**When:** Writing any sentence a person reads.
|
|
4
|
-
|
|
5
|
-
State the chosen point directly and leave out the discarded reading. Remove these six forms: `trailing-comma-not`, `corrective-it-is-not`, `substitution-rather-than`, `additive-not-just`, `comparative-ranking`, `substitution-as-opposed-to`. Keep quantity comparisons. If a sentence loses substance, add the failing check, log line, measured number, or file and line.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** staged policy lint and `scripts/durable_post_lint.py` check authored text; the agent checks chat.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/no-contrast-framing.md). Read it for examples and checker exceptions.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Proof before a pull request
|
|
2
|
-
|
|
3
|
-
**When:** Before you open a pull request.
|
|
4
|
-
|
|
5
|
-
Run the change where it runs, with and without it, and quote both results under "Proof in practice" in the body. A hook denies a body without one. Unproven: leave it unopened and tell the user.
|
|
6
|
-
|
|
7
|
-
**Full text:** [guide](../docs/rule-guides/proof-before-pull-request.md).
|
package/rules/request-scope.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Request scope
|
|
2
|
-
|
|
3
|
-
**When:** Asked whether part of a block is still needed, or asked to change one.
|
|
4
|
-
|
|
5
|
-
A shipped gate makes each block line restating what it enforces dead text. Propose removing all of them, past the part asked about, and give each kept line's reason. Read the output back before reporting done.
|
|
6
|
-
|
|
7
|
-
**Full text:** [guide](../docs/rule-guides/request-scope.md)
|
package/rules/research-mode.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Research mode
|
|
2
|
-
|
|
3
|
-
**When:** Making a factual claim, recommendation, or piece of advice.
|
|
4
|
-
|
|
5
|
-
State no claim without a credible source; do not guess or infer. Before an answer depends on an unsettled fact, run each tool that could settle it and that reads state or that permissions already allow, and stop at the first one that settles it. When none can, name the exact check and who can run or approve it. Cite a project file, web source, named expert or paper, or official documentation for each claim. Retract claims a source cannot support. Extract document text before analysis and ground the answer in word-for-word quotes. In chat, cite compactly with a link or `file:line`; put full quotes and citation lists in artifacts or replies that request them. Ground synthesis in sourced inputs. Creative ideas need no citation.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/research-mode.md). Read it when checking a source or citation.
|
package/rules/session-title.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Status in the session title
|
|
2
|
-
|
|
3
|
-
**When:** The session has a tool whose name ends in `__set_session_title`.
|
|
4
|
-
|
|
5
|
-
Before each turn ends, set the title to `<emoji> <name>`. Use the red flag when the user must act, the check mark when the work is done, and the hourglass while work, CI, or a merge queue runs. The name has 25 characters or fewer, in sentence case, with concrete nouns and no IDs, dates, or branch names.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** `hooks/blocking/session_title_format_gate.py` denies a malformed title; `hooks/blocking/session_title_stop_gate.py` blocks a turn end with no title.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/session-title.md). Read it for the emoji table and naming rules.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Shell invocation
|
|
2
|
-
|
|
3
|
-
**When:** Issuing a Bash-tool shell command, or writing a `settings.json` permission rule.
|
|
4
|
-
|
|
5
|
-
On Windows, run shell work through `pwsh`: `pwsh -NoProfile -File '<script>.ps1' <args>` for a script, and `pwsh -NoProfile -Command "..."` or the `PowerShell` tool for inline work. A direct interpreter call such as `python script.py` conforms as written. Keep `powershell`, `cmd /c`, and `bash -c` out of permission rules. Keep `$(...)`, unescaped backticks, and `<(...)` or `>(...)` out of Bash commands; split the call or use a flag form such as `git -C "<path>"`. Arithmetic `$((...))` passes.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none; a permission prompt on a wrapped command is the signal. `blocking/msys_rev_path_rewriter.py` only rewrites a `<rev>:<path>` argument.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/shell-invocation.md). Read it before writing a permission rule or a multi-step shell command.
|
package/rules/skill-pointers.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Verify before asking
|
|
2
|
-
|
|
3
|
-
**When:** Before asking the user a clarifying question.
|
|
4
|
-
|
|
5
|
-
Inspect files, directories, configuration, environment, databases, and available tools for the answer. Recheck facts recalled from earlier sessions. Apply any criterion the user already supplied and state the decision. Ask only for a judgment, preference, or inaccessible fact.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/verify-before-asking.md). Read it when the source of an answer is uncertain.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Verify runtime state
|
|
2
|
-
|
|
3
|
-
**When:** Before saying a component works, is healthy, or is not the cause of a failure.
|
|
4
|
-
|
|
5
|
-
Gather a live signal this session: process list, port probe, log, status code, or fresh reproduction. Read the user's terminal and named error log while a reported failure is still visible. Check the effect the work should produce, including each job result, loaded config, or deployed artifact; a success status alone does not establish the effect. When testing code, identify the loaded module path.
|
|
6
|
-
|
|
7
|
-
**Enforcement:** none.
|
|
8
|
-
|
|
9
|
-
**Full text:** [guide](../docs/rule-guides/verify-runtime-state.md). Read it when choosing the probe for a claim.
|