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
@@ -12,6 +12,7 @@ import os
12
12
  import pathlib
13
13
  import subprocess
14
14
  import sys
15
+ from collections.abc import Callable
15
16
 
16
17
  import pytest
17
18
 
@@ -568,3 +569,13 @@ def test_minted_verdict_from_other_worktree_with_wrong_hash_denies(
568
569
  deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
569
570
  assert deny_reason is not None
570
571
  assert "VERIFIED_COMMIT_GATE" in deny_reason
572
+
573
+
574
+ def test_gate_imports_under_foreign_config_shadow(
575
+ run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
576
+ ) -> None:
577
+ completed_probe = run_under_config_shadow(
578
+ "import verified_commit_gate\nprint('IMPORT_OK')\n"
579
+ )
580
+ assert completed_probe.returncode == 0, completed_probe.stderr
581
+ assert "IMPORT_OK" in completed_probe.stdout
@@ -21,6 +21,7 @@ import json
21
21
  import pathlib
22
22
  import subprocess
23
23
  import sys
24
+ from collections.abc import Callable
24
25
 
25
26
  _HOOK_DIR = pathlib.Path(__file__).parent
26
27
  if str(_HOOK_DIR) not in sys.path:
@@ -389,3 +390,13 @@ def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -
389
390
  finally:
390
391
  if verdict_path is not None and verdict_path.exists():
391
392
  verdict_path.unlink()
393
+
394
+
395
+ def test_minter_imports_under_foreign_config_shadow(
396
+ run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
397
+ ) -> None:
398
+ completed_probe = run_under_config_shadow(
399
+ "import verifier_verdict_minter\nprint('IMPORT_OK')\n"
400
+ )
401
+ assert completed_probe.returncode == 0, completed_probe.stderr
402
+ assert "IMPORT_OK" in completed_probe.stdout
@@ -41,6 +41,12 @@ hooks_directory = str(Path(__file__).resolve().parent.parent)
41
41
  if hooks_directory not in sys.path:
42
42
  sys.path.append(hooks_directory)
43
43
 
44
+ from verified_commit_config_bootstrap import ( # noqa: E402
45
+ register_verified_commit_constants,
46
+ )
47
+
48
+ register_verified_commit_constants()
49
+
44
50
  from config.verified_commit_constants import ( # noqa: E402
45
51
  ALL_GATED_TOOL_NAMES,
46
52
  ALL_VERDICT_PATH_SEGMENT_BODIES,
@@ -25,14 +25,22 @@ blocking_directory = str(Path(__file__).resolve().parent)
25
25
  if blocking_directory not in sys.path:
26
26
  sys.path.insert(0, blocking_directory)
27
27
 
28
+ from verified_commit_config_bootstrap import ( # noqa: E402
29
+ register_verified_commit_constants,
30
+ )
31
+
32
+ register_verified_commit_constants()
33
+
28
34
  from config.verified_commit_constants import (
29
35
  AGENT_META_SIDECAR_SUFFIX,
30
36
  AGENT_META_TYPE_KEY,
31
37
  AGENT_TRANSCRIPT_GLOB,
32
38
  BRANCH_REFERENCE_PREFIX,
39
+ BRANCH_REMOTE_CONFIG_KEY_TEMPLATE,
33
40
  BRANCH_WORKTREE_ABSENT_MESSAGE,
34
41
  CLAUDE_HOME_DIRECTORY_NAME,
35
42
  CONFTEST_FILE_NAME,
43
+ DETACHED_HEAD_LABEL,
36
44
  DOCS_ONLY_EXTENSIONS,
37
45
  ALL_FALLBACK_BASE_REFERENCES,
38
46
  EMPTY_SURFACE_GUARD_MESSAGE,
@@ -122,13 +130,65 @@ def _tracked_upstream_reference(repo_root: str) -> str | None:
122
130
  )
123
131
 
124
132
 
133
+ def _head_branch_name(repo_root: str) -> str | None:
134
+ """Read the short name of the branch HEAD points at.
135
+
136
+ Args:
137
+ repo_root: The repository top-level directory.
138
+
139
+ Returns:
140
+ The short branch name, or None when HEAD is detached or git fails.
141
+ """
142
+ branch_name = run_git(repo_root, "rev-parse", "--abbrev-ref", "HEAD")
143
+ if not branch_name or branch_name == DETACHED_HEAD_LABEL:
144
+ return None
145
+ return branch_name
146
+
147
+
148
+ def _upstream_is_own_remote_counterpart(repo_root: str, tracked_upstream: str) -> bool:
149
+ """Decide whether HEAD's upstream is the current branch's own remote ref.
150
+
151
+ ::
152
+
153
+ branch feature tracks origin/feature -> True (own counterpart)
154
+ branch feature tracks origin/develop -> False (different branch)
155
+
156
+ A branch tracking its own remote counterpart makes the merge base against
157
+ that ref resolve to the branch tip itself, masking the branch's real state
158
+ versus the default branch — so the caller ranks that ref below the
159
+ default-branch fallbacks.
160
+
161
+ Args:
162
+ repo_root: The repository top-level directory.
163
+ tracked_upstream: HEAD's configured upstream tracking reference.
164
+
165
+ Returns:
166
+ True when the upstream names ``<remote>/<current-branch>``; False when
167
+ it names a different branch, HEAD is detached, or git fails.
168
+ """
169
+ branch_name = _head_branch_name(repo_root)
170
+ if branch_name is None:
171
+ return False
172
+ configured_remote = run_git(
173
+ repo_root, "config", BRANCH_REMOTE_CONFIG_KEY_TEMPLATE.format(branch_name=branch_name)
174
+ )
175
+ if not configured_remote:
176
+ return False
177
+ return tracked_upstream == f"{configured_remote}/{branch_name}"
178
+
179
+
125
180
  def candidate_base_references(repo_root: str) -> tuple[str, ...]:
126
181
  """Collect the upstream references to probe for the merge base, in order.
127
182
 
128
- Probes ``origin/HEAD`` first, then HEAD's configured upstream tracking
129
- reference (so a non-standard default branch like ``origin/develop`` is
130
- found regardless of its name), then the fixed ``origin/main`` /
131
- ``origin/master`` fallbacks for checkouts with no tracking ref set.
183
+ Probes ``origin/HEAD`` first, then a tracked upstream that names a
184
+ different branch (so a differently-named default like ``origin/develop`` is
185
+ found), then the fixed ``origin/main`` / ``origin/master`` fallbacks. A
186
+ tracked upstream that is the current branch's own remote counterpart
187
+ (``origin/<current-branch>``) ranks last, below the fallbacks: on a PR
188
+ branch whose fork carries ``origin/main`` the merge base comes from
189
+ ``origin/main`` rather than the branch's stale pushed ref, while a repo
190
+ whose default branch is the tracked branch itself (no ``origin/main`` or
191
+ ``origin/master`` present) still falls through to that own-counterpart ref.
132
192
 
133
193
  Args:
134
194
  repo_root: The repository top-level directory.
@@ -138,10 +198,18 @@ def candidate_base_references(repo_root: str) -> tuple[str, ...]:
138
198
  """
139
199
  upstream_head = run_git(repo_root, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
140
200
  tracked_upstream = _tracked_upstream_reference(repo_root)
201
+ high_priority_upstream: tuple[str, ...] = ()
202
+ demoted_upstream: tuple[str, ...] = ()
203
+ if tracked_upstream:
204
+ if _upstream_is_own_remote_counterpart(repo_root, tracked_upstream):
205
+ demoted_upstream = (tracked_upstream,)
206
+ else:
207
+ high_priority_upstream = (tracked_upstream,)
141
208
  ordered_references = (
142
209
  ((upstream_head,) if upstream_head else ())
143
- + ((tracked_upstream,) if tracked_upstream else ())
210
+ + high_priority_upstream
144
211
  + ALL_FALLBACK_BASE_REFERENCES
212
+ + demoted_upstream
145
213
  )
146
214
  return tuple(dict.fromkeys(ordered_references))
147
215
 
@@ -0,0 +1,51 @@
1
+ """Deterministic loader for the verified-commit constants module.
2
+
3
+ The blocking hooks import their shared constants as
4
+ ``from config.verified_commit_constants import ...``. In the installed hook
5
+ tree a second, unrelated ``config`` package can sit ahead of this package on
6
+ ``sys.path`` and win that dotted name by path order, so the import binds to the
7
+ wrong file and raises ImportError on any constant the stale copy lacks. This
8
+ module binds the dotted name to the sibling ``config/verified_commit_constants``
9
+ file by explicit location, so resolution never depends on ``sys.path`` order.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def register_verified_commit_constants() -> None:
20
+ """Bind ``config.verified_commit_constants`` to the sibling config file.
21
+
22
+ ::
23
+
24
+ sys.path = ["<installed hooks>", "<blocking>"] # foreign config first
25
+ register_verified_commit_constants()
26
+ from config.verified_commit_constants import DETACHED_HEAD_LABEL
27
+ ok: bound to <blocking>/config/verified_commit_constants.py
28
+
29
+ A ``from config.verified_commit_constants import`` that follows this call
30
+ reads the entry straight from ``sys.modules``, so it resolves to this file
31
+ whatever else owns the ``config`` name. An entry already cached is left in
32
+ place, so the call is idempotent and never displaces a module a caller
33
+ loaded on purpose.
34
+
35
+ Returns:
36
+ None. The effect is the ``sys.modules`` registration.
37
+ """
38
+ config_module_dotted_name = "config.verified_commit_constants"
39
+ if config_module_dotted_name in sys.modules:
40
+ return
41
+ constants_file_path = (
42
+ Path(__file__).resolve().parent / "config" / "verified_commit_constants.py"
43
+ )
44
+ module_spec = importlib.util.spec_from_file_location(
45
+ config_module_dotted_name, constants_file_path
46
+ )
47
+ if module_spec is None or module_spec.loader is None:
48
+ return
49
+ constants_module = importlib.util.module_from_spec(module_spec)
50
+ sys.modules[config_module_dotted_name] = constants_module
51
+ module_spec.loader.exec_module(constants_module)
@@ -42,6 +42,12 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
42
42
  if _hooks_dir not in sys.path:
43
43
  sys.path.insert(0, _hooks_dir)
44
44
 
45
+ from verified_commit_config_bootstrap import ( # noqa: E402
46
+ register_verified_commit_constants,
47
+ )
48
+
49
+ register_verified_commit_constants()
50
+
45
51
  from config.verified_commit_constants import (
46
52
  ALL_GIT_BINARY_NAMES,
47
53
  CORRECTIVE_MESSAGE,
@@ -35,6 +35,12 @@ blocking_directory = str(Path(__file__).resolve().parent)
35
35
  if blocking_directory not in sys.path:
36
36
  sys.path.insert(0, blocking_directory)
37
37
 
38
+ from verified_commit_config_bootstrap import ( # noqa: E402
39
+ register_verified_commit_constants,
40
+ )
41
+
42
+ register_verified_commit_constants()
43
+
38
44
  from config.verified_commit_constants import (
39
45
  MINTING_AGENT_TYPE,
40
46
  VERDICT_KEY_MANIFEST_SHA256,
package/hooks/hooks.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\"",
10
+ "command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\" --pre-tool-use",
11
11
  "timeout": 15
12
12
  }
13
13
  ]
@@ -55,6 +55,11 @@
55
55
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/gh_body_arg_blocker.py",
56
56
  "timeout": 10
57
57
  },
58
+ {
59
+ "type": "command",
60
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/nas_ssh_binary_enforcer.py",
61
+ "timeout": 10
62
+ },
58
63
  {
59
64
  "type": "command",
60
65
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/volatile_path_in_post_blocker.py",
@@ -88,7 +93,7 @@
88
93
  {
89
94
  "type": "command",
90
95
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_description_enforcer.py",
91
- "timeout": 10
96
+ "timeout": 30
92
97
  },
93
98
  {
94
99
  "type": "command",
@@ -38,6 +38,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
38
38
  | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
39
39
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
40
40
  | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
41
+ | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, the NAS address pattern, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, the batch-mode pattern, and the two deny messages for the NAS ssh binary enforcer |
41
42
  | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
42
43
  | `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
43
44
  | `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
@@ -47,6 +48,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
47
48
  | `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
48
49
  | `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
49
50
  | `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
51
+ | `pr_description_proof_of_work_constants.py` | Proof-part detection patterns, gh command tokens, and corrective messages for the proof-of-work comment audit |
50
52
  | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
51
53
  | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
52
54
  | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
@@ -24,6 +24,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
24
24
  "bugteam_scripts_constants",
25
25
  "implement_scripts_constants",
26
26
  "log_audit_constants",
27
+ "usage_pause_constants",
27
28
  "dev_env_scripts_constants",
28
29
  }
29
30
  )
@@ -26,6 +26,9 @@ MERGE_TREE_CONFLICT_EXIT_CODE: int = 1
26
26
  MERGE_TREE_CLEAN_EXIT_CODE: int = 0
27
27
  MERGE_TREE_TIMEOUT_SECONDS: int = 30
28
28
 
29
+ ALL_MERGE_HEAD_PROBE_FLAGS: tuple[str, ...] = ("rev-parse", "--verify", "--quiet", "MERGE_HEAD")
30
+ ALL_UNMERGED_PATHS_DIFF_FLAGS: tuple[str, ...] = ("diff", "--name-only", "--diff-filter=U")
31
+
29
32
  ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
30
33
  "-c",
31
34
  "core.quotePath=false",
@@ -0,0 +1,59 @@
1
+ """Constants for the NAS ssh binary enforcer hook.
2
+
3
+ Holds the Bash tool name, the NAS host-token pattern, the ssh-family command
4
+ basenames, the Windows OpenSSH binary path suffixes, the launcher-wrapper set, the
5
+ shell control-operator tokens and their split pattern, the leading-assignment and
6
+ launcher-duration patterns, the batch-mode pattern, and the two deny messages.
7
+ """
8
+
9
+ import re
10
+
11
+ BASH_TOOL_NAME = "Bash"
12
+
13
+ NAS_HOST_TOKEN_PATTERN = re.compile(r"(?:^|@)192\.168\.1\.100(?::|$)")
14
+ """Match the NAS address only as a connection host.
15
+
16
+ Anchored to token start or an ``@`` and followed by ``:`` or token end, so the
17
+ address is matched as a bare host (``192.168.1.100``), a ``user@host``
18
+ (``jon@192.168.1.100``), or a ``host:path`` target (``192.168.1.100:/vol1/``,
19
+ ``jon@192.168.1.100:/vol1/``). The address inside a remote-command argument
20
+ (``"ping 192.168.1.100"``) or a remote path component on another host
21
+ (``jon@other:/backup/192.168.1.100/``) does not count as a NAS connection.
22
+ """
23
+
24
+ ALL_SSH_FAMILY_COMMAND_BASENAMES = frozenset(
25
+ {"ssh", "scp", "sftp", "ssh.exe", "scp.exe", "sftp.exe"}
26
+ )
27
+ ALL_OPENSSH_BINARY_PATH_SUFFIXES = (
28
+ "/openssh/ssh.exe",
29
+ "/openssh/scp.exe",
30
+ "/openssh/sftp.exe",
31
+ )
32
+ ALL_LAUNCHER_WRAPPER_COMMANDS = frozenset({"timeout", "nohup", "nice", "stdbuf", "setsid", "env"})
33
+ ALL_SHELL_CONTROL_OPERATOR_TOKENS = frozenset({"&&", "||", ";", "|", "&", "|&"})
34
+ CONTROL_OPERATOR_SPLIT_PATTERN = re.compile(r"(&&|\|\||;|\|&|\||(?<!>)&(?!>))")
35
+ LEADING_ASSIGNMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
36
+ LAUNCHER_DURATION_PATTERN = re.compile(r"^\d+[a-z]*$", re.IGNORECASE)
37
+ BATCH_MODE_PATTERN = re.compile(r"batchmode\s*=\s*yes", re.IGNORECASE)
38
+
39
+ BARE_SSH_BINARY_MESSAGE = (
40
+ "BLOCKED [nas-ssh-binary]: Git Bash's MSYS ssh reads ~/.ssh/id_ed25519 as "
41
+ "world-readable through its ACL mapping, rejects the key as bad permissions, "
42
+ "and falls back to an interactive password prompt that hangs unattended "
43
+ "sessions against the NAS at 192.168.1.100.\n\n"
44
+ "Use the Windows OpenSSH binary, which authenticates the key without prompting:\n"
45
+ ' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
46
+ '-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
47
+ "See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
48
+ )
49
+
50
+ MISSING_BATCH_MODE_MESSAGE = (
51
+ "BLOCKED [nas-ssh-binary]: this NAS ssh command uses the Windows OpenSSH binary "
52
+ "but omits -o BatchMode=yes, so an authentication regression falls back to an "
53
+ "interactive password prompt that hangs unattended sessions against the NAS at "
54
+ "192.168.1.100.\n\n"
55
+ "Add -o BatchMode=yes so a key failure exits loudly rather than prompting:\n"
56
+ ' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
57
+ '-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
58
+ "See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
59
+ )
@@ -41,6 +41,7 @@ ALL_HEAVY_TESTING_HEADERS: frozenset[str] = frozenset(
41
41
  {TEST_PLAN_HEADER, TESTING_HEADER, TESTS_HEADER, VERIFICATION_HEADER, VALIDATION_HEADER}
42
42
  )
43
43
  GH_PR_COMMAND_MIN_TOKEN_COUNT: int = 3
44
+ ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS: frozenset[str] = frozenset({"edit", "comment", "ready"})
44
45
  BODY_FILE_STDIN_SENTINEL: str = "-"
45
46
  ATOMIC_WRITE_TEMP_SUFFIX: str = ".tmp"
46
47
  SELF_CLOSING_REFERENCE_MESSAGE_PREFIX: str = "PR body references its own PR number #"
@@ -134,6 +135,7 @@ __all__ = [
134
135
  "FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
135
136
  "FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
136
137
  "GH_PR_COMMAND_MIN_TOKEN_COUNT",
138
+ "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
137
139
  "HEADING_LINE_PATTERN",
138
140
  "HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
139
141
  "HEAVY_SHAPE",
@@ -0,0 +1,111 @@
1
+ """Configuration constants for the proof-of-work PR-comment audit.
2
+
3
+ Holds the proof-part detection patterns, the gap and linkage word lists,
4
+ the gh command tokens the audit runs to read a PR's comments and diff, and
5
+ the corrective messages returned when a proof part is missing or when
6
+ ``gh pr ready`` runs with no passing proof comment on the PR.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ ALL_PROOF_HEADING_KEYWORDS: tuple[str, ...] = ("proof", "verification")
14
+
15
+ PR_READY_INVOCATION_PATTERN: re.Pattern[str] = re.compile(r"(?:^|[;&|])\s*gh\s+pr\s+ready\b")
16
+ PR_READY_UNDO_FLAG: str = "--undo"
17
+
18
+ GH_EXECUTABLE: str = "gh"
19
+ GH_API_SUBCOMMAND: str = "api"
20
+ GH_PAGINATE_FLAG: str = "--paginate"
21
+ GH_SLURP_FLAG: str = "--slurp"
22
+ PR_COMMENTS_API_PATH_TEMPLATE: str = "repos/{{owner}}/{{repo}}/issues/{pr_number}/comments"
23
+ ALL_PR_DIFF_SUBCOMMANDS: tuple[str, ...] = ("pr", "diff")
24
+ PR_DIFF_NAME_ONLY_FLAG: str = "--name-only"
25
+ ALL_PR_VIEW_NUMBER_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "number")
26
+ PR_NUMBER_JSON_FIELD: str = "number"
27
+ COMMENT_BODY_JSON_FIELD: str = "body"
28
+ GH_COMMAND_TIMEOUT_SECONDS: int = 6
29
+ MAX_DIFF_SCAN_CHARS: int = 200000
30
+
31
+ ALL_VISUAL_FILE_SUFFIXES: frozenset[str] = frozenset(
32
+ {".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".html", ".css"}
33
+ )
34
+ HEX_COLOR_ADDED_LINE_PATTERN: re.Pattern[str] = re.compile(r"^\+.*#[0-9A-Fa-f]{6}\b", re.MULTILINE)
35
+ IMAGE_EMBED_PATTERN: re.Pattern[str] = re.compile(r"!\[[^\]]*\]\([^)\s]+\)")
36
+ ISSUE_REFERENCE_PATTERN: re.Pattern[str] = re.compile(r"#\d+")
37
+ PLAN_LINKAGE_KEYWORD_PATTERN: re.Pattern[str] = re.compile(
38
+ r"\b(?:issue|phase|plan|parent|advanc\w*|milestone|part of)\b", re.IGNORECASE
39
+ )
40
+ DIGIT_PATTERN: re.Pattern[str] = re.compile(r"\d")
41
+ ALL_HONEST_GAP_PHRASES: tuple[str, ...] = (
42
+ "gap",
43
+ "limitation",
44
+ "cannot",
45
+ "does not show",
46
+ "not shown",
47
+ "unverified",
48
+ "not covered",
49
+ )
50
+
51
+ PROOF_PART_COMMAND_MESSAGE: str = (
52
+ "proof comment missing a fenced command block showing the exact command(s) run on real data"
53
+ )
54
+ PROOF_PART_MEASURED_MESSAGE: str = (
55
+ "proof comment missing a measured-value element -- add a table row or bullet fact carrying "
56
+ "the numbers read from the produced artifact"
57
+ )
58
+ PROOF_PART_PLAN_LINKAGE_MESSAGE: str = (
59
+ "proof comment missing a plan-linkage sentence -- add one line naming the parent issue or "
60
+ "phase this PR advances, with its #number"
61
+ )
62
+ PROOF_PART_VISUAL_MESSAGE: str = (
63
+ "proof comment missing a visual element on a visual change -- embed an image (color "
64
+ "swatches, before/after screenshots) for values a human cannot read at a glance"
65
+ )
66
+ PROOF_PART_HONEST_GAPS_MESSAGE: str = (
67
+ "proof comment missing an honest-gaps statement -- say plainly what this offline proof "
68
+ "cannot show and what covers that gap"
69
+ )
70
+
71
+ PR_READY_GATE_MESSAGE_TEMPLATE: str = (
72
+ "BLOCKED: [PROOF_OF_WORK] PR #{pr_number} carries no passing proof-of-work comment. "
73
+ "Post one comment with a proof or verification heading carrying: "
74
+ "(1) the exact command(s) run on real data in a fenced code block, "
75
+ "(2) measured outcomes from the produced artifact as a table row or bullet facts, "
76
+ "(3) one sentence naming the parent issue or phase this PR advances, with its #number, "
77
+ "(4) an image embed when the diff touches images, HTML, CSS, or hex color values, and "
78
+ "(5) an honest statement of what the offline proof cannot show. "
79
+ "Then re-run gh pr ready."
80
+ )
81
+
82
+ __all__ = [
83
+ "ALL_HONEST_GAP_PHRASES",
84
+ "ALL_PROOF_HEADING_KEYWORDS",
85
+ "ALL_PR_DIFF_SUBCOMMANDS",
86
+ "ALL_PR_VIEW_NUMBER_ARGUMENTS",
87
+ "ALL_VISUAL_FILE_SUFFIXES",
88
+ "COMMENT_BODY_JSON_FIELD",
89
+ "DIGIT_PATTERN",
90
+ "GH_API_SUBCOMMAND",
91
+ "GH_COMMAND_TIMEOUT_SECONDS",
92
+ "GH_EXECUTABLE",
93
+ "GH_PAGINATE_FLAG",
94
+ "GH_SLURP_FLAG",
95
+ "HEX_COLOR_ADDED_LINE_PATTERN",
96
+ "IMAGE_EMBED_PATTERN",
97
+ "ISSUE_REFERENCE_PATTERN",
98
+ "MAX_DIFF_SCAN_CHARS",
99
+ "PLAN_LINKAGE_KEYWORD_PATTERN",
100
+ "PROOF_PART_COMMAND_MESSAGE",
101
+ "PROOF_PART_HONEST_GAPS_MESSAGE",
102
+ "PROOF_PART_MEASURED_MESSAGE",
103
+ "PROOF_PART_PLAN_LINKAGE_MESSAGE",
104
+ "PROOF_PART_VISUAL_MESSAGE",
105
+ "PR_COMMENTS_API_PATH_TEMPLATE",
106
+ "PR_DIFF_NAME_ONLY_FLAG",
107
+ "PR_NUMBER_JSON_FIELD",
108
+ "PR_READY_GATE_MESSAGE_TEMPLATE",
109
+ "PR_READY_INVOCATION_PATTERN",
110
+ "PR_READY_UNDO_FLAG",
111
+ ]