claude-dev-env 1.90.0 → 1.92.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 +0 -13
- package/agents/clean-coder.md +2 -2
- package/agents/code-verifier.md +0 -1
- package/agents/test_agent_frontmatter.py +78 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/bin/install.mjs +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +5 -2
- package/hooks/blocking/code_rules_comments.py +2 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
- package/hooks/blocking/config/verified_commit_constants.py +2 -0
- package/hooks/blocking/conftest.py +115 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
- package/hooks/blocking/pr_description_enforcer.py +46 -22
- package/hooks/blocking/pr_description_pr_number.py +5 -3
- package/hooks/blocking/pr_description_proof_of_work.py +367 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
- package/hooks/blocking/test_verification_verdict_store.py +11 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
- package/hooks/blocking/test_verified_commit_gate.py +11 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
- package/hooks/blocking/verification_verdict_store.py +73 -5
- package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
- package/hooks/blocking/verified_commit_gate.py +6 -0
- package/hooks/blocking/verifier_verdict_minter.py +6 -0
- package/hooks/hooks.json +7 -2
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
- package/hooks/validators/run_all_validators.py +216 -4
- package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +2 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/scripts/CLAUDE.md +1 -0
- package/scripts/claude-chain.example.json +8 -0
- package/scripts/claude_chain_runner.py +400 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/team-advisor/SKILL.md +188 -0
- package/skills/team-advisor-refresh/SKILL.md +25 -0
- package/skills/usage-pause/SKILL.md +108 -0
- package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
- package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
- package/system-prompts/software-engineer.xml +3 -2
|
@@ -6,6 +6,7 @@ Exit code 0 = all checks pass, 1 = violations found.
|
|
|
6
6
|
# pragma: no-tdd-gate
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
+
import json
|
|
9
10
|
import os
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
@@ -27,6 +28,15 @@ VALIDATORS_DIR = Path(__file__).parent
|
|
|
27
28
|
hooks_dir = VALIDATORS_DIR.parent
|
|
28
29
|
package_name = VALIDATORS_DIR.name
|
|
29
30
|
|
|
31
|
+
_hooks_directory_on_path = str(hooks_dir.resolve())
|
|
32
|
+
if _hooks_directory_on_path not in sys.path:
|
|
33
|
+
sys.path.insert(0, _hooks_directory_on_path)
|
|
34
|
+
|
|
35
|
+
from hooks_constants.multi_edit_reconstruction import ( # noqa: E402
|
|
36
|
+
apply_edits,
|
|
37
|
+
edits_for_tool,
|
|
38
|
+
)
|
|
39
|
+
|
|
30
40
|
|
|
31
41
|
def _windows_non_unc_working_directory_string(
|
|
32
42
|
candidate_directory_strings: list[str | None],
|
|
@@ -99,8 +109,19 @@ def invoke_validator_module(module_stem: str, forwarded_file_paths: List[str]) -
|
|
|
99
109
|
|
|
100
110
|
def run_validators_entrypoint_subprocess(
|
|
101
111
|
extra_arguments: List[str],
|
|
112
|
+
stdin_text: Optional[str] = None,
|
|
102
113
|
) -> subprocess.CompletedProcess[str]:
|
|
103
|
-
"""Run ``python -m validators.run_all_validators`` with a Windows-safe cwd.
|
|
114
|
+
"""Run ``python -m validators.run_all_validators`` with a Windows-safe cwd.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
extra_arguments: Argument vector appended after the module name.
|
|
118
|
+
stdin_text: Text replayed as the subprocess stdin, or None to leave
|
|
119
|
+
stdin empty. The PreToolUse gate mode reads its payload from stdin,
|
|
120
|
+
so a caller exercising ``--pre-tool-use`` passes the payload here.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The completed subprocess carrying its captured stdout and stderr.
|
|
124
|
+
"""
|
|
104
125
|
working_directory_string, environment = (
|
|
105
126
|
_hooks_subprocess_working_directory_and_environment()
|
|
106
127
|
)
|
|
@@ -111,6 +132,7 @@ def run_validators_entrypoint_subprocess(
|
|
|
111
132
|
text=True,
|
|
112
133
|
cwd=working_directory_string,
|
|
113
134
|
env=environment,
|
|
135
|
+
input=stdin_text,
|
|
114
136
|
)
|
|
115
137
|
|
|
116
138
|
|
|
@@ -375,9 +397,9 @@ def run_comment_checks(files: List[Path]) -> ValidatorResult:
|
|
|
375
397
|
"""Comment preservation is enforced by code_rules_enforcer hook.
|
|
376
398
|
|
|
377
399
|
The hook compares old vs new content to block NEW comments and
|
|
378
|
-
|
|
379
|
-
is disabled because it flags ALL comments in
|
|
380
|
-
which forces agents to remove them to pass validation.
|
|
400
|
+
print a stderr advisory when an existing comment is removed. This
|
|
401
|
+
standalone validator is disabled because it flags ALL comments in
|
|
402
|
+
existing files, which forces agents to remove them to pass validation.
|
|
381
403
|
"""
|
|
382
404
|
return ValidatorResult(
|
|
383
405
|
name="No Comments",
|
|
@@ -658,6 +680,188 @@ def get_changed_files() -> List[Path]:
|
|
|
658
680
|
return [Path(f) for f in files if f]
|
|
659
681
|
|
|
660
682
|
|
|
683
|
+
def _read_target_file_content(file_path: str) -> Optional[str]:
|
|
684
|
+
"""Return the on-disk content of *file_path*, or None when it cannot be read."""
|
|
685
|
+
try:
|
|
686
|
+
with open(file_path, "r", encoding="utf-8") as readable_file:
|
|
687
|
+
return readable_file.read()
|
|
688
|
+
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def reconstruct_proposed_content(
|
|
693
|
+
tool_name: str, tool_input: Dict[str, object]
|
|
694
|
+
) -> Optional[str]:
|
|
695
|
+
"""Return the post-edit content one Write, Edit, or MultiEdit payload leaves on disk.
|
|
696
|
+
|
|
697
|
+
::
|
|
698
|
+
|
|
699
|
+
Write -> tool_input["content"] verbatim
|
|
700
|
+
Edit -> existing file, each old_string rewritten to new_string
|
|
701
|
+
MultiEdit -> existing file, each edit applied in order
|
|
702
|
+
|
|
703
|
+
The Edit and MultiEdit reconstruction reuses the shared applier so this gate
|
|
704
|
+
judges the same post-edit content the standalone blockers judge.
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
tool_name: The intercepted tool — Write, Edit, or MultiEdit.
|
|
708
|
+
tool_input: The tool's input payload.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
The proposed post-edit content, or None when the payload carries no
|
|
712
|
+
readable target for an edit or no string content for a write.
|
|
713
|
+
"""
|
|
714
|
+
if tool_name == "Write":
|
|
715
|
+
written_content = tool_input.get("content", "")
|
|
716
|
+
return written_content if isinstance(written_content, str) else None
|
|
717
|
+
file_path = tool_input.get("file_path", "")
|
|
718
|
+
if not isinstance(file_path, str) or not file_path:
|
|
719
|
+
return None
|
|
720
|
+
existing_content = _read_target_file_content(file_path)
|
|
721
|
+
if existing_content is None:
|
|
722
|
+
return None
|
|
723
|
+
return apply_edits(existing_content, edits_for_tool(tool_name, dict(tool_input)))
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def run_file_scoped_validators(all_files: List[Path]) -> List[ValidatorResult]:
|
|
727
|
+
"""Run every validator scoped to individual files against *all_files*.
|
|
728
|
+
|
|
729
|
+
Excludes the branch-scoped File Structure and Git validators, which grade
|
|
730
|
+
the whole project rather than a single proposed file.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
all_files: The files under validation — a single reconstructed file in
|
|
734
|
+
gate mode.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
One ValidatorResult per file-scoped validator, in run order.
|
|
738
|
+
"""
|
|
739
|
+
return [
|
|
740
|
+
run_python_style_checks(all_files),
|
|
741
|
+
run_test_safety_checks(all_files),
|
|
742
|
+
run_react_checks(all_files),
|
|
743
|
+
run_ruff_checks(all_files),
|
|
744
|
+
run_mypy_checks(all_files),
|
|
745
|
+
run_abbreviation_checks(all_files),
|
|
746
|
+
run_pr_reference_checks(all_files),
|
|
747
|
+
run_magic_value_checks(all_files),
|
|
748
|
+
run_useless_test_checks(all_files),
|
|
749
|
+
run_security_checks(all_files),
|
|
750
|
+
run_code_quality_checks(all_files),
|
|
751
|
+
run_python_antipattern_checks(all_files),
|
|
752
|
+
run_todo_checks(all_files),
|
|
753
|
+
run_type_safety_checks(all_files),
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def validate_proposed_file(
|
|
758
|
+
file_path: str, proposed_content: str
|
|
759
|
+
) -> List[ValidatorResult]:
|
|
760
|
+
"""Validate *proposed_content* as if written to *file_path*.
|
|
761
|
+
|
|
762
|
+
Writes the content to a temporary file that carries the target's basename so
|
|
763
|
+
suffix-based and test-name-based validator filtering matches the real path,
|
|
764
|
+
then runs the file-scoped validators against it.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
file_path: The destination path the write or edit targets.
|
|
768
|
+
proposed_content: The reconstructed post-edit content of that file.
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
One ValidatorResult per file-scoped validator.
|
|
772
|
+
"""
|
|
773
|
+
base_name = Path(file_path).name
|
|
774
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
775
|
+
temporary_file = Path(temporary_directory) / base_name
|
|
776
|
+
temporary_file.write_text(proposed_content, encoding="utf-8")
|
|
777
|
+
return run_file_scoped_validators([temporary_file])
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _proposed_content_deny_reason(failed_results: List[ValidatorResult]) -> str:
|
|
781
|
+
"""Compose the deny reason naming each failing validator and its output.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
failed_results: The validator results that did not pass.
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
The composed ``permissionDecisionReason`` text.
|
|
788
|
+
"""
|
|
789
|
+
violation_summaries = [
|
|
790
|
+
f"{each_result.name} (checks {each_result.checks}): {each_result.output.strip()}"
|
|
791
|
+
for each_result in failed_results
|
|
792
|
+
]
|
|
793
|
+
deny_reason_item_separator = " | "
|
|
794
|
+
joined_summaries = deny_reason_item_separator.join(violation_summaries)
|
|
795
|
+
return (
|
|
796
|
+
f"BLOCKED: [validators] {len(failed_results)} "
|
|
797
|
+
f"validator(s) failed: {joined_summaries}"
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _emit_pre_tool_use_deny(deny_reason: str) -> None:
|
|
802
|
+
"""Write one PreToolUse deny JSON payload carrying *deny_reason* to stdout."""
|
|
803
|
+
deny_payload = {
|
|
804
|
+
"hookSpecificOutput": {
|
|
805
|
+
"hookEventName": "PreToolUse",
|
|
806
|
+
"permissionDecision": "deny",
|
|
807
|
+
"permissionDecisionReason": deny_reason,
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
sys.stdout.write(json.dumps(deny_payload) + "\n")
|
|
811
|
+
sys.stdout.flush()
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _evaluate_pre_tool_use_payload() -> None:
|
|
815
|
+
"""Read the PreToolUse payload from stdin and deny a violating file write.
|
|
816
|
+
|
|
817
|
+
Reconstructs the proposed post-edit content of the one target file and runs
|
|
818
|
+
the file-scoped validators against it. Emits a deny decision naming each
|
|
819
|
+
failing validator when any fires; writes nothing for a clean file, an
|
|
820
|
+
unparseable payload, or a payload no validator covers.
|
|
821
|
+
"""
|
|
822
|
+
pre_tool_use_payload = json.load(sys.stdin)
|
|
823
|
+
if not isinstance(pre_tool_use_payload, dict):
|
|
824
|
+
return
|
|
825
|
+
tool_name = pre_tool_use_payload.get("tool_name", "")
|
|
826
|
+
tool_input = pre_tool_use_payload.get("tool_input", {})
|
|
827
|
+
if not isinstance(tool_name, str) or not isinstance(tool_input, dict):
|
|
828
|
+
return
|
|
829
|
+
file_path = tool_input.get("file_path", "")
|
|
830
|
+
if not isinstance(file_path, str) or not file_path:
|
|
831
|
+
return
|
|
832
|
+
proposed_content = reconstruct_proposed_content(tool_name, tool_input)
|
|
833
|
+
if not proposed_content:
|
|
834
|
+
return
|
|
835
|
+
all_results = validate_proposed_file(file_path, proposed_content)
|
|
836
|
+
failed_results = [
|
|
837
|
+
each_result
|
|
838
|
+
for each_result in all_results
|
|
839
|
+
if not each_result.passed and not each_result.skipped
|
|
840
|
+
]
|
|
841
|
+
if failed_results:
|
|
842
|
+
_emit_pre_tool_use_deny(_proposed_content_deny_reason(failed_results))
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def run_pre_tool_use_gate() -> int:
|
|
846
|
+
"""Run the PreToolUse gate, never crashing the tool call on an internal error.
|
|
847
|
+
|
|
848
|
+
A hook that raises is rendered by the harness as a tool malfunction, so an
|
|
849
|
+
unexpected failure here logs to stderr and returns 0 rather than propagating.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
Always 0 — the gate signals a block through the deny payload, not an
|
|
853
|
+
exit code.
|
|
854
|
+
"""
|
|
855
|
+
try:
|
|
856
|
+
_evaluate_pre_tool_use_payload()
|
|
857
|
+
except json.JSONDecodeError:
|
|
858
|
+
return 0
|
|
859
|
+
except Exception as error:
|
|
860
|
+
sys.stderr.write(f"[run_all_validators] pre-tool-use gate error: {error}\n")
|
|
861
|
+
sys.stderr.flush()
|
|
862
|
+
return 0
|
|
863
|
+
|
|
864
|
+
|
|
661
865
|
def main() -> int:
|
|
662
866
|
"""Run all validators and report results."""
|
|
663
867
|
parser = argparse.ArgumentParser(description="Run pre-push validators")
|
|
@@ -687,8 +891,16 @@ def main() -> int:
|
|
|
687
891
|
default=2,
|
|
688
892
|
help="Lines of context around violations",
|
|
689
893
|
)
|
|
894
|
+
parser.add_argument(
|
|
895
|
+
"--pre-tool-use",
|
|
896
|
+
action="store_true",
|
|
897
|
+
help="Run as a PreToolUse gate on the single proposed file write from stdin",
|
|
898
|
+
)
|
|
690
899
|
args = parser.parse_args()
|
|
691
900
|
|
|
901
|
+
if args.pre_tool_use:
|
|
902
|
+
return run_pre_tool_use_gate()
|
|
903
|
+
|
|
692
904
|
if args.health:
|
|
693
905
|
health = get_system_health()
|
|
694
906
|
print_health_report(health)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Behavioral tests for the run_all_validators PreToolUse gate mode.
|
|
2
|
+
|
|
3
|
+
The gate mode validates the proposed post-edit content of the single file a
|
|
4
|
+
Write, Edit, or MultiEdit would produce and emits a PreToolUse deny decision
|
|
5
|
+
when that content violates a validator, rather than grading the whole branch.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from unittest.mock import patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from .run_all_validators import main, run_validators_entrypoint_subprocess
|
|
17
|
+
|
|
18
|
+
CLEAN_PYTHON_SOURCE = (
|
|
19
|
+
"def add_two_numbers(first_number: int, second_number: int) -> int:\n"
|
|
20
|
+
" return first_number + second_number\n"
|
|
21
|
+
)
|
|
22
|
+
VIOLATING_PYTHON_SOURCE = (
|
|
23
|
+
"def calculate_total_price(unit_price: int, quantity: int) -> int:\n"
|
|
24
|
+
" return unit_price * quantity * 199 * 42\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_gate(payload: dict[str, object]) -> "subprocess.CompletedProcess[str]":
|
|
29
|
+
return run_validators_entrypoint_subprocess(["--pre-tool-use"], stdin_text=json.dumps(payload))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestPreToolUseGate:
|
|
33
|
+
def test_write_with_violating_content_denies(self) -> None:
|
|
34
|
+
completed = run_gate(
|
|
35
|
+
{
|
|
36
|
+
"tool_name": "Write",
|
|
37
|
+
"tool_input": {
|
|
38
|
+
"file_path": "calculate.py",
|
|
39
|
+
"content": VIOLATING_PYTHON_SOURCE,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
assert completed.returncode == 0, completed.stderr
|
|
44
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
45
|
+
assert "Magic Values" in completed.stdout
|
|
46
|
+
|
|
47
|
+
def test_write_with_clean_content_allows(self) -> None:
|
|
48
|
+
completed = run_gate(
|
|
49
|
+
{
|
|
50
|
+
"tool_name": "Write",
|
|
51
|
+
"tool_input": {
|
|
52
|
+
"file_path": "add.py",
|
|
53
|
+
"content": CLEAN_PYTHON_SOURCE,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
assert completed.returncode == 0, completed.stderr
|
|
58
|
+
assert "deny" not in completed.stdout
|
|
59
|
+
|
|
60
|
+
def test_edit_validates_reconstructed_post_edit_content(self, tmp_path: Path) -> None:
|
|
61
|
+
target_file = tmp_path / "calculate.py"
|
|
62
|
+
target_file.write_text(CLEAN_PYTHON_SOURCE, encoding="utf-8")
|
|
63
|
+
completed = run_gate(
|
|
64
|
+
{
|
|
65
|
+
"tool_name": "Edit",
|
|
66
|
+
"tool_input": {
|
|
67
|
+
"file_path": str(target_file),
|
|
68
|
+
"old_string": " return first_number + second_number\n",
|
|
69
|
+
"new_string": " return first_number + second_number + 199 * 42\n",
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
assert completed.returncode == 0, completed.stderr
|
|
74
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
75
|
+
|
|
76
|
+
def test_unparseable_payload_exits_silently(self) -> None:
|
|
77
|
+
completed = run_validators_entrypoint_subprocess(
|
|
78
|
+
["--pre-tool-use"], stdin_text="not json at all"
|
|
79
|
+
)
|
|
80
|
+
assert completed.returncode == 0, completed.stderr
|
|
81
|
+
assert "deny" not in completed.stdout
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestCliModeRegression:
|
|
85
|
+
def test_cli_mode_reports_violations_and_exits_one(
|
|
86
|
+
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
87
|
+
) -> None:
|
|
88
|
+
violating_file = tmp_path / "calculate.py"
|
|
89
|
+
violating_file.write_text(VIOLATING_PYTHON_SOURCE, encoding="utf-8")
|
|
90
|
+
with patch(
|
|
91
|
+
"validators.run_all_validators.get_changed_files",
|
|
92
|
+
return_value=[violating_file],
|
|
93
|
+
):
|
|
94
|
+
original_argv = sys.argv
|
|
95
|
+
try:
|
|
96
|
+
sys.argv = ["run_all_validators.py"]
|
|
97
|
+
exit_code = main()
|
|
98
|
+
finally:
|
|
99
|
+
sys.argv = original_argv
|
|
100
|
+
captured = capsys.readouterr()
|
|
101
|
+
assert exit_code == 1
|
|
102
|
+
assert "PRE-PUSH VALIDATOR RESULTS" in captured.out
|
package/package.json
CHANGED
package/rules/CLAUDE.md
CHANGED
|
@@ -25,6 +25,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
|
|
|
25
25
|
| `git-workflow.md` | PR workflow: always create as draft, one commit per review stage, never commit working docs or images |
|
|
26
26
|
| `hook-prose-matches-detector.md` | Hook prose descriptions match what the hook actually detects |
|
|
27
27
|
| `long-horizon-autonomy.md` | Autonomous-run behaviors: act on what you have, do not end on a promise, delegate and keep working |
|
|
28
|
+
| `nas-ssh-invocation.md` | Reach the NAS at `192.168.1.100` through the `System32/OpenSSH` binary with `-o BatchMode=yes`; bare `ssh`/`scp`/`sftp` stalls on an interactive password prompt |
|
|
28
29
|
| `no-cross-skill-duplicate-helpers.md` | No duplicating shared helpers across skills; use `_shared/` |
|
|
29
30
|
| `no-historical-clutter.md` | Documentation describes current state only; no historical or transitional language |
|
|
30
31
|
| `no-inline-destructive-literals.md` | No destructive-command literals in Bash tool command strings, even as data |
|
|
@@ -37,6 +38,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
|
|
|
37
38
|
| `plain-illustrative-docstrings.md` | Public docstring narrative reads plainly and shows behavior with a diagram block (a `::` example or a doctest), painting a concrete scene a general developer follows on first read; a run-on backstop hook, a prose-wall backstop hook, and Category O9 audit enforce it |
|
|
38
39
|
| `plain-language.md` | Everyday words, short active sentences, lead with the answer |
|
|
39
40
|
| `prompt-workflow-context-controls.md` | Keep prompt-workflow instruction layers small and stable; load heavy skills on demand |
|
|
41
|
+
| `proof-of-work-pr-comments.md` | Every PR carries one five-part proof-of-work comment before it leaves draft; the `pr_description_enforcer` hook audits proof-shaped comments and gates `gh pr ready` |
|
|
40
42
|
| `re-stage-before-commit.md` | Stage the files edited this session before `git commit`; the session edit stage gate denies a commit that leaves a tracked session edit unstaged, with `-a`, a pathspec, a preceding `git add`, and `# partial-commit` as escapes |
|
|
41
43
|
| `research-mode.md` | Three anti-hallucination constraints: say "I don't know", verify with citations, quote for factual grounding |
|
|
42
44
|
| `right-sized-engineering.md` | Simple over clever; functions over classes; concrete over abstract |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# NAS SSH Invocation Policy
|
|
2
|
+
|
|
3
|
+
**When this applies:** Any `ssh`, `scp`, or `sftp` command against the NAS at `192.168.1.100`.
|
|
4
|
+
|
|
5
|
+
## Rule
|
|
6
|
+
|
|
7
|
+
Reach the NAS through the Windows OpenSSH binary with batch mode on. Git Bash's MSYS `ssh` reads `~/.ssh/id_ed25519` as world-readable through its ACL mapping, rejects the key as bad permissions, offers no key, and falls back to an interactive password prompt. In an unattended session no one answers that prompt, so the session hangs. The `System32/OpenSSH` binary authenticates the same key without a prompt.
|
|
8
|
+
|
|
9
|
+
Use this form for every NAS ssh command:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
"/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 -p 9222 jon@192.168.1.100 "<cmd>"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`scp` and `sftp` take the matching `System32/OpenSSH` binary, `-o BatchMode=yes`, and port `9222` (`-P` for `scp`).
|
|
16
|
+
|
|
17
|
+
`-o BatchMode=yes` is required, not optional: it turns a key-authentication failure into a loud non-zero exit rather than a silent password prompt, so an auth regression surfaces as an error you can read.
|
|
18
|
+
|
|
19
|
+
## Enforcement
|
|
20
|
+
|
|
21
|
+
`nas_ssh_binary_enforcer.py` (PreToolUse on Bash) denies a bare `ssh`/`scp`/`sftp` command word aimed at `192.168.1.100` and points at the full-binary form. It also denies the full `System32/OpenSSH` binary to that host when the command omits `-o BatchMode=yes`. Commands to any other host, and commands that mention the address without an ssh-family command word, pass.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Proof-of-Work PR Comments
|
|
2
|
+
|
|
3
|
+
**When this applies:** Every pull request. One comment on the PR carries the proof of the work — posted after `gh pr create` or after a material new commit, and always before the PR leaves draft.
|
|
4
|
+
|
|
5
|
+
## The standard
|
|
6
|
+
|
|
7
|
+
A proof comment has five parts:
|
|
8
|
+
|
|
9
|
+
1. **The exact commands run on real data.** A fenced code block showing the command(s) that produced the artifact — not test output alone.
|
|
10
|
+
2. **Measured outcomes.** Numbers read from the produced artifact — counts, dimensions, hashes, byte sizes, rankings — as a table or a bullet list of facts.
|
|
11
|
+
3. **Plan linkage.** One sentence naming the parent issue or phase the PR advances, with the issue reference (for example: "Advances phase 2 of issue #12").
|
|
12
|
+
4. **Visual evidence for values a human cannot read at a glance.** When the change is visual (the diff touches images, HTML, CSS, or hex color values), embed an image: hex colors as inline swatch images (``), coordinates as marked-up screenshots, size changes as before/after pairs. A wall of raw values fails the standard even when every value is correct.
|
|
13
|
+
5. **Honest gaps.** State plainly what the offline proof cannot show, and what covers that gap.
|
|
14
|
+
|
|
15
|
+
## Enforcement
|
|
16
|
+
|
|
17
|
+
The `pr_description_enforcer` hook enforces the standard at two points:
|
|
18
|
+
|
|
19
|
+
- **On `gh pr comment`:** a comment body whose heading names proof or verification is audited for the five parts. The audit looks for a fenced code block with content, a table row or bullet line carrying a number, a line pairing an issue reference (`#123`) with a linkage word (issue, phase, plan, parent, advances, milestone, part of), an image embed when the PR diff is visual, and a gap phrase (gap, limitation, cannot, does not show, not shown, unverified, not covered). The block message names each missing part.
|
|
20
|
+
- **On `gh pr ready`:** the hook reads the PR's comments and blocks readying while no comment passes the audit. `gh pr ready --undo` returns a PR to draft and is never blocked.
|
|
21
|
+
|
|
22
|
+
A `gh` failure (network, auth, missing executable) never blocks — the gate fails open on tooling problems, and the comment audit skips bodies it cannot read.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
A PR body says what changed; the proof comment shows that it worked. Real command output, measured numbers, and a rendered image let a reviewer check the claim in seconds, with no need to re-run the work. Stating the gaps keeps the proof honest: the reviewer knows exactly what still rests on trust and where that is covered. Gating draft-to-ready makes the comment land before review starts, on every machine, whatever the session's habits.
|
package/scripts/CLAUDE.md
CHANGED
|
@@ -6,6 +6,7 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
|
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|---|---|
|
|
9
|
+
| `claude_chain_runner.py` | Runs a `claude` invocation through a config-driven fallback chain (`~/.claude/claude-chain.json`): the leading binary serves the call, and only a usage-limit failure falls over to the next logged-in binary; usable as an imported module (`run_claude`) or a CLI. Copy `claude-chain.example.json` to `~/.claude/claude-chain.json` and list your binaries in fallback order |
|
|
9
10
|
| `gh_artifact_upload.py` | Uploads a file to a repo's durable `artifacts` prerelease under a timestamped asset name and prints the permanent download URL a GitHub post can link |
|
|
10
11
|
| `setup_project_paths.py` | One-time bootstrap: discovers git repos via `es.exe` (Everything) and writes `~/.claude/project-paths.json`; never hardcodes scan roots |
|
|
11
12
|
| `sweep_empty_dirs.py` | Deletes empty directories older than a configurable age under a given root; runs once (`--once`) or in continuous-watch mode |
|