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.
Files changed (63) hide show
  1. package/CLAUDE.md +0 -13
  2. package/agents/clean-coder.md +2 -2
  3. package/agents/code-verifier.md +0 -1
  4. package/agents/test_agent_frontmatter.py +78 -0
  5. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  6. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
  7. package/bin/install.mjs +1 -0
  8. package/docs/CODE_RULES.md +2 -2
  9. package/hooks/blocking/CLAUDE.md +5 -2
  10. package/hooks/blocking/code_rules_comments.py +2 -2
  11. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
  12. package/hooks/blocking/config/verified_commit_constants.py +2 -0
  13. package/hooks/blocking/conftest.py +115 -0
  14. package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
  15. package/hooks/blocking/pr_description_enforcer.py +46 -22
  16. package/hooks/blocking/pr_description_pr_number.py +5 -3
  17. package/hooks/blocking/pr_description_proof_of_work.py +367 -0
  18. package/hooks/blocking/precommit_code_rules_gate.py +5 -1
  19. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
  20. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
  21. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
  22. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
  23. package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
  24. package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
  25. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
  26. package/hooks/blocking/test_verification_verdict_store.py +11 -0
  27. package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
  28. package/hooks/blocking/test_verified_commit_gate.py +11 -0
  29. package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
  30. package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
  31. package/hooks/blocking/verification_verdict_store.py +73 -5
  32. package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
  33. package/hooks/blocking/verified_commit_gate.py +6 -0
  34. package/hooks/blocking/verifier_verdict_minter.py +6 -0
  35. package/hooks/hooks.json +7 -2
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  38. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
  39. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
  40. package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
  41. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
  42. package/hooks/validators/run_all_validators.py +216 -4
  43. package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
  44. package/package.json +1 -1
  45. package/rules/CLAUDE.md +2 -0
  46. package/rules/nas-ssh-invocation.md +21 -0
  47. package/rules/proof-of-work-pr-comments.md +26 -0
  48. package/scripts/CLAUDE.md +1 -0
  49. package/scripts/claude-chain.example.json +8 -0
  50. package/scripts/claude_chain_runner.py +400 -0
  51. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -0
  52. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
  53. package/scripts/sync_to_cursor/rules.py +1 -1
  54. package/scripts/test_claude_chain_runner.py +472 -0
  55. package/skills/CLAUDE.md +3 -0
  56. package/skills/team-advisor/SKILL.md +188 -0
  57. package/skills/team-advisor-refresh/SKILL.md +25 -0
  58. package/skills/usage-pause/SKILL.md +108 -0
  59. package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
  60. package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
  61. package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
  62. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
  63. 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
- block DELETION of existing comments. This standalone validator
379
- is disabled because it flags ALL comments in existing files,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.90.0",
3
+ "version": "1.92.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
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 (`![](https://placehold.co/20x20/RRGGBB/RRGGBB.png)`), 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 |
@@ -0,0 +1,8 @@
1
+ {
2
+ "chain": [
3
+ { "command": "claude", "extra_args": [] },
4
+ { "command": "claude-1", "extra_args": [] },
5
+ { "command": "claude-2", "extra_args": [] },
6
+ { "command": "claude-3", "extra_args": [] }
7
+ ]
8
+ }