claude-dev-env 2.3.0 → 2.4.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/CLAUDE.md +3 -2
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +3 -3
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +13 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -903
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +27 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +2 -2
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Tests for the sandbox safety-hook probe.
|
|
2
|
+
|
|
3
|
+
The block-detection unit tests drive fixture hook scripts, and the
|
|
4
|
+
integration tests point a settings file at the real installed safety hooks
|
|
5
|
+
to prove they block their probe payloads end to end.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
from prototype_scripts_constants.config.build_sandbox_settings_constants import (
|
|
17
|
+
ALL_SAFETY_HOOK_SCRIPT_BASENAMES,
|
|
18
|
+
COMMAND_KEY,
|
|
19
|
+
HOOKS_KEY,
|
|
20
|
+
MATCHER_KEY,
|
|
21
|
+
PRE_TOOL_USE_KEY,
|
|
22
|
+
)
|
|
23
|
+
from prototype_scripts_constants.config.probe_sandbox_safety_constants import (
|
|
24
|
+
DESTRUCTIVE_PROBE_COMMAND,
|
|
25
|
+
ENVELOPE_TOOL_INPUT_KEY,
|
|
26
|
+
ENVELOPE_TOOL_NAME_KEY,
|
|
27
|
+
PII_PROBE_TOKEN_PREFIX,
|
|
28
|
+
PII_PROBE_TOOL_NAME,
|
|
29
|
+
PROBE_FAILURE_EXIT_CODE,
|
|
30
|
+
PROBE_SUCCESS_EXIT_CODE,
|
|
31
|
+
TOOL_INPUT_COMMAND_KEY,
|
|
32
|
+
TOOL_INPUT_CONTENT_KEY,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
36
|
+
PROBE_PATH = SCRIPTS_DIRECTORY / "probe_sandbox_safety.py"
|
|
37
|
+
HOOKS_BLOCKING_DIRECTORY = SCRIPTS_DIRECTORY.parents[2] / "hooks" / "blocking"
|
|
38
|
+
|
|
39
|
+
PII_HOOK_BASENAME = ALL_SAFETY_HOOK_SCRIPT_BASENAMES[0]
|
|
40
|
+
DESTRUCTIVE_HOOK_BASENAME = ALL_SAFETY_HOOK_SCRIPT_BASENAMES[1]
|
|
41
|
+
|
|
42
|
+
DENY_HOOK_SOURCE = (
|
|
43
|
+
"import json, sys\n"
|
|
44
|
+
"sys.stdout.write(json.dumps("
|
|
45
|
+
'{"hookSpecificOutput": {"permissionDecision": "deny"}}))\n'
|
|
46
|
+
)
|
|
47
|
+
ASK_HOOK_SOURCE = (
|
|
48
|
+
"import json, sys\n"
|
|
49
|
+
"sys.stdout.write(json.dumps("
|
|
50
|
+
'{"hookSpecificOutput": {"permissionDecision": "ask"}}))\n'
|
|
51
|
+
)
|
|
52
|
+
ALLOW_HOOK_SOURCE = "import sys\nsys.exit(0)\n"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_probe_module() -> ModuleType:
|
|
56
|
+
spec = importlib.util.spec_from_file_location("probe_sandbox_safety", PROBE_PATH)
|
|
57
|
+
assert spec is not None
|
|
58
|
+
assert spec.loader is not None
|
|
59
|
+
probe_module = importlib.util.module_from_spec(spec)
|
|
60
|
+
spec.loader.exec_module(probe_module)
|
|
61
|
+
return probe_module
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def quoted_command(script_path: Path) -> str:
|
|
65
|
+
return f'"{sys.executable}" "{script_path}"'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def real_hook_settings_document() -> dict:
|
|
69
|
+
pii_hook_path = HOOKS_BLOCKING_DIRECTORY / PII_HOOK_BASENAME
|
|
70
|
+
destructive_hook_path = HOOKS_BLOCKING_DIRECTORY / DESTRUCTIVE_HOOK_BASENAME
|
|
71
|
+
return {
|
|
72
|
+
HOOKS_KEY: {
|
|
73
|
+
PRE_TOOL_USE_KEY: [
|
|
74
|
+
{
|
|
75
|
+
MATCHER_KEY: "Write",
|
|
76
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(pii_hook_path)}],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
MATCHER_KEY: "Bash",
|
|
80
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(destructive_hook_path)}],
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def real_hook_deny_mode_settings_document() -> dict:
|
|
88
|
+
settings_document = real_hook_settings_document()
|
|
89
|
+
settings_document["env"] = {"CLAUDE_DESTRUCTIVE_DENY_MODE": "1"}
|
|
90
|
+
return settings_document
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_settings_file(tmp_path: Path, settings_document: dict) -> Path:
|
|
94
|
+
settings_path = tmp_path / "sandbox-settings.json"
|
|
95
|
+
settings_path.write_text(json.dumps(settings_document), encoding="utf-8")
|
|
96
|
+
return settings_path
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_settings_environment_overrides_reads_the_env_block() -> None:
|
|
100
|
+
probe = load_probe_module()
|
|
101
|
+
environment_overrides = probe.settings_environment_overrides(
|
|
102
|
+
real_hook_deny_mode_settings_document()
|
|
103
|
+
)
|
|
104
|
+
assert environment_overrides == {"CLAUDE_DESTRUCTIVE_DENY_MODE": "1"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_settings_environment_overrides_empty_without_an_env_block() -> None:
|
|
108
|
+
probe = load_probe_module()
|
|
109
|
+
environment_overrides = probe.settings_environment_overrides(
|
|
110
|
+
real_hook_settings_document()
|
|
111
|
+
)
|
|
112
|
+
assert environment_overrides == {}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_main_exits_zero_when_the_real_destructive_hook_denies_in_deny_mode(
|
|
116
|
+
tmp_path: Path,
|
|
117
|
+
) -> None:
|
|
118
|
+
probe = load_probe_module()
|
|
119
|
+
settings_path = write_settings_file(
|
|
120
|
+
tmp_path, real_hook_deny_mode_settings_document()
|
|
121
|
+
)
|
|
122
|
+
exit_code = probe.main(["--settings", str(settings_path)])
|
|
123
|
+
assert exit_code == PROBE_SUCCESS_EXIT_CODE
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_pii_basename_probe_payload_carries_the_secret() -> None:
|
|
127
|
+
probe = load_probe_module()
|
|
128
|
+
probe_payload = probe.build_probe_payload_for_basename(PII_HOOK_BASENAME)
|
|
129
|
+
assert probe_payload[ENVELOPE_TOOL_NAME_KEY] == PII_PROBE_TOOL_NAME
|
|
130
|
+
content = probe_payload[ENVELOPE_TOOL_INPUT_KEY][TOOL_INPUT_CONTENT_KEY]
|
|
131
|
+
assert PII_PROBE_TOKEN_PREFIX in content
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_destructive_basename_probe_payload_carries_the_command() -> None:
|
|
135
|
+
probe = load_probe_module()
|
|
136
|
+
probe_payload = probe.build_probe_payload_for_basename(DESTRUCTIVE_HOOK_BASENAME)
|
|
137
|
+
command = probe_payload[ENVELOPE_TOOL_INPUT_KEY][TOOL_INPUT_COMMAND_KEY]
|
|
138
|
+
assert command == DESTRUCTIVE_PROBE_COMMAND
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_find_hook_command_for_basename_returns_the_matching_command() -> None:
|
|
142
|
+
probe = load_probe_module()
|
|
143
|
+
settings_document = real_hook_settings_document()
|
|
144
|
+
command = probe.find_hook_command_for_basename(settings_document, PII_HOOK_BASENAME)
|
|
145
|
+
assert command is not None
|
|
146
|
+
assert PII_HOOK_BASENAME in command
|
|
147
|
+
assert probe.find_hook_command_for_basename(settings_document, "absent.py") is None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_parse_command_argv_splits_and_unquotes_the_command() -> None:
|
|
151
|
+
probe = load_probe_module()
|
|
152
|
+
command_argv = probe.parse_command_argv('"/usr/bin/python" "/a/b hook.py"')
|
|
153
|
+
assert command_argv == ["/usr/bin/python", "/a/b hook.py"]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_hook_blocks_probe_reports_true_for_a_deny_hook(tmp_path: Path) -> None:
|
|
157
|
+
probe = load_probe_module()
|
|
158
|
+
deny_hook_path = tmp_path / "deny_hook.py"
|
|
159
|
+
deny_hook_path.write_text(DENY_HOOK_SOURCE, encoding="utf-8")
|
|
160
|
+
command_argv = [sys.executable, str(deny_hook_path)]
|
|
161
|
+
probe_payload = probe.build_probe_payload_for_basename(DESTRUCTIVE_HOOK_BASENAME)
|
|
162
|
+
assert probe.hook_blocks_probe(command_argv, probe_payload, {}) is True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_hook_blocks_probe_reports_false_for_an_ask_hook(tmp_path: Path) -> None:
|
|
166
|
+
probe = load_probe_module()
|
|
167
|
+
ask_hook_path = tmp_path / "ask_hook.py"
|
|
168
|
+
ask_hook_path.write_text(ASK_HOOK_SOURCE, encoding="utf-8")
|
|
169
|
+
command_argv = [sys.executable, str(ask_hook_path)]
|
|
170
|
+
probe_payload = probe.build_probe_payload_for_basename(DESTRUCTIVE_HOOK_BASENAME)
|
|
171
|
+
assert probe.hook_blocks_probe(command_argv, probe_payload, {}) is False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_hook_blocks_probe_reports_false_for_an_allow_hook(tmp_path: Path) -> None:
|
|
175
|
+
probe = load_probe_module()
|
|
176
|
+
allow_hook_path = tmp_path / "allow_hook.py"
|
|
177
|
+
allow_hook_path.write_text(ALLOW_HOOK_SOURCE, encoding="utf-8")
|
|
178
|
+
command_argv = [sys.executable, str(allow_hook_path)]
|
|
179
|
+
probe_payload = probe.build_probe_payload_for_basename(DESTRUCTIVE_HOOK_BASENAME)
|
|
180
|
+
assert probe.hook_blocks_probe(command_argv, probe_payload, {}) is False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_probe_safety_hook_blocks_against_the_real_pii_hook() -> None:
|
|
184
|
+
probe = load_probe_module()
|
|
185
|
+
settings_document = real_hook_settings_document()
|
|
186
|
+
assert probe.probe_safety_hook(settings_document, PII_HOOK_BASENAME) is True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_main_exits_three_when_the_real_destructive_hook_only_asks(
|
|
190
|
+
tmp_path: Path,
|
|
191
|
+
) -> None:
|
|
192
|
+
probe = load_probe_module()
|
|
193
|
+
settings_path = write_settings_file(tmp_path, real_hook_settings_document())
|
|
194
|
+
exit_code = probe.main(["--settings", str(settings_path)])
|
|
195
|
+
assert exit_code == PROBE_FAILURE_EXIT_CODE
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_main_exits_zero_when_both_real_hooks_hard_deny(tmp_path: Path) -> None:
|
|
199
|
+
probe = load_probe_module()
|
|
200
|
+
deny_pii_hook_path = tmp_path / PII_HOOK_BASENAME
|
|
201
|
+
deny_pii_hook_path.write_text(DENY_HOOK_SOURCE, encoding="utf-8")
|
|
202
|
+
deny_destructive_hook_path = tmp_path / DESTRUCTIVE_HOOK_BASENAME
|
|
203
|
+
deny_destructive_hook_path.write_text(DENY_HOOK_SOURCE, encoding="utf-8")
|
|
204
|
+
settings_document = {
|
|
205
|
+
HOOKS_KEY: {
|
|
206
|
+
PRE_TOOL_USE_KEY: [
|
|
207
|
+
{
|
|
208
|
+
MATCHER_KEY: "Write",
|
|
209
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(deny_pii_hook_path)}],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
MATCHER_KEY: "Bash",
|
|
213
|
+
HOOKS_KEY: [
|
|
214
|
+
{COMMAND_KEY: quoted_command(deny_destructive_hook_path)}
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
settings_path = write_settings_file(tmp_path, settings_document)
|
|
221
|
+
exit_code = probe.main(["--settings", str(settings_path)])
|
|
222
|
+
assert exit_code == PROBE_SUCCESS_EXIT_CODE
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_main_exits_three_when_a_hook_allows(tmp_path: Path) -> None:
|
|
226
|
+
probe = load_probe_module()
|
|
227
|
+
allow_pii_hook_path = tmp_path / PII_HOOK_BASENAME
|
|
228
|
+
allow_pii_hook_path.write_text(ALLOW_HOOK_SOURCE, encoding="utf-8")
|
|
229
|
+
destructive_hook_path = HOOKS_BLOCKING_DIRECTORY / DESTRUCTIVE_HOOK_BASENAME
|
|
230
|
+
settings_document = {
|
|
231
|
+
HOOKS_KEY: {
|
|
232
|
+
PRE_TOOL_USE_KEY: [
|
|
233
|
+
{
|
|
234
|
+
MATCHER_KEY: "Write",
|
|
235
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(allow_pii_hook_path)}],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
MATCHER_KEY: "Bash",
|
|
239
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(destructive_hook_path)}],
|
|
240
|
+
},
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
settings_path = write_settings_file(tmp_path, settings_document)
|
|
245
|
+
exit_code = probe.main(["--settings", str(settings_path)])
|
|
246
|
+
assert exit_code == PROBE_FAILURE_EXIT_CODE
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_main_exits_three_when_the_settings_file_is_absent(tmp_path: Path) -> None:
|
|
250
|
+
probe = load_probe_module()
|
|
251
|
+
absent_settings_path = tmp_path / "does-not-exist.json"
|
|
252
|
+
exit_code = probe.main(["--settings", str(absent_settings_path)])
|
|
253
|
+
assert exit_code == PROBE_FAILURE_EXIT_CODE
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_main_exits_three_when_the_settings_file_is_invalid_json(tmp_path: Path) -> None:
|
|
257
|
+
probe = load_probe_module()
|
|
258
|
+
settings_path = tmp_path / "sandbox-settings.json"
|
|
259
|
+
settings_path.write_text("{not valid json", encoding="utf-8")
|
|
260
|
+
exit_code = probe.main(["--settings", str(settings_path)])
|
|
261
|
+
assert exit_code == PROBE_FAILURE_EXIT_CODE
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_main_exits_three_when_a_hook_script_is_bogus(tmp_path: Path) -> None:
|
|
265
|
+
probe = load_probe_module()
|
|
266
|
+
bogus_pii_hook_path = tmp_path / PII_HOOK_BASENAME
|
|
267
|
+
destructive_hook_path = HOOKS_BLOCKING_DIRECTORY / DESTRUCTIVE_HOOK_BASENAME
|
|
268
|
+
settings_document = {
|
|
269
|
+
HOOKS_KEY: {
|
|
270
|
+
PRE_TOOL_USE_KEY: [
|
|
271
|
+
{
|
|
272
|
+
MATCHER_KEY: "Write",
|
|
273
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(bogus_pii_hook_path)}],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
MATCHER_KEY: "Bash",
|
|
277
|
+
HOOKS_KEY: [{COMMAND_KEY: quoted_command(destructive_hook_path)}],
|
|
278
|
+
},
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
settings_path = write_settings_file(tmp_path, settings_document)
|
|
283
|
+
exit_code = probe.main(["--settings", str(settings_path)])
|
|
284
|
+
assert exit_code == PROBE_FAILURE_EXIT_CODE
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Promotion workflow (Phase 2)
|
|
2
|
+
|
|
3
|
+
Turn a successful proof-of-concept into a real, verified change. Run every step in the normal, fully-hooked session — never inside the `--bare` sandbox. The sandbox produced a reference build; promotion re-verifies it to standard and often rewrites parts of it.
|
|
4
|
+
|
|
5
|
+
## Seed the task list first
|
|
6
|
+
|
|
7
|
+
Register every item in `reference/promotion-tasks.md` as a session task (`TaskCreate`, or `TodoWrite` if that is the host tool). Work only from the task list. Mark each complete with evidence — a command result, a path, a verdict, or a skill's return.
|
|
8
|
+
|
|
9
|
+
## The clean-room protocol
|
|
10
|
+
|
|
11
|
+
The task seeds carry the full ordered detail. The shape:
|
|
12
|
+
|
|
13
|
+
1. **Confirm** the POC is worth promoting and the user wants it shipped.
|
|
14
|
+
2. **Fresh branch** off freshly-fetched `origin/main` via `fresh-branch` — clean history, based on live upstream.
|
|
15
|
+
3. **Bring content as an uncommitted diff.** Copy the POC's file changes into the new branch's working tree. Do not cherry-pick or merge the sandbox commits; the sandbox history stays behind.
|
|
16
|
+
4. **Cleanup.** Remove scratch files, debug dumps, and temp helpers the POC created (`cleanup-temp-files` rule).
|
|
17
|
+
5. **Privacy sweep** via `privacy-hygiene` over the diff.
|
|
18
|
+
6. **Verify** with the `code-verifier` agent in a fresh context. This is where standards re-engage. Expect findings and a repair loop — the POC was un-TDD'd.
|
|
19
|
+
7. **Commit and PR.** Only on a clean verdict, run `/commit`, then open a draft PR per the `git-workflow` rule.
|
|
20
|
+
8. **State the honest limitations** from `reference/honest-limitations.md` in the PR body or to the user.
|
|
21
|
+
9. **Converge** by handing the PR to `autoconverge` by default; use `pr-converge` for paced ticks or `bugteam` for an open-loop audit.
|
|
22
|
+
|
|
23
|
+
## Why the clean room, not a push
|
|
24
|
+
|
|
25
|
+
`code_rules_enforcer` is a write-time Write/Edit gate. Content that lands through `git apply`, `git checkout`, or cherry-pick never passes through it, so pushing the sandbox branch would ship code the rule engine never saw, carrying sandbox scratch along with it. Standards re-engage through the `code-verifier` agent, the `privacy-hygiene` sweep, and the PR review — not through a write-time hook. Steps 4-6 are hard gates: the honest claim on promoted POC code is code-verifier-passed, privacy-swept, review-passed, with TDD ordering waived.
|
|
26
|
+
|
|
27
|
+
The `verified_commit_gate` hook is the backstop under all of this — it refuses a commit or push with no minted verdict — but the clean-room steps are the mechanism, not the gate firing.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Sandbox workflow (Phase 1)
|
|
2
|
+
|
|
3
|
+
Stand up an isolated worktree where a hookless agent builds a proof-of-concept with zero standards friction. Only two safety gates stay live: personal-data blocking and destructive-command blocking.
|
|
4
|
+
|
|
5
|
+
## Step 1 — Isolated worktree
|
|
6
|
+
|
|
7
|
+
Invoke the `fresh-branch` skill to create a worktree off `origin/main`. Keep the returned `worktree_path` and `base_commit` for the promotion phase. If `fresh-branch` is not installed, stop with the refusal line from `SKILL.md`.
|
|
8
|
+
|
|
9
|
+
## Step 2 — Minimal safety settings
|
|
10
|
+
|
|
11
|
+
Run `scripts/build_sandbox_settings.py --out <worktree>/.prototype-sandbox-settings.json`.
|
|
12
|
+
|
|
13
|
+
It reads the live `~/.claude/settings.json`, resolves each safety hook's command (the correct interpreter and absolute path for this machine), and registers it on the matchers the sandbox needs — personal-data on Write, Edit, MultiEdit, and Bash; destructive-command on Bash. The write file carries only those two gates, plus an `env` block that sets `CLAUDE_DESTRUCTIVE_DENY_MODE`, so the destructive gate hard-denies a match. A hard deny holds under `--dangerously-skip-permissions`; an `ask` there is auto-resolved.
|
|
14
|
+
|
|
15
|
+
- Exit 0 → continue with the printed settings path.
|
|
16
|
+
- Exit 2 → one of the two safety hooks could not be resolved from the live settings. Stop; the sandbox cannot be contained without both. Report which one is missing.
|
|
17
|
+
|
|
18
|
+
## Step 3 — Prove containment
|
|
19
|
+
|
|
20
|
+
Run `scripts/probe_sandbox_safety.py --settings <settings path>`.
|
|
21
|
+
|
|
22
|
+
It feeds each safety hook a payload the hook is proven to block and confirms a hard-deny decision — the block that holds even under `--dangerously-skip-permissions`, unlike an `ask` prompt the flag auto-resolves. It runs each hook under the settings' `env` block, so the destructive gate runs in deny mode, the same way the launched session runs it.
|
|
23
|
+
|
|
24
|
+
- Exit 0 → both gates hard-deny their probe; the sandbox is contained.
|
|
25
|
+
- Exit 3 → a gate allowed its probe, returned an `ask`, or errored. Stop; do not launch an uncontained sandbox. This is a runtime check, not a code read (verify-runtime-state) — do not skip it.
|
|
26
|
+
|
|
27
|
+
## Step 4 — Launch the hookless session
|
|
28
|
+
|
|
29
|
+
Run `scripts/launch_sandbox.py --worktree <worktree_path> --settings <settings path> --task-file <task file>`.
|
|
30
|
+
|
|
31
|
+
It starts a headless `claude -p --bare --dangerously-skip-permissions --settings <settings path>` session in the worktree, so every standards gate is skipped and the two safety gates from the settings file are the only containment. Write the proof-of-concept task to the task file first; the sandbox agent cannot invoke other skills, so the task must be self-contained.
|
|
32
|
+
|
|
33
|
+
## Step 5 — Judge the result
|
|
34
|
+
|
|
35
|
+
Read what the sandbox built and decide whether the proof-of-concept proves the idea. On success, move to `promotion.md`. To discard, remove the worktree with `git worktree remove --force <worktree_path>`.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# skill-builder
|
|
2
2
|
|
|
3
|
-
Orchestrates the skill-building lifecycle: classify type, scaffold folders, write via `skill-writer
|
|
3
|
+
Orchestrates the skill-building lifecycle: classify type, scaffold folders, write via the `skill-writer-agent` agent, enforce modularity (sub-skills / composition), write description as a trigger catalog, require deterministic steps as code, self-audit, and refine from real usage.
|
|
4
4
|
|
|
5
5
|
**Trigger:** build a skill, new skill workflow, improve this skill, optimize skill description, skill development lifecycle, skill modularity, description trigger catalog, deterministic skill scripts.
|
|
6
6
|
|
|
7
7
|
## Purpose
|
|
8
8
|
|
|
9
|
-
Enforces craft standards for new and existing skills. For quick one-off SKILL.md edits,
|
|
9
|
+
Enforces craft standards for new and existing skills. For quick one-off SKILL.md edits, spawn the `skill-writer-agent` agent directly. This skill classifies, scaffolds, gathers context (composition plan, description triggers, deterministic inventory), delegates writing, and self-audits.
|
|
10
10
|
|
|
11
11
|
## Key files
|
|
12
12
|
|
|
@@ -45,5 +45,5 @@ Enforces craft standards for new and existing skills. For quick one-off SKILL.md
|
|
|
45
45
|
|
|
46
46
|
- Every build ends with the self-audit at `references/self-audit-checklist.md`; fix failures before delivery.
|
|
47
47
|
- Modularity items, description trigger-catalog items, and deterministic-element classification are mandatory on every delivery.
|
|
48
|
-
- `skill-builder` orchestrates; `skill-writer` authors. Handoff packet must include type, gap analysis, composition plan, description trigger catalog, deterministic inventory, degree-of-freedom assessment, and constraints.
|
|
48
|
+
- `skill-builder` orchestrates; the `skill-writer-agent` agent authors. Handoff packet must include type, gap analysis, composition plan, description trigger catalog, deterministic inventory, degree-of-freedom assessment, and constraints.
|
|
49
49
|
- Claude A / Claude B: Claude A (this session) designs; Claude B (subagents) tests the built skill on real tasks.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-builder
|
|
3
3
|
description: >-
|
|
4
|
-
Skill lifecycle: classify, scaffold, write via skill-writer, self-audit, compose
|
|
4
|
+
Skill lifecycle: classify, scaffold, write via the skill-writer-agent, self-audit, compose
|
|
5
5
|
sub-skills, polish description triggers, enforce deterministic scripts. Triggers:
|
|
6
6
|
build a skill, new skill workflow, improve this skill, optimize skill description,
|
|
7
7
|
skill development lifecycle, skill modularity, skill composition, description
|
|
@@ -28,9 +28,9 @@ Highest-signal content. Append a bullet each time a skill build fails in a new w
|
|
|
28
28
|
|
|
29
29
|
## When this skill applies
|
|
30
30
|
|
|
31
|
-
Trigger for requests to **build**, **improve**, or **polish** a skill. This skill orchestrates the process — it classifies, scaffolds, gathers context, delegates writing to
|
|
31
|
+
Trigger for requests to **build**, **improve**, or **polish** a skill. This skill orchestrates the process — it classifies, scaffolds, gathers context, delegates writing to the `skill-writer-agent` agent, and self-audits the result.
|
|
32
32
|
|
|
33
|
-
For quick skill syntax questions or one-off SKILL.md edits,
|
|
33
|
+
For quick skill syntax questions or one-off SKILL.md edits, spawn the `skill-writer-agent` agent directly.
|
|
34
34
|
|
|
35
35
|
**Refusal cases — first match wins:**
|
|
36
36
|
|
|
@@ -161,9 +161,9 @@ Also: story-form descriptions; monolith multi-capability skills; silent reimplem
|
|
|
161
161
|
|
|
162
162
|
After every build, improvement, or polish pass, load `${CLAUDE_SKILL_DIR}/references/self-audit-checklist.md`, **register every bullet as a session task**, and complete each with evidence. Every item must pass before delivery. Fix failures, then re-audit.
|
|
163
163
|
|
|
164
|
-
## Delegation to skill-writer
|
|
164
|
+
## Delegation to skill-writer-agent
|
|
165
165
|
|
|
166
|
-
skill-builder orchestrates; skill-writer authors. The handoff packet from Step 4 must include:
|
|
166
|
+
skill-builder orchestrates; the `skill-writer-agent` agent authors. The caller spawns it with `Agent(subagent_type="skill-writer-agent", ...)`. The handoff packet from Step 4 must include:
|
|
167
167
|
|
|
168
168
|
- Skill type and folder structure
|
|
169
169
|
- Gap analysis or observation findings (composition plan + description triggers + deterministic inventory)
|
|
@@ -12,7 +12,7 @@ Best-practice specifications and the mandatory self-audit checklist for the `/sk
|
|
|
12
12
|
| `skill-modularity.md` | Cross-skill modularity: one capability, sub-skills by name, composition plan, anti-monolith. |
|
|
13
13
|
| `description-field.md` | Frontmatter description as trigger catalog (capability stem + Triggers). No story prose. |
|
|
14
14
|
| `deterministic-elements.md` | Classify process steps; deterministic ones ship as code, task seeds, or fixed artifacts under CODE_RULES. |
|
|
15
|
-
| `delegation-map.md` | Subagent handoff patterns and transcript guidance for skill-builder → skill-writer. |
|
|
15
|
+
| `delegation-map.md` | Subagent handoff patterns and transcript guidance for skill-builder → skill-writer-agent. |
|
|
16
16
|
| `thariq-x-post-skills.json` | Source reference data from Anthropic lessons on building Claude Code skills. |
|
|
17
17
|
|
|
18
18
|
## Conventions
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Delegation Map
|
|
2
2
|
|
|
3
|
-
How skill-builder delegates
|
|
3
|
+
How skill-builder delegates authoring to the `skill-writer-agent` agent.
|
|
4
4
|
|
|
5
|
-
## Delegating to skill-writer (Step 4)
|
|
5
|
+
## Delegating to skill-writer-agent (Step 4)
|
|
6
6
|
|
|
7
|
-
skill-builder orchestrates; skill-writer authors the SKILL.md and companion files. The handoff must be structured so
|
|
7
|
+
skill-builder orchestrates; the `skill-writer-agent` agent authors the SKILL.md and companion files. The caller spawns it with `Agent(subagent_type="skill-writer-agent", ...)`. The handoff must be structured so the agent has everything it needs.
|
|
8
8
|
|
|
9
9
|
### New skill handoff
|
|
10
10
|
|
|
@@ -44,7 +44,7 @@ description: >-
|
|
|
44
44
|
|
|
45
45
|
```yaml
|
|
46
46
|
description: >-
|
|
47
|
-
Skill lifecycle: classify, scaffold, write via skill-writer, self-audit, compose sub-skills, polish description triggers. Triggers: build a skill, new skill workflow, improve this skill, optimize skill description, skill development lifecycle, skill modularity.
|
|
47
|
+
Skill lifecycle: classify, scaffold, write via the skill-writer-agent, self-audit, compose sub-skills, polish description triggers. Triggers: build a skill, new skill workflow, improve this skill, optimize skill description, skill development lifecycle, skill modularity.
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
```yaml
|
|
@@ -54,7 +54,7 @@ sibling-skill-a/ # one capability
|
|
|
54
54
|
sibling-skill-b/ # one capability
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
The orchestrator owns sequence and gates. Sub-skills own domain detail. skill-builder itself follows this shape: it orchestrates and delegates writing to
|
|
57
|
+
The orchestrator owns sequence and gates. Sub-skills own domain detail. skill-builder itself follows this shape: it orchestrates and delegates writing to the `skill-writer-agent` agent (spawned via the Agent tool).
|
|
58
58
|
|
|
59
59
|
### Within-skill vs split
|
|
60
60
|
|
|
@@ -85,12 +85,11 @@ In Process or a dedicated **Sub-skills** section:
|
|
|
85
85
|
| Skill | When | Produces |
|
|
86
86
|
|---|---|---|
|
|
87
87
|
| `/reviewer-gates` | Step 1 — gate external reviewers | opt-out, Copilot quota, Bugbot trigger decisions |
|
|
88
|
-
| `/skill-writer` | Step 4 — author SKILL.md | skill package files |
|
|
89
88
|
|
|
90
89
|
If a listed skill is not installed, respond: `[exact refusal or degrade line]`.
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
Self-audit requires this table (or equivalent) whenever the skill composes others.
|
|
92
|
+
Self-audit requires this table (or equivalent) whenever the skill composes others. skill-builder itself delegates SKILL.md authoring to the `skill-writer-agent` agent, spawned via the Agent tool.
|
|
94
93
|
|
|
95
94
|
## Anti-patterns
|
|
96
95
|
|
|
@@ -6,7 +6,7 @@ Step-by-step workflow files for each skill lifecycle phase, loaded on demand by
|
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `new-skill.md` | Full lifecycle: classify, scaffold, gather (composition + description triggers + deterministic inventory), write via skill-writer, self-audit, deliver. |
|
|
9
|
+
| `new-skill.md` | Full lifecycle: classify, scaffold, gather (composition + description triggers + deterministic inventory), write via the skill-writer-agent agent, self-audit, deliver. |
|
|
10
10
|
| `improve-skill.md` | Observation-first improve: diagnose activation/modularity/quality/deterministic prose, targeted fix, re-audit. |
|
|
11
11
|
| `polish-skill.md` | Description trigger-catalog audit; progressive disclosure + modularity + deterministic audit; task-seeded self-audit. |
|
|
12
12
|
|
|
@@ -83,7 +83,7 @@ For each diagnosis from Step 2:
|
|
|
83
83
|
6. For checkbox/tracker failures: replace markdown `- [ ]` boards with a plain task-seed list and a seed instruction (`TaskCreate` / `TodoWrite`).
|
|
84
84
|
7. Verify the fix doesn’t break anything that was working.
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Spawn the `skill-writer-agent` agent for larger rewrites, using the refine-skill handoff from delegation-map.md.
|
|
87
87
|
|
|
88
88
|
**Output:** Modified skill files with targeted fixes.
|
|
89
89
|
|
|
@@ -125,11 +125,11 @@ Register each task seed under **Required task seeds** in `deterministic-elements
|
|
|
125
125
|
|
|
126
126
|
**Goal:** Produce the skill package — SKILL.md and companion files.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
Spawn the `skill-writer-agent` agent (`Agent(subagent_type="skill-writer-agent", ...)`) with the structured handoff from `${CLAUDE_SKILL_DIR}/references/delegation-map.md`.
|
|
129
129
|
|
|
130
130
|
The handoff must include: skill type, folder structure, gap analysis (composition plan + description triggers + deterministic inventory), initial gotchas, degree of freedom, constraints, sub-skills to name in SKILL.md, exact description string to put in frontmatter, script/test paths for every deterministic step.
|
|
131
131
|
|
|
132
|
-
After skill-writer produces the draft:
|
|
132
|
+
After the `skill-writer-agent` agent produces the draft:
|
|
133
133
|
|
|
134
134
|
1. Verify it follows the hub layout (principle → gotchas → when-applies → process → file index → folder map).
|
|
135
135
|
2. Verify SKILL.md body is under 500 lines.
|
|
@@ -17,8 +17,8 @@ One warm, addressable advisor available at the strongest model tier the session
|
|
|
17
17
|
|
|
18
18
|
**Detect the host profile first** (Host profiles in
|
|
19
19
|
[`_shared/advisor/advisor-protocol.md`](../../_shared/advisor/advisor-protocol.md)
|
|
20
|
-
— e.g.
|
|
21
|
-
walk until the host is known.
|
|
20
|
+
— e.g. %USERPROFILE%\.claude\_shared\advisor\advisor-protocol.md). Do not start a model-floor
|
|
21
|
+
walk until the host is known.
|
|
22
22
|
|
|
23
23
|
This session is the shared advisor's sole consumer, so its model floor is
|
|
24
24
|
simply this session's own tier — no routing table to take a max against.
|