claude-dev-env 2.2.1 → 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/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +4 -4
- 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 +24 -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_code_verifier_tools_contract.py +28 -0
- 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 -810
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +118 -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/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- 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/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- 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/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- 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 +5 -4
|
@@ -31,7 +31,7 @@ def _load_revoke_module() -> ModuleType:
|
|
|
31
31
|
|
|
32
32
|
def test_module_imports_constants_from_config_modules() -> None:
|
|
33
33
|
revoke_module = _load_revoke_module()
|
|
34
|
-
assert revoke_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "
|
|
34
|
+
assert revoke_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Read")
|
|
35
35
|
assert revoke_module.AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX == (
|
|
36
36
|
"Trusted local workspace:"
|
|
37
37
|
)
|
|
@@ -49,3 +49,90 @@ def test_revoke_module_guards_sys_path_insert_against_duplicates() -> None:
|
|
|
49
49
|
"revoke_project_claude_permissions.py must guard sys.path.insert against "
|
|
50
50
|
"duplicate entries on reload (consistent with sibling modules)"
|
|
51
51
|
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _section_of(settings: dict[str, object], section_key: str) -> dict[str, object]:
|
|
55
|
+
section = settings[section_key]
|
|
56
|
+
assert isinstance(section, dict)
|
|
57
|
+
return section
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_remove_values_from_list_removes_matching_strings() -> None:
|
|
61
|
+
revoke_module = _load_revoke_module()
|
|
62
|
+
target_list: list[object] = ["keep", "drop", 7, "drop"]
|
|
63
|
+
removed_count = revoke_module.remove_values_from_list(target_list, {"drop"})
|
|
64
|
+
assert removed_count == 2
|
|
65
|
+
assert target_list == ["keep", 7]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_remove_rules_from_allow_list_removes_named_rules() -> None:
|
|
69
|
+
revoke_module = _load_revoke_module()
|
|
70
|
+
settings: dict[str, object] = {
|
|
71
|
+
"permissions": {
|
|
72
|
+
"allow": ["Edit(/proj/.claude/**)", "Read(/proj/.claude/**)"],
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
removed_count = revoke_module.remove_rules_from_allow_list(
|
|
76
|
+
settings, ["Edit(/proj/.claude/**)"]
|
|
77
|
+
)
|
|
78
|
+
assert removed_count == 1
|
|
79
|
+
assert _section_of(settings, "permissions")["allow"] == ["Read(/proj/.claude/**)"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_remove_rules_from_deny_list_removes_named_rules() -> None:
|
|
83
|
+
revoke_module = _load_revoke_module()
|
|
84
|
+
settings: dict[str, object] = {
|
|
85
|
+
"permissions": {
|
|
86
|
+
"deny": ["Edit(/proj/.claude/hooks/**)", "Read(/proj/.claude/hooks/**)"],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
removed_count = revoke_module.remove_rules_from_deny_list(
|
|
90
|
+
settings, ["Edit(/proj/.claude/hooks/**)"]
|
|
91
|
+
)
|
|
92
|
+
assert removed_count == 1
|
|
93
|
+
assert _section_of(settings, "permissions")["deny"] == [
|
|
94
|
+
"Read(/proj/.claude/hooks/**)"
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_remove_directory_from_additional_directories_removes_entry() -> None:
|
|
99
|
+
revoke_module = _load_revoke_module()
|
|
100
|
+
settings: dict[str, object] = {
|
|
101
|
+
"permissions": {
|
|
102
|
+
"additionalDirectories": ["/proj", "/other"],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
removed_count = revoke_module.remove_directory_from_additional_directories(
|
|
106
|
+
settings, "/proj"
|
|
107
|
+
)
|
|
108
|
+
assert removed_count == 1
|
|
109
|
+
assert _section_of(settings, "permissions")["additionalDirectories"] == ["/other"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_remove_trust_entries_for_project_removes_every_project_entry() -> None:
|
|
113
|
+
revoke_module = _load_revoke_module()
|
|
114
|
+
trust_prefix = revoke_module.AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX
|
|
115
|
+
project_entry_a = "Trusted local workspace: /proj/.claude/** wording A"
|
|
116
|
+
project_entry_b = "Trusted local workspace: /proj/.claude/** wording B"
|
|
117
|
+
other_project_entry = "Trusted local workspace: /other/.claude/** unrelated"
|
|
118
|
+
settings: dict[str, object] = {
|
|
119
|
+
"autoMode": {
|
|
120
|
+
"environment": [project_entry_a, project_entry_b, other_project_entry],
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
removed_count = revoke_module.remove_trust_entries_for_project(
|
|
124
|
+
settings, "/proj", trust_prefix
|
|
125
|
+
)
|
|
126
|
+
assert removed_count == 2
|
|
127
|
+
assert _section_of(settings, "autoMode")["environment"] == [other_project_entry]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_prune_settings_after_revoke_drops_empty_sections() -> None:
|
|
131
|
+
revoke_module = _load_revoke_module()
|
|
132
|
+
settings: dict[str, object] = {
|
|
133
|
+
"permissions": {"allow": [], "deny": []},
|
|
134
|
+
"autoMode": {"environment": []},
|
|
135
|
+
}
|
|
136
|
+
revoke_module.prune_settings_after_revoke(settings)
|
|
137
|
+
assert "permissions" not in settings
|
|
138
|
+
assert "autoMode" not in settings
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Behavioral tests for stale_worktree_rule_sweep.
|
|
2
|
+
|
|
3
|
+
Drives the real sweep functions against real temp directories and a real
|
|
4
|
+
temp settings file, and drives the real grant and revoke flows end to end.
|
|
5
|
+
A live worktree directory exists on disk; a dead worktree path does not. The
|
|
6
|
+
sweep keeps every rule whose worktree directory still exists and drops every
|
|
7
|
+
rule whose worktree directory is gone, then removes exact duplicates from
|
|
8
|
+
each rule array. The grant and revoke runs perform that sweep before they
|
|
9
|
+
save, so a run whose only effect is the sweep still persists the cleanup.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_scripts_module(module_name: str) -> ModuleType:
|
|
23
|
+
scripts_directory = Path(__file__).parent.parent
|
|
24
|
+
parent_directory = str(scripts_directory.resolve())
|
|
25
|
+
if parent_directory not in sys.path:
|
|
26
|
+
sys.path.insert(0, parent_directory)
|
|
27
|
+
module_path = scripts_directory / f"{module_name}.py"
|
|
28
|
+
specification = importlib.util.spec_from_file_location(module_name, module_path)
|
|
29
|
+
assert specification is not None
|
|
30
|
+
assert specification.loader is not None
|
|
31
|
+
module = importlib.util.module_from_spec(specification)
|
|
32
|
+
specification.loader.exec_module(module)
|
|
33
|
+
return module
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_sweep_module() -> ModuleType:
|
|
37
|
+
return _load_scripts_module("stale_worktree_rule_sweep")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
permissions_common = _load_scripts_module("_claude_permissions_common")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _make_live_worktree_directory(worktrees_root: Path) -> Path:
|
|
44
|
+
live_worktree_directory = worktrees_root / "repo" / "live-worktree"
|
|
45
|
+
live_worktree_directory.mkdir(parents=True)
|
|
46
|
+
return live_worktree_directory
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _allow_rule_for(worktree_directory: Path) -> str:
|
|
50
|
+
return f"Edit({worktree_directory}/.claude/**)"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _deny_rule_for(worktree_directory: Path) -> str:
|
|
54
|
+
return f"Read({worktree_directory}/.claude/settings*.json)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _permission_list(all_settings: dict[str, object], list_key: str) -> list[object]:
|
|
58
|
+
permissions_section = all_settings["permissions"]
|
|
59
|
+
assert isinstance(permissions_section, dict)
|
|
60
|
+
rule_list = permissions_section[list_key]
|
|
61
|
+
assert isinstance(rule_list, list)
|
|
62
|
+
return rule_list
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_sweep_keeps_live_worktree_rules_and_drops_dead_ones(tmp_path: Path) -> None:
|
|
66
|
+
sweep_module = _load_sweep_module()
|
|
67
|
+
live_worktree_directory = _make_live_worktree_directory(tmp_path)
|
|
68
|
+
dead_worktree_directory = tmp_path / "repo" / "dead-worktree"
|
|
69
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
70
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
71
|
+
live_deny_rule = _deny_rule_for(live_worktree_directory)
|
|
72
|
+
dead_deny_rule = _deny_rule_for(dead_worktree_directory)
|
|
73
|
+
all_settings: dict[str, object] = {
|
|
74
|
+
"permissions": {
|
|
75
|
+
"allow": [live_allow_rule, dead_allow_rule],
|
|
76
|
+
"deny": [live_deny_rule, dead_deny_rule],
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
80
|
+
all_settings, tmp_path
|
|
81
|
+
)
|
|
82
|
+
assert removed_count == 2
|
|
83
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
84
|
+
assert _permission_list(all_settings, "deny") == [live_deny_rule]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_sweep_keeps_live_flat_worktree_rules_and_drops_dead_ones(tmp_path: Path) -> None:
|
|
88
|
+
sweep_module = _load_sweep_module()
|
|
89
|
+
live_worktree_directory = tmp_path / "flat-live"
|
|
90
|
+
live_worktree_directory.mkdir(parents=True)
|
|
91
|
+
dead_worktree_directory = tmp_path / "flat-dead"
|
|
92
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
93
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
94
|
+
all_settings: dict[str, object] = {
|
|
95
|
+
"permissions": {
|
|
96
|
+
"allow": [live_allow_rule, dead_allow_rule],
|
|
97
|
+
"deny": [],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
101
|
+
all_settings, tmp_path
|
|
102
|
+
)
|
|
103
|
+
assert removed_count == 1
|
|
104
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_sweep_leaves_rules_for_projects_outside_the_worktrees_root(
|
|
108
|
+
tmp_path: Path,
|
|
109
|
+
) -> None:
|
|
110
|
+
sweep_module = _load_sweep_module()
|
|
111
|
+
unrelated_project_rule = "Edit(/home/developer/project/.claude/**)"
|
|
112
|
+
all_settings: dict[str, object] = {
|
|
113
|
+
"permissions": {"allow": [unrelated_project_rule]},
|
|
114
|
+
}
|
|
115
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
116
|
+
all_settings, tmp_path
|
|
117
|
+
)
|
|
118
|
+
assert removed_count == 0
|
|
119
|
+
assert _permission_list(all_settings, "allow") == [unrelated_project_rule]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_sweep_removes_exact_duplicate_rules(tmp_path: Path) -> None:
|
|
123
|
+
sweep_module = _load_sweep_module()
|
|
124
|
+
live_worktree_directory = _make_live_worktree_directory(tmp_path)
|
|
125
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
126
|
+
all_settings: dict[str, object] = {
|
|
127
|
+
"permissions": {"allow": [live_allow_rule, live_allow_rule]},
|
|
128
|
+
}
|
|
129
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
130
|
+
all_settings, tmp_path
|
|
131
|
+
)
|
|
132
|
+
assert removed_count == 1
|
|
133
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_sweep_ignores_malformed_and_non_string_entries(tmp_path: Path) -> None:
|
|
137
|
+
sweep_module = _load_sweep_module()
|
|
138
|
+
malformed_entry = "not a rule at all"
|
|
139
|
+
all_settings: dict[str, object] = {
|
|
140
|
+
"permissions": {"allow": [malformed_entry, 7, malformed_entry]},
|
|
141
|
+
}
|
|
142
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
143
|
+
all_settings, tmp_path
|
|
144
|
+
)
|
|
145
|
+
assert removed_count == 1
|
|
146
|
+
assert _permission_list(all_settings, "allow") == [malformed_entry, 7]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_sweep_tolerates_missing_permission_sections(tmp_path: Path) -> None:
|
|
150
|
+
sweep_module = _load_sweep_module()
|
|
151
|
+
all_settings: dict[str, object] = {}
|
|
152
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
153
|
+
all_settings, tmp_path
|
|
154
|
+
)
|
|
155
|
+
assert removed_count == 0
|
|
156
|
+
assert all_settings == {}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_sweep_round_trips_through_a_real_temp_settings_file(tmp_path: Path) -> None:
|
|
160
|
+
sweep_module = _load_sweep_module()
|
|
161
|
+
worktrees_root = tmp_path / "worktrees"
|
|
162
|
+
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
163
|
+
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
164
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
165
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
166
|
+
settings_path = tmp_path / "settings.json"
|
|
167
|
+
permissions_common.save_settings(
|
|
168
|
+
settings_path,
|
|
169
|
+
{"permissions": {"allow": [live_allow_rule, dead_allow_rule]}},
|
|
170
|
+
)
|
|
171
|
+
loaded_settings = permissions_common.load_settings(settings_path)
|
|
172
|
+
sweep_module.sweep_and_deduplicate_permission_lists(loaded_settings, worktrees_root)
|
|
173
|
+
permissions_common.save_settings(settings_path, loaded_settings)
|
|
174
|
+
reloaded_settings = permissions_common.load_settings(settings_path)
|
|
175
|
+
assert _permission_list(reloaded_settings, "allow") == [live_allow_rule]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_wrapper_resolves_worktrees_root_under_the_user_claude_home(
|
|
179
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
180
|
+
) -> None:
|
|
181
|
+
sweep_module = _load_sweep_module()
|
|
182
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
183
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
184
|
+
worktrees_root = tmp_path / ".claude" / "worktrees"
|
|
185
|
+
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
186
|
+
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
187
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
188
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
189
|
+
all_settings: dict[str, object] = {
|
|
190
|
+
"permissions": {"allow": [live_allow_rule, dead_allow_rule]},
|
|
191
|
+
}
|
|
192
|
+
removed_count = sweep_module.sweep_stale_worktree_rules_from_settings(all_settings)
|
|
193
|
+
assert removed_count == 1
|
|
194
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _prepare_home_with_worktrees(
|
|
198
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
199
|
+
) -> tuple[Path, Path, Path]:
|
|
200
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
201
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
202
|
+
worktrees_root = tmp_path / ".claude" / "worktrees"
|
|
203
|
+
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
204
|
+
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
205
|
+
settings_path = tmp_path / ".claude" / "settings.json"
|
|
206
|
+
return settings_path, live_worktree_directory, dead_worktree_directory
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _make_fake_project(tmp_path: Path) -> Path:
|
|
210
|
+
fake_project_root = tmp_path / "fake_project"
|
|
211
|
+
(fake_project_root / ".claude").mkdir(parents=True)
|
|
212
|
+
return fake_project_root
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _seed_allow_rules(settings_path: Path, all_rules: list[object]) -> None:
|
|
216
|
+
permissions_common.save_settings(
|
|
217
|
+
settings_path, {"permissions": {"allow": all_rules}}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _saved_allow_list(settings_path: Path) -> list[object]:
|
|
222
|
+
saved_settings = permissions_common.load_settings(settings_path)
|
|
223
|
+
permissions_section = saved_settings.get("permissions")
|
|
224
|
+
if not isinstance(permissions_section, dict):
|
|
225
|
+
return []
|
|
226
|
+
allow_list = permissions_section.get("allow")
|
|
227
|
+
return allow_list if isinstance(allow_list, list) else []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_real_grant_run_sweeps_a_dead_worktree_rule_from_saved_settings(
|
|
231
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
232
|
+
) -> None:
|
|
233
|
+
settings_path, live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
234
|
+
tmp_path, monkeypatch
|
|
235
|
+
)
|
|
236
|
+
live_rule = _allow_rule_for(live_directory)
|
|
237
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
238
|
+
unrelated_rule = "Edit(/home/developer/project/.claude/**)"
|
|
239
|
+
_seed_allow_rules(settings_path, [live_rule, dead_rule, unrelated_rule])
|
|
240
|
+
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
241
|
+
grant_module = _load_scripts_module("grant_project_claude_permissions")
|
|
242
|
+
grant_module.grant_permissions_for_current_directory()
|
|
243
|
+
saved_allow = _saved_allow_list(settings_path)
|
|
244
|
+
assert dead_rule not in saved_allow
|
|
245
|
+
assert live_rule in saved_allow
|
|
246
|
+
assert unrelated_rule in saved_allow
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_real_grant_run_with_only_a_sweep_change_still_saves(
|
|
250
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
251
|
+
) -> None:
|
|
252
|
+
settings_path, _live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
253
|
+
tmp_path, monkeypatch
|
|
254
|
+
)
|
|
255
|
+
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
256
|
+
grant_module = _load_scripts_module("grant_project_claude_permissions")
|
|
257
|
+
grant_module.grant_permissions_for_current_directory()
|
|
258
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
259
|
+
granted_settings = permissions_common.load_settings(settings_path)
|
|
260
|
+
_permission_list(granted_settings, "allow").append(dead_rule)
|
|
261
|
+
permissions_common.save_settings(settings_path, granted_settings)
|
|
262
|
+
grant_module.grant_permissions_for_current_directory()
|
|
263
|
+
assert dead_rule not in _saved_allow_list(settings_path)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_real_revoke_run_sweeps_a_dead_worktree_rule_from_saved_settings(
|
|
267
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
268
|
+
) -> None:
|
|
269
|
+
settings_path, live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
270
|
+
tmp_path, monkeypatch
|
|
271
|
+
)
|
|
272
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
273
|
+
monkeypatch.chdir(fake_project_root)
|
|
274
|
+
revoke_module = _load_scripts_module("revoke_project_claude_permissions")
|
|
275
|
+
project_path = str(fake_project_root).replace("\\", "/")
|
|
276
|
+
project_allow_rules = revoke_module.build_permission_rules(
|
|
277
|
+
project_path, revoke_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
278
|
+
)
|
|
279
|
+
live_rule = _allow_rule_for(live_directory)
|
|
280
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
281
|
+
_seed_allow_rules(settings_path, [*project_allow_rules, live_rule, dead_rule])
|
|
282
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
283
|
+
saved_allow = _saved_allow_list(settings_path)
|
|
284
|
+
assert dead_rule not in saved_allow
|
|
285
|
+
assert live_rule in saved_allow
|
|
286
|
+
for each_project_rule in project_allow_rules:
|
|
287
|
+
assert each_project_rule not in saved_allow
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_real_revoke_run_with_only_a_sweep_change_still_saves(
|
|
291
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
292
|
+
) -> None:
|
|
293
|
+
settings_path, _live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
294
|
+
tmp_path, monkeypatch
|
|
295
|
+
)
|
|
296
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
297
|
+
_seed_allow_rules(settings_path, [dead_rule])
|
|
298
|
+
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
299
|
+
revoke_module = _load_scripts_module("revoke_project_claude_permissions")
|
|
300
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
301
|
+
assert dead_rule not in _saved_allow_list(settings_path)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Behavioral tests for stale_worktree_rule_sweep_constants.
|
|
2
|
+
|
|
3
|
+
Confirms get_claude_worktrees_root resolves the worktrees directory under
|
|
4
|
+
the user's ~/.claude home, and that the rule-parsing constants carry the
|
|
5
|
+
literal shapes the sweep relies on.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_constants_module() -> ModuleType:
|
|
19
|
+
scripts_directory = Path(__file__).parent.parent
|
|
20
|
+
parent_directory = str(scripts_directory.resolve())
|
|
21
|
+
if parent_directory not in sys.path:
|
|
22
|
+
sys.path.insert(0, parent_directory)
|
|
23
|
+
module_path = (
|
|
24
|
+
scripts_directory
|
|
25
|
+
/ "pr_loop_shared_constants"
|
|
26
|
+
/ "stale_worktree_rule_sweep_constants.py"
|
|
27
|
+
)
|
|
28
|
+
specification = importlib.util.spec_from_file_location(
|
|
29
|
+
"pr_loop_shared_constants.stale_worktree_rule_sweep_constants", module_path
|
|
30
|
+
)
|
|
31
|
+
assert specification is not None
|
|
32
|
+
assert specification.loader is not None
|
|
33
|
+
module = importlib.util.module_from_spec(specification)
|
|
34
|
+
specification.loader.exec_module(module)
|
|
35
|
+
return module
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_worktrees_root_resolves_under_the_user_claude_home(
|
|
39
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
40
|
+
) -> None:
|
|
41
|
+
constants_module = _load_constants_module()
|
|
42
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
43
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
44
|
+
resolved_worktrees_root = constants_module.get_claude_worktrees_root()
|
|
45
|
+
assert resolved_worktrees_root == tmp_path / ".claude" / "worktrees"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_extract_rule_target_path_reads_the_delimited_path() -> None:
|
|
49
|
+
constants_module = _load_constants_module()
|
|
50
|
+
extracted_path = constants_module.extract_rule_target_path(
|
|
51
|
+
"Edit(/repo/wt/.claude/**)"
|
|
52
|
+
)
|
|
53
|
+
assert extracted_path == "/repo/wt/.claude/**"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_extract_rule_target_path_returns_none_for_a_non_rule() -> None:
|
|
57
|
+
constants_module = _load_constants_module()
|
|
58
|
+
assert constants_module.extract_rule_target_path("not a rule") is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_worktree_directory_for_rule_keeps_nested_segments_below_the_root() -> None:
|
|
62
|
+
constants_module = _load_constants_module()
|
|
63
|
+
worktrees_root = Path("/home/dev/.claude/worktrees")
|
|
64
|
+
resolved_directory = constants_module.worktree_directory_for_rule(
|
|
65
|
+
"Edit(/home/dev/.claude/worktrees/repo/feature/.claude/**)", worktrees_root
|
|
66
|
+
)
|
|
67
|
+
assert resolved_directory == worktrees_root / "repo" / "feature"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_worktree_directory_for_rule_keeps_one_segment_flat_layout() -> None:
|
|
71
|
+
constants_module = _load_constants_module()
|
|
72
|
+
worktrees_root = Path("/home/dev/.claude/worktrees")
|
|
73
|
+
resolved_directory = constants_module.worktree_directory_for_rule(
|
|
74
|
+
"Edit(/home/dev/.claude/worktrees/flat-worktree/.claude/**)", worktrees_root
|
|
75
|
+
)
|
|
76
|
+
assert resolved_directory == worktrees_root / "flat-worktree"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_worktree_directory_for_rule_returns_none_outside_the_root() -> None:
|
|
80
|
+
constants_module = _load_constants_module()
|
|
81
|
+
worktrees_root = Path("/home/dev/.claude/worktrees")
|
|
82
|
+
resolved_directory = constants_module.worktree_directory_for_rule(
|
|
83
|
+
"Edit(/home/dev/project/.claude/**)", worktrees_root
|
|
84
|
+
)
|
|
85
|
+
assert resolved_directory is None
|
|
@@ -146,7 +146,7 @@ Mode decision:
|
|
|
146
146
|
| `ThirdParty` | any | `chain` | Headless spawn through `claude_chain_runner` |
|
|
147
147
|
|
|
148
148
|
The review always runs at effort **high** on model **opus**. Chain mode builds
|
|
149
|
-
a single-turn prompt `/code-review
|
|
149
|
+
a single-turn prompt `/code-review xhigh --fix`, pins `--model opus`, sets
|
|
150
150
|
subprocess `cwd` to the PR working tree, and reads stdin from the empty stream
|
|
151
151
|
so the spawn does not wait for input.
|
|
152
152
|
|
package/agents/CLAUDE.md
CHANGED
|
@@ -18,6 +18,7 @@ Agent definition files installed into `~/.claude/agents/` by `bin/install.mjs`.
|
|
|
18
18
|
| `plan-packet-validator.md` | Plan Packet Validator | Fresh-context validator for workflow-generated plan packets under `docs/plans/` |
|
|
19
19
|
| `pr-description-writer.md` | PR Description Writer | Authors PR descriptions in Anthropic-style shapes that pass the `pr_description_enforcer` hook's body audit |
|
|
20
20
|
| `session-advisor.md` | Session Advisor | Standing multi-consumer reviewer; SendMessage only; returns endorse/correction/plan/stop |
|
|
21
|
+
| `skill-writer-agent.md` | Skill Writer Agent | Authors SKILL.md and companion files to skill-builder conventions; caller-agnostic authoring specialist |
|
|
21
22
|
|
|
22
23
|
## Format
|
|
23
24
|
|
package/agents/code-verifier.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-verifier
|
|
3
|
-
description: Post-hoc verification agent for the
|
|
3
|
+
description: Post-hoc verification agent for the three-phase code workflow. Spawned by the main session after coder agents finish. Runs every check itself in a fresh context — named gates, tests against recorded baselines, two-way diff-vs-task reading — puts the draft verdict through one strongest-tier validation subagent that tries to refute it, then ends with a fenced verdict block the verifier_verdict_minter hook turns into the commit-gate verdict. Read and execute only; it never edits files.
|
|
4
4
|
tools: Read, Grep, Glob, Bash, Task
|
|
5
5
|
model: sonnet
|
|
6
6
|
color: orange
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
You are the verifier in a
|
|
9
|
+
You are the verifier in a three-phase code workflow: coder agents wrote changes, and you grade the result on its own terms (Claude Code best practices, fresh-context review: https://code.claude.com/docs/en/best-practices). The agent doing the work is never the one grading it — that is you, so you trust nothing you did not run or read yourself this session.
|
|
10
10
|
|
|
11
11
|
The caller gives you task texts, the diff scope, and baselines recorded before the coders ran. Treat every claim in the caller's message — and any coder summary quoted in it — as a hypothesis to test, never as a fact.
|
|
12
12
|
|
|
@@ -33,7 +33,7 @@ On Windows the same file sits at %USERPROFILE%\.claude\hooks\blocking\verificati
|
|
|
33
33
|
|
|
34
34
|
The printed hash commits to every changed and untracked file's content in the verified work tree, so it names that surface no matter which directory you or the committer run from. If the CLI prints an empty-surface or wrong-work-tree error and no hash, you are pointed at a work tree with no changes versus origin/main — re-run with the branch mode to locate the correct work tree.
|
|
35
35
|
|
|
36
|
-
As the last step before the verdict, put your draft verdict through one best-effort strongest-tier validation pass. Spawn a single validation subagent through the Task tool as the `Explore` agent type at the strongest reachable tier: set the Task `subagent_type` to `Explore`, detect the host profile first per `~/.claude/_shared/advisor/advisor-protocol.md` — the source of truth for host detection, the ladder, and its aliases — then on a Claude host pick the strongest reachable tier on the Fable → Opus → Sonnet → Haiku ladder and on a third-party host use the single third-party tier, and set the Task `model:` field to that tier's alias. The `Explore` type carries no Edit or Write tools and cannot spawn further agents, so the harness itself holds the validator to the no-edit, no-spawn contract the next paragraph names. Hand it the draft verdict together with your evidence — every command you ran with its output, and your two-way diff-to-task mapping — and state that its task is adversarial verification of that supplied draft verdict: refute it against the supplied evidence rather than discover code, naming any gate you misread, any task item you mapped wrong, or any finding that does not hold. When the spawn is unavailable — a Task tool error, an unreachable tier at every rung, or this subagent being barred from spawning further agents — skip the validation pass and emit the draft verdict as it stands, noting the skip in your final message; a spawn failure never blocks the verdict fence from being emitted.
|
|
36
|
+
As the last step before the verdict, put your draft verdict through one best-effort strongest-tier validation pass. Spawn a single validation subagent through the Task tool as the `Explore` agent type at the strongest reachable tier: set the Task `subagent_type` to `Explore`, detect the host profile first per `~/.claude/_shared/advisor/advisor-protocol.md` — the source of truth for host detection, the ladder, and its aliases — then on a Claude host pick the strongest reachable tier on the Fable → Opus → Sonnet → Haiku ladder and on a third-party host use the single third-party tier, and set the Task `model:` field to that tier's alias. The `Explore` type carries no Edit or Write tools and cannot spawn further agents, so the harness itself holds the validator to the no-edit, no-spawn contract the next paragraph names. Hand it the draft verdict together with your evidence — every command you ran with its output, and your two-way diff-to-task mapping — and state that its task is adversarial verification of that supplied draft verdict: refute it against the supplied evidence rather than discover code, naming any gate you misread, any task item you mapped wrong, or any finding that does not hold. This pass is always a cold `Explore` spawn, not a message to the session's warm advisor: the refutation needs a grader with no accumulated session context or prior positions, and the verifier runs in sessions that have no advisor bound. When the spawn is unavailable — a Task tool error, an unreachable tier at every rung, or this subagent being barred from spawning further agents — skip the validation pass and emit the draft verdict as it stands, noting the skip in your final message; a spawn failure never blocks the verdict fence from being emitted.
|
|
37
37
|
|
|
38
38
|
This validation pass is terminal: the `Explore` type gives the validation subagent no way to spawn a further agent or edit a file, so it answers with prose only. When it refutes any part, re-check that part yourself against the commands and the diff, and correct the verdict before you emit it. When it refutes nothing, the draft verdict stands. Then emit the fenced verdict block below — it stays the last thing in your message so the verifier_verdict_minter hook reads it.
|
|
39
39
|
|
|
@@ -43,4 +43,4 @@ End your final message with exactly one fenced verdict block — the verifier_ve
|
|
|
43
43
|
{"all_pass": false, "findings": [{"check": "<gate or task item>", "detail": "<command + output, or the named task item and what is missing>"}], "manifest_sha256": "<hash the CLI printed>"}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Set `all_pass` to true with an empty `findings` list only when every layer came back clean. Always include `manifest_sha256` so the verdict clears the commit regardless of which work tree the verifier or the committer ran in.
|
|
46
|
+
Set `all_pass` to true with an empty `findings` list only when every layer came back clean. Always include `manifest_sha256` so the verdict clears the commit regardless of which work tree the verifier or the committer ran in. Commit-committability gates (CODE_RULES / merge conflicts) must already be green before you are spawned; you are the last semantic check before commit. Any file change after you finish moves that hash and invalidates the verdict.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-writer-agent
|
|
3
|
+
description: >-
|
|
4
|
+
Authors a skill package — SKILL.md plus companion files — to skill-builder conventions: hub layout, trigger-catalog description, deterministic work shipped as scripts and task-seed lists. Triggers: author a skill, write a SKILL.md, skill-writer-agent, delegate skill file authoring, produce skill package files.
|
|
5
|
+
tools: Read, Write, Edit, Grep, Glob
|
|
6
|
+
color: green
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill Writer Agent
|
|
10
|
+
|
|
11
|
+
You author skill files. A caller hands you a skill to build; you read the live skill-building references, write the SKILL.md and its companion files to current conventions, work a before-return pass, and reply with a file manifest. skill-builder is the common caller and delegates authoring to you; a direct request reaches you the same way and you serve it yourself.
|
|
12
|
+
|
|
13
|
+
## Input
|
|
14
|
+
|
|
15
|
+
Callers reach you in one of two shapes.
|
|
16
|
+
|
|
17
|
+
**Structured packet** (skill-builder sends this). A new-skill packet carries these fields, in order:
|
|
18
|
+
|
|
19
|
+
- `Skill type` — one of the skill-builder skill types.
|
|
20
|
+
- `Folder structure` — the directories to create (`reference/`, `scripts/`, `templates/`, `workflow/`).
|
|
21
|
+
- `What it does` — one sentence naming the single job.
|
|
22
|
+
- `Domain context` — what the authored skill needs Claude to know.
|
|
23
|
+
- `Initial gotchas` — failure patterns to document from the start.
|
|
24
|
+
- `Degree of freedom` — high, medium, or low, with reasoning.
|
|
25
|
+
- `Constraints` — the non-negotiables.
|
|
26
|
+
- `Composition plan` — related skills, sub-skills with when/produces/missing, leaf-versus-orchestrator call.
|
|
27
|
+
- `Description` — the exact frontmatter string, written as a trigger catalog.
|
|
28
|
+
- `Deterministic inventory` — each process step mapped to its class, its home path, and its paired test.
|
|
29
|
+
|
|
30
|
+
A caller may bundle three of these fields — `Composition plan`, `Description`, and `Deterministic inventory` — under one `gap analysis` label; treat that label as those three fields together, and read them from the packet either way.
|
|
31
|
+
|
|
32
|
+
A refine packet instead carries: the current SKILL.md, what was observed, what to change, new gotchas to add, what to preserve, a description rewrite, a composition change, and a deterministic fix.
|
|
33
|
+
|
|
34
|
+
**Direct request.** A looser prompt names the skill to build or the file to edit. Fill the packet yourself from the references below, and ask the caller for any field only they hold, such as domain context or constraints.
|
|
35
|
+
|
|
36
|
+
## Read first
|
|
37
|
+
|
|
38
|
+
At the start of every job, read the live references at `~/.claude/skills/skill-builder/references/` so you author to current conventions:
|
|
39
|
+
|
|
40
|
+
- `delegation-map.md` — the handoff packet and the Produce contract.
|
|
41
|
+
- `deterministic-elements.md` — the rule that deterministic work ships as code, artifacts, or session tasks.
|
|
42
|
+
- `description-field.md` — the trigger-catalog description shape.
|
|
43
|
+
- `skill-modularity.md` — hub layout and compose-by-name.
|
|
44
|
+
|
|
45
|
+
Read them fresh each run; they carry the rules you author to.
|
|
46
|
+
|
|
47
|
+
## Standards every file meets
|
|
48
|
+
|
|
49
|
+
1. SKILL.md follows the hub layout: principle, gotchas, when-applies, process, file index, folder map.
|
|
50
|
+
2. The frontmatter description is the exact trigger-catalog string — a `capability stem` plus a `Triggers:` list.
|
|
51
|
+
3. A sub-skills table appears whenever the composition plan lists peer skills.
|
|
52
|
+
4. Companion files carry the detail: reference docs, workflow steps, templates, scripts.
|
|
53
|
+
5. Every deterministic-inventory row gets a script or workflow; SKILL.md points to it and handles its exit codes.
|
|
54
|
+
6. Skill scripts follow CODE_RULES: a `*_constants/` package, full type hints, specific errors, and a paired test.
|
|
55
|
+
7. Every file stays under 500 lines, with a table of contents on any file over 100 lines.
|
|
56
|
+
8. A file index lists every file and its purpose.
|
|
57
|
+
9. Steps owned by a named sub-skill stay in that sub-skill; SKILL.md invokes it by name.
|
|
58
|
+
|
|
59
|
+
## Deterministic elements
|
|
60
|
+
|
|
61
|
+
Honor `deterministic-elements.md` in every skill you author. A step is deterministic when the same inputs give the same outputs, success is machine-checkable, and a human could write it as a pure function or a fixed sequence. Route each kind to its home:
|
|
62
|
+
|
|
63
|
+
- Validators, transforms, detection, and mechanical sequences ship as `scripts/` or `workflow/*.mjs`, each with tests and a `*_constants/` package.
|
|
64
|
+
- Verbatim templates ship in `templates/`.
|
|
65
|
+
- Long fixed catalogs Claude reads word-for-word ship in `reference/`.
|
|
66
|
+
- Ordered work the future skill-runner must finish ships as a task-seed list: plain bullets or numbered lines in a reference file, plus an instruction in SKILL.md to register one session task per item through the host task tool (`TaskCreate`, else `TodoWrite`). Judgment, routing, and refusal stay in markdown prose.
|
|
67
|
+
|
|
68
|
+
Keep required step lists on the task list. Where a host task tool is present, the authored SKILL.md registers each item through it; where the host offers only prose, the SKILL.md states that limit and stops.
|
|
69
|
+
|
|
70
|
+
## Before you return
|
|
71
|
+
|
|
72
|
+
Work your before-return checks as one pass over the file set:
|
|
73
|
+
|
|
74
|
+
- The SKILL.md hub layout is present and complete.
|
|
75
|
+
- The description reads as a trigger catalog.
|
|
76
|
+
- Every deterministic row carries its script and paired test.
|
|
77
|
+
- Files stay under the line caps, with a table of contents wherever the cap calls for one.
|
|
78
|
+
- The file index is present, and the sub-skills table is present whenever the skill composes others.
|
|
79
|
+
|
|
80
|
+
On a direct call, this pass is your whole quality gate — work it in full. When skill-builder is the caller, it runs its 38-point audit next; hand it a coherent, complete file set so that audit starts clean.
|
|
81
|
+
|
|
82
|
+
## Return
|
|
83
|
+
|
|
84
|
+
Reply with a file manifest kept thin: each path you created or edited on its own line, then one line naming what the caller picks up next, such as the audit or the tests to run. Keep the reply light so the caller acts on it directly.
|