claude-dev-env 2.30.0 → 2.31.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/pr-description-writer.md +1 -1
- package/.agents/skills/e-simplify/SKILL.md +20 -8
- package/AGENTS.md +6 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +37 -0
- package/bin/install.prune.test.mjs +4 -2
- package/docs/CODE_RULES.md +11 -0
- package/hooks/advisory/migration_safety_advisor.py +25 -1
- package/hooks/advisory/refactor_guard.py +85 -18
- package/hooks/advisory/test_migration_safety_advisor.py +89 -5
- package/hooks/advisory/test_refactor_guard.py +87 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +72 -1
- package/hooks/blocking/code_rules_constants_config.py +7 -6
- package/hooks/blocking/code_rules_enforcer.py +59 -36
- package/hooks/blocking/codex_apply_patch.py +136 -21
- package/hooks/blocking/config/codex_apply_patch_constants.py +13 -0
- package/hooks/blocking/conftest.py +70 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +26 -16
- package/hooks/blocking/hook_prose_detector_consistency.py +30 -20
- package/hooks/blocking/pii_payload_scan.py +61 -0
- package/hooks/blocking/pii_prevention_blocker.py +5 -0
- package/hooks/blocking/pr_description_writer_gate.py +263 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +1 -1
- package/hooks/blocking/sensitive_file_protector.py +18 -3
- package/hooks/blocking/shell_substitution_blocker.py +62 -8
- package/hooks/blocking/stale_comment_reference_blocker.py +94 -24
- package/hooks/blocking/state_description_blocker.py +39 -26
- package/hooks/blocking/subprocess_budget_completeness.py +17 -43
- package/hooks/blocking/tdd_enforcer.py +57 -14
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +11 -1
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +180 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +6 -2
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +18 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +66 -1
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +143 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +12 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +63 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +81 -2
- package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +35 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +29 -16
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_multi_edit.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +29 -28
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_stage.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -0
- package/hooks/blocking/test_code_rules_validation_phase.py +51 -0
- package/hooks/blocking/test_codex_apply_patch.py +43 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +68 -0
- package/hooks/blocking/test_hook_prose_detector_consistency.py +41 -0
- package/hooks/blocking/test_pii_payload_scan.py +65 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +80 -10
- package/hooks/blocking/test_pr_description_writer_gate.py +216 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +215 -25
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -4
- package/hooks/blocking/test_sensitive_file_protector.py +42 -2
- package/hooks/blocking/test_shell_substitution_blocker.py +69 -0
- package/hooks/blocking/test_stale_comment_reference_blocker.py +71 -0
- package/hooks/blocking/test_state_description_blocker.py +68 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +67 -0
- package/hooks/blocking/test_tdd_enforcer.py +182 -2
- package/hooks/blocking/test_windows_rmtree_blocker.py +63 -0
- package/hooks/blocking/test_write_existing_file_blocker.py +122 -0
- package/hooks/blocking/windows_rmtree_blocker.py +21 -7
- package/hooks/blocking/write_existing_file_blocker.py +75 -29
- package/hooks/conftest.py +47 -0
- package/hooks/hooks.json +8 -13
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +4 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +3 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +7 -2
- package/hooks/hooks_constants/migration_safety_advisor_constants.py +7 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +44 -3
- package/hooks/hooks_constants/pii_prevention_constants.py +9 -7
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +39 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +31 -14
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +15 -1
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +4 -1
- package/hooks/hooks_constants/state_description_blocker_constants.py +1 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +71 -0
- package/hooks/hooks_constants/task_tool_prompt_constants.py +13 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +2 -9
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +102 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +139 -25
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +70 -0
- package/hooks/hooks_constants/validation_phase_constants.py +2 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +7 -3
- package/hooks/observability/pr_description_writer_spawn_tracker.py +121 -0
- package/hooks/observability/session_file_edit_tracker.py +25 -4
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +146 -0
- package/hooks/observability/test_session_file_edit_tracker.py +32 -0
- package/hooks/session/task_tool_prompt.py +37 -0
- package/hooks/session/test_task_tool_prompt.py +65 -0
- package/hooks/validation/hook_format_validator.py +23 -9
- package/hooks/validation/test_hook_format_validator.py +58 -0
- package/hooks/validators/README.md +1 -1
- package/hooks/validators/abbreviation_checks.py +3 -5
- package/hooks/validators/config/abbreviation_checks_constants.py +3 -0
- package/hooks/validators/config/fast_save_validator_constants.py +32 -0
- package/hooks/validators/config/hook_timing_harness_constants.py +3 -1
- package/hooks/validators/config/test_abbreviation_checks_constants.py +11 -0
- package/hooks/validators/fast_save_validators.py +332 -0
- package/hooks/validators/hook_timing_harness.py +8 -4
- package/hooks/validators/python_style_checks.py +34 -60
- package/hooks/validators/python_style_helpers.py +16 -2
- package/hooks/validators/python_style_import_bootstrap.py +83 -0
- package/hooks/validators/run_all_validators.py +42 -41
- package/hooks/validators/test_abbreviation_checks.py +20 -0
- package/hooks/validators/test_fast_save_validators.py +172 -0
- package/hooks/validators/test_python_style_checks.py +9 -0
- package/hooks/validators/test_python_style_checks_import_order.py +81 -0
- package/hooks/validators/test_python_style_helpers.py +35 -0
- package/hooks/validators/test_python_style_import_bootstrap.py +69 -0
- package/hooks/validators/test_run_all_validators.py +46 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +2 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/git-workflow.md +2 -0
- package/rules/testing.md +4 -0
- package/scripts/codex_compat_materializer.py +5 -0
- package/scripts/tests/test_codex_compat_materializer.py +37 -11
- package/hooks/blocking/tests/conftest.py +0 -10
- package/hooks/hooks_constants/task_list_loop_starter_constants.py +0 -19
- package/hooks/session/task_list_loop_starter.py +0 -45
- package/hooks/session/test_task_list_loop_starter.py +0 -63
|
@@ -31,6 +31,10 @@ _ASSERT_FALSE_SOURCE = (
|
|
|
31
31
|
" assert False, observed_total\n"
|
|
32
32
|
)
|
|
33
33
|
PRINT_PRODUCTION_CONTENT = 'def run() -> None:\n print("x")\n'
|
|
34
|
+
CLEAN_PROBE_SOURCE = (
|
|
35
|
+
"def add_two_numbers(first_number: int, second_number: int) -> int:\n"
|
|
36
|
+
" return first_number + second_number\n"
|
|
37
|
+
)
|
|
34
38
|
ALL_PYTHON_TEST_EXEMPT_PATHS = [
|
|
35
39
|
"pkg/test_helpers/worker.py",
|
|
36
40
|
"pkg/conftest.py",
|
|
@@ -66,6 +70,48 @@ def test_validate_proposed_file_threads_ruff_config_from_original_path() -> None
|
|
|
66
70
|
assert "B011" in ruff_output
|
|
67
71
|
|
|
68
72
|
|
|
73
|
+
def test_validate_proposed_file_never_probes_mypy_availability() -> None:
|
|
74
|
+
"""The save-path gate drops Mypy entirely; it must not even probe for it.
|
|
75
|
+
|
|
76
|
+
Mypy runs 500-1800ms cold, the largest single item on the save path by a
|
|
77
|
+
wide margin (see hooks/validators/hook_timing_harness.py). CLI/full mode
|
|
78
|
+
keeps probing it; only the save-path gate skips the probe.
|
|
79
|
+
"""
|
|
80
|
+
with patch("validators.run_all_validators.check_mypy_available") as mock_check_mypy:
|
|
81
|
+
validate_proposed_file(
|
|
82
|
+
str(_VALIDATORS_DIRECTORY / "probe_target.py"), CLEAN_PROBE_SOURCE
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
mock_check_mypy.assert_not_called()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_validate_proposed_file_never_spawns_a_validator_subprocess() -> None:
|
|
89
|
+
"""The save-path gate must not spawn a ``python -m validators.<module>`` per check.
|
|
90
|
+
|
|
91
|
+
Twelve validator subprocesses cost roughly 600ms in interpreter startup
|
|
92
|
+
and import alone (see hook_timing_harness.py), for checks that measure in
|
|
93
|
+
single digits once running. CLI/full mode keeps spawning one process per
|
|
94
|
+
validator; only the save-path gate runs them in-process instead.
|
|
95
|
+
"""
|
|
96
|
+
with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
|
|
97
|
+
validate_proposed_file(
|
|
98
|
+
str(_VALIDATORS_DIRECTORY / "probe_target.py"), CLEAN_PROBE_SOURCE
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
mock_invoke.assert_not_called()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_validate_proposed_file_still_finds_a_fast_path_violation() -> None:
|
|
105
|
+
"""Dropping mypy and the per-validator subprocess must not blind the gate."""
|
|
106
|
+
all_results = validate_proposed_file(
|
|
107
|
+
str(_VALIDATORS_DIRECTORY / "probe_target.py"), PRINT_PRODUCTION_CONTENT
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
failed_names = {each_result.name for each_result in all_results if not each_result.passed}
|
|
111
|
+
assert failed_names, "expected at least one failing fast-path check"
|
|
112
|
+
assert "Mypy" not in failed_names
|
|
113
|
+
|
|
114
|
+
|
|
69
115
|
def _validators_deny_print(file_path: str) -> bool:
|
|
70
116
|
all_results = validate_proposed_file(file_path, PRINT_PRODUCTION_CONTENT)
|
|
71
117
|
return any(not each_result.passed for each_result in all_results)
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ Many deterministic shapes of this drift have Write/Edit gates in `~/.claude/hook
|
|
|
34
34
|
|
|
35
35
|
A hook module is the sharpest case of the same rule: its docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags, and claim no broader trigger surface than the regex enforces.
|
|
36
36
|
|
|
37
|
-
`hook_prose_detector_consistency` (PreToolUse on Write|Edit of hook modules and their `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
|
|
37
|
+
`hook_prose_detector_consistency` (PreToolUse on Write|Edit|MultiEdit of hook modules and their `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
|
|
38
38
|
|
|
39
39
|
After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
|
|
40
40
|
|
package/rules/git-workflow.md
CHANGED
|
@@ -17,6 +17,8 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh` (no ex
|
|
|
17
17
|
|
|
18
18
|
**ALWAYS create PRs as DRAFT:** Use `gh pr create --draft` for ALL PRs
|
|
19
19
|
|
|
20
|
+
**The pr-description-writer agent writes the body:** Spawn the `pr-description-writer` agent (Agent tool, `subagent_type` `pr-description-writer`), then publish what it returns with `--body-file`. `pr_description_writer_gate.py` (PreToolUse on Bash and PowerShell, hosted by `bash_pre_tool_use_dispatcher`) denies a `gh pr create` when the session records no such spawn. A trailing `# pr-description-skip` comment on the command opts out for a body you write yourself.
|
|
21
|
+
|
|
20
22
|
## Git Golden Rules (NON-NEGOTIABLE)
|
|
21
23
|
|
|
22
24
|
1. **DRAFT BEFORE PUSH**: When pushing ANYTHING to a PR, it MUST be in draft state first
|
package/rules/testing.md
CHANGED
|
@@ -22,3 +22,7 @@ Incomplete mocks make it impossible to distinguish "broken code" from "missing d
|
|
|
22
22
|
## Tests Exercise Real Behavior
|
|
23
23
|
|
|
24
24
|
Tests exercise real behavior, real data, and production code paths. A test that asserts on a stand-in for the production path proves the stand-in works.
|
|
25
|
+
|
|
26
|
+
## The File-Level TDD Gate Reads Content
|
|
27
|
+
|
|
28
|
+
`tdd_enforcer.py` records each candidate test's content hash at every sighting and compares that hash on the next write. A first sighting requires content that differs from HEAD.
|
|
@@ -64,6 +64,8 @@ codex_hook_dependency_manifest = (
|
|
|
64
64
|
"hooks/blocking/code_rules_comments.py",
|
|
65
65
|
"hooks/blocking/code_rules_constants_config.py",
|
|
66
66
|
"hooks/blocking/codex_apply_patch.py",
|
|
67
|
+
"hooks/blocking/config/__init__.py",
|
|
68
|
+
"hooks/blocking/config/codex_apply_patch_constants.py",
|
|
67
69
|
"hooks/blocking/code_rules_docstrings.py",
|
|
68
70
|
"hooks/blocking/code_rules_duplicate_body.py",
|
|
69
71
|
"hooks/blocking/code_rules_enforcer.py",
|
|
@@ -101,14 +103,17 @@ codex_hook_dependency_manifest = (
|
|
|
101
103
|
"hooks/hooks_constants/hook_block_logger.py",
|
|
102
104
|
"hooks/hooks_constants/inline_tuple_string_magic_constants.py",
|
|
103
105
|
"hooks/hooks_constants/js_conventions_constants.py",
|
|
106
|
+
"hooks/hooks_constants/multi_edit_reconstruction.py",
|
|
104
107
|
"hooks/hooks_constants/orphan_css_class_constants.py",
|
|
105
108
|
"hooks/hooks_constants/paired_test_coverage_constants.py",
|
|
106
109
|
"hooks/hooks_constants/setup_project_paths_constants.py",
|
|
107
110
|
"hooks/hooks_constants/stuttering_check_config.py",
|
|
108
111
|
"hooks/hooks_constants/stuttering_import_binding_constants.py",
|
|
112
|
+
"hooks/hooks_constants/subprocess_budget_completeness_content.py",
|
|
109
113
|
"hooks/hooks_constants/sys_path_insert_constants.py",
|
|
110
114
|
"hooks/hooks_constants/test_layout_constants.py",
|
|
111
115
|
"hooks/hooks_constants/unused_module_import_constants.py",
|
|
116
|
+
"hooks/hooks_constants/validation_phase_constants.py",
|
|
112
117
|
)
|
|
113
118
|
full_prune_opt_in_flag = "--allow-prune-all"
|
|
114
119
|
unreadable_source_root_message = (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
@@ -841,23 +842,35 @@ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
|
|
|
841
842
|
assert drifted_projection != first_projection
|
|
842
843
|
|
|
843
844
|
|
|
844
|
-
def
|
|
845
|
+
def test_codex_hook_manifest_folds_apply_patch_into_the_dispatcher_matcher() -> None:
|
|
846
|
+
"""apply_patch's code_rules_enforcer coverage comes from the dispatcher's own matcher.
|
|
847
|
+
|
|
848
|
+
hooks.json once ran a separate apply_patch -> code_rules_enforcer.py
|
|
849
|
+
PreToolUse command after the dispatcher entry. That coverage is now
|
|
850
|
+
folded into the dispatcher's own Write|Edit|MultiEdit|apply_patch
|
|
851
|
+
matcher, so no standalone apply_patch entry remains.
|
|
852
|
+
"""
|
|
845
853
|
hooks_path = Path(__file__).parents[2] / "hooks" / "hooks.json"
|
|
846
854
|
hooks_configuration = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
847
855
|
pre_tool_use_entries = hooks_configuration["hooks"]["PreToolUse"]
|
|
848
|
-
matcher_names = [each_entry.get("matcher") for each_entry in pre_tool_use_entries]
|
|
849
856
|
|
|
850
|
-
|
|
851
|
-
|
|
857
|
+
dispatcher_entries = [
|
|
858
|
+
each_entry
|
|
859
|
+
for each_entry in pre_tool_use_entries
|
|
860
|
+
if any(
|
|
861
|
+
"/hooks/blocking/pre_tool_use_dispatcher.py" in each_hook.get("command", "")
|
|
862
|
+
for each_hook in each_entry.get("hooks", [])
|
|
863
|
+
)
|
|
864
|
+
]
|
|
865
|
+
assert len(dispatcher_entries) == 1
|
|
866
|
+
assert dispatcher_entries[0]["matcher"] == "Write|Edit|MultiEdit|apply_patch"
|
|
867
|
+
|
|
868
|
+
standalone_apply_patch_entries = [
|
|
852
869
|
each_entry
|
|
853
870
|
for each_entry in pre_tool_use_entries
|
|
854
871
|
if each_entry.get("matcher") == "apply_patch"
|
|
855
872
|
]
|
|
856
|
-
assert
|
|
857
|
-
assert matcher_names.index("apply_patch") > dispatcher_index
|
|
858
|
-
assert enforcer_entries[0]["hooks"][0]["command"].endswith(
|
|
859
|
-
"/hooks/blocking/code_rules_enforcer.py"
|
|
860
|
-
)
|
|
873
|
+
assert standalone_apply_patch_entries == []
|
|
861
874
|
|
|
862
875
|
|
|
863
876
|
def _prepare_projection_fixture(
|
|
@@ -1008,7 +1021,18 @@ def _assert_projection_and_manifest(
|
|
|
1008
1021
|
|
|
1009
1022
|
|
|
1010
1023
|
def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
1024
|
+
"""Prove the detached, materialized enforcer allows a clean add and denies an unnamed raise.
|
|
1025
|
+
|
|
1026
|
+
Disables the ephemeral-path exemption for both runs: without it, a target
|
|
1027
|
+
rooted under the OS temp directory (where this fixture lives) would skip
|
|
1028
|
+
code_rules_enforcer's checks entirely and both assertions would pass
|
|
1029
|
+
without the enforcer ever judging the payload.
|
|
1030
|
+
"""
|
|
1011
1031
|
target_entrypoint = target / materializer.codex_enforcer_script_relative_path
|
|
1032
|
+
subprocess_environment = {
|
|
1033
|
+
**os.environ,
|
|
1034
|
+
"CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT": "1",
|
|
1035
|
+
}
|
|
1012
1036
|
allow_payload = {
|
|
1013
1037
|
"tool_name": "apply_patch",
|
|
1014
1038
|
"cwd": str(target),
|
|
@@ -1029,8 +1053,9 @@ def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
|
1029
1053
|
capture_output=True,
|
|
1030
1054
|
text=True,
|
|
1031
1055
|
check=False,
|
|
1056
|
+
env=subprocess_environment,
|
|
1032
1057
|
)
|
|
1033
|
-
assert allow_run.returncode == 0
|
|
1058
|
+
assert allow_run.returncode == 0, allow_run.stderr
|
|
1034
1059
|
assert allow_run.stdout == ""
|
|
1035
1060
|
|
|
1036
1061
|
allow_tool_input = allow_payload["tool_input"]
|
|
@@ -1050,8 +1075,9 @@ def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
|
1050
1075
|
capture_output=True,
|
|
1051
1076
|
text=True,
|
|
1052
1077
|
check=False,
|
|
1078
|
+
env=subprocess_environment,
|
|
1053
1079
|
)
|
|
1054
|
-
assert deny_run.returncode == 0
|
|
1080
|
+
assert deny_run.returncode == 0, deny_run.stderr
|
|
1055
1081
|
assert json.loads(deny_run.stdout)["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
1056
1082
|
assert target_entrypoint.is_file()
|
|
1057
1083
|
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"""Put the blocking and hooks directories on sys.path for the blocking tests."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
_blocking_directory = str(Path(__file__).resolve().parents[1])
|
|
7
|
-
_hooks_directory = str(Path(__file__).resolve().parents[2])
|
|
8
|
-
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
9
|
-
if each_bootstrap_directory not in sys.path:
|
|
10
|
-
sys.path.insert(0, each_bootstrap_directory)
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""Directive text for the task_list_loop_starter SessionStart hook."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"TASK_LIST_MAINTENANCE_INSTRUCTION",
|
|
7
|
-
"TASK_LIST_LOOP_DIRECTIVE",
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
TASK_LIST_MAINTENANCE_INSTRUCTION = "build or update the task list using the task tools"
|
|
11
|
-
|
|
12
|
-
TASK_LIST_LOOP_DIRECTIVE = (
|
|
13
|
-
"TASK LIST MAINTENANCE: Keep this session's task list current on a 10-minute "
|
|
14
|
-
"cadence. Reuse the active task-list maintenance loop when available. Start "
|
|
15
|
-
"Claude Code's built-in /loop skill with a 10-minute interval when this session "
|
|
16
|
-
"needs its first loop: "
|
|
17
|
-
f'/loop 10m "{TASK_LIST_MAINTENANCE_INSTRUCTION}". Run that instruction once '
|
|
18
|
-
"immediately when starting the loop, then let the loop refresh the list every 10 minutes."
|
|
19
|
-
)
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""SessionStart hook — start a task-list maintenance loop for the session.
|
|
3
|
-
|
|
4
|
-
At session start this hook emits an ``additionalContext`` directive asking Claude
|
|
5
|
-
to keep the session's task list current on a 10-minute cadence. Claude reuses the
|
|
6
|
-
active ``/loop`` skill when available and starts the skill for the session's first
|
|
7
|
-
task-list maintenance loop.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
17
|
-
if _hooks_dir not in sys.path:
|
|
18
|
-
sys.path.insert(0, _hooks_dir)
|
|
19
|
-
|
|
20
|
-
from hooks_constants.task_list_loop_starter_constants import ( # noqa: E402
|
|
21
|
-
TASK_LIST_LOOP_DIRECTIVE,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def build_session_directive() -> str:
|
|
26
|
-
"""Return the task-list loop directive emitted at session start."""
|
|
27
|
-
return TASK_LIST_LOOP_DIRECTIVE
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def main() -> None:
|
|
31
|
-
"""Emit the task-list loop directive as SessionStart additionalContext."""
|
|
32
|
-
print(
|
|
33
|
-
json.dumps(
|
|
34
|
-
{
|
|
35
|
-
"hookSpecificOutput": {
|
|
36
|
-
"hookEventName": "SessionStart",
|
|
37
|
-
"additionalContext": build_session_directive(),
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
)
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if __name__ == "__main__":
|
|
45
|
-
main()
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"""Tests for task_list_loop_starter — SessionStart hook that starts a task-list loop."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
from io import StringIO
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from unittest.mock import patch
|
|
8
|
-
|
|
9
|
-
_SESSION_DIR = Path(__file__).resolve().parent
|
|
10
|
-
_HOOKS_ROOT = _SESSION_DIR.parent
|
|
11
|
-
for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
|
|
12
|
-
if each_sys_path_entry not in sys.path:
|
|
13
|
-
sys.path.insert(0, each_sys_path_entry)
|
|
14
|
-
|
|
15
|
-
import task_list_loop_starter as starter
|
|
16
|
-
|
|
17
|
-
from hooks_constants.task_list_loop_starter_constants import (
|
|
18
|
-
TASK_LIST_LOOP_DIRECTIVE,
|
|
19
|
-
TASK_LIST_MAINTENANCE_INSTRUCTION,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _run_main() -> str:
|
|
24
|
-
"""Return stdout produced by running the hook's main()."""
|
|
25
|
-
captured_stdout = StringIO()
|
|
26
|
-
with patch("sys.stdout", captured_stdout):
|
|
27
|
-
starter.main()
|
|
28
|
-
return captured_stdout.getvalue()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class TestSessionDirective:
|
|
32
|
-
def test_main_emits_additional_context(self) -> None:
|
|
33
|
-
emitted = json.loads(_run_main())
|
|
34
|
-
hook_output = emitted["hookSpecificOutput"]
|
|
35
|
-
assert hook_output["hookEventName"] == "SessionStart"
|
|
36
|
-
assert "additionalContext" in hook_output
|
|
37
|
-
|
|
38
|
-
def test_directive_carries_the_one_line_instruction(self) -> None:
|
|
39
|
-
emitted = json.loads(_run_main())
|
|
40
|
-
assert TASK_LIST_MAINTENANCE_INSTRUCTION in emitted["hookSpecificOutput"]["additionalContext"]
|
|
41
|
-
|
|
42
|
-
def test_directive_names_the_ten_minute_cadence(self) -> None:
|
|
43
|
-
emitted = json.loads(_run_main())
|
|
44
|
-
assert "10-minute" in emitted["hookSpecificOutput"]["additionalContext"]
|
|
45
|
-
|
|
46
|
-
def test_directive_names_the_loop_command(self) -> None:
|
|
47
|
-
emitted = json.loads(_run_main())
|
|
48
|
-
assert "/loop 10m" in emitted["hookSpecificOutput"]["additionalContext"]
|
|
49
|
-
|
|
50
|
-
def test_directive_runs_the_instruction_immediately(self) -> None:
|
|
51
|
-
emitted = json.loads(_run_main())
|
|
52
|
-
assert "Run that instruction once immediately when starting the loop, then" in emitted[
|
|
53
|
-
"hookSpecificOutput"
|
|
54
|
-
]["additionalContext"]
|
|
55
|
-
|
|
56
|
-
def test_directive_reuses_an_existing_loop(self) -> None:
|
|
57
|
-
emitted = json.loads(_run_main())
|
|
58
|
-
assert "Reuse the active task-list maintenance loop when available." in emitted[
|
|
59
|
-
"hookSpecificOutput"
|
|
60
|
-
]["additionalContext"]
|
|
61
|
-
|
|
62
|
-
def test_build_session_directive_returns_the_shared_constant(self) -> None:
|
|
63
|
-
assert starter.build_session_directive() == TASK_LIST_LOOP_DIRECTIVE
|