claude-dev-env 8.6.1 → 8.8.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 (119) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +2 -2
  3. package/_shared/advisor/reference/identity.md +1 -1
  4. package/_shared/advisor/reference/spawn-walk-log.md +1 -1
  5. package/_shared/advisor/reference/warm-up.md +3 -1
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +23 -5
  8. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +85 -6
  9. package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
  10. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
  11. package/bin/install.test.mjs +25 -0
  12. package/docs/high-trust-agent-delivery.md +132 -0
  13. package/docs/references/advisor-tool.md +3 -2
  14. package/docs/second-claude-account.md +50 -0
  15. package/hooks/advisory/refactor_guard.py +2 -2
  16. package/hooks/advisory/test_refactor_guard.py +21 -0
  17. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  18. package/hooks/blocking/code_rules_shared.py +4 -7
  19. package/hooks/blocking/conftest.py +22 -0
  20. package/hooks/blocking/state_description_blocker.py +10 -8
  21. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  22. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
  23. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  24. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  25. package/hooks/blocking/test_code_rules_shared.py +18 -0
  26. package/hooks/blocking/test_conftest.py +35 -0
  27. package/hooks/blocking/test_pii_scanner.py +8 -0
  28. package/hooks/blocking/test_state_description_blocker.py +16 -0
  29. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  30. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  31. package/hooks/git-hooks/test_config.py +10 -3
  32. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  33. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  34. package/hooks/git-hooks/verification_notice_state.py +3 -1
  35. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  36. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  37. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  38. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  39. package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
  40. package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
  41. package/hooks/validators/exempt_paths.py +3 -18
  42. package/hooks/validators/test_exempt_paths.py +20 -0
  43. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  44. package/package.json +1 -1
  45. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  46. package/rules/archiving-agent-config.md +10 -0
  47. package/rules/code-standards.md +1 -0
  48. package/rules/correction-lens.md +134 -0
  49. package/rules/flag-non-breaking-findings.md +11 -0
  50. package/rules/long-horizon-autonomy.md +11 -0
  51. package/rules/no-contrast-framing.md +68 -0
  52. package/rules/review-closure-is-a-check.md +44 -0
  53. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  54. package/scripts/agent_merge_check.py +381 -0
  55. package/scripts/check.ps1 +12 -1
  56. package/scripts/ci/install-playtest.mjs +291 -0
  57. package/scripts/ci/install-playtest.test.mjs +84 -0
  58. package/scripts/ci/installer-lifecycle.mjs +11 -30
  59. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  60. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  61. package/scripts/ci/scratch-home-install.mjs +73 -0
  62. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  63. package/scripts/claude_account_choice.py +246 -0
  64. package/scripts/claude_account_profile.py +371 -0
  65. package/scripts/claude_chain_usage.py +49 -5
  66. package/scripts/codex_compat_materializer.py +33 -16
  67. package/scripts/contrast_framing.py +101 -0
  68. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  69. package/scripts/dev_env_scripts_constants/claude_account_constants.py +166 -0
  70. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  71. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  72. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  73. package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +21 -0
  74. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  75. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  76. package/scripts/durable_post_lint.py +30 -4
  77. package/scripts/followup_cli.py +6 -10
  78. package/scripts/merge_gate_checks.py +236 -0
  79. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  80. package/scripts/policy_lint/adapters.py +30 -0
  81. package/scripts/policy_lint/config/constants.py +2 -0
  82. package/scripts/policy_lint/registry.py +5 -0
  83. package/scripts/repository_checks/config/constants.py +12 -0
  84. package/scripts/repository_checks/config/policy_document.py +28 -2
  85. package/scripts/repository_checks/followups.py +54 -0
  86. package/scripts/repository_checks/match_exemptions.py +67 -0
  87. package/scripts/repository_checks/models.py +46 -2
  88. package/scripts/repository_checks/reporting.py +23 -7
  89. package/scripts/repository_checks/tracked_secrets.py +2 -2
  90. package/scripts/repository_policy.py +4 -2
  91. package/scripts/review_closure.py +134 -0
  92. package/scripts/review_closure_github.py +345 -0
  93. package/scripts/review_closure_model.py +360 -0
  94. package/scripts/test_agent_merge_check.py +317 -0
  95. package/scripts/test_claude_account_choice.py +217 -0
  96. package/scripts/test_claude_account_profile.py +220 -0
  97. package/scripts/test_claude_chain_usage.py +40 -0
  98. package/scripts/test_durable_post_lint.py +46 -0
  99. package/scripts/test_followup_cli.py +40 -0
  100. package/scripts/test_merge_gate_checks.py +92 -0
  101. package/scripts/tests/test_code_review_constants.py +78 -0
  102. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  103. package/scripts/tests/test_contrast_framing.py +117 -0
  104. package/scripts/tests/test_followups.py +88 -0
  105. package/scripts/tests/test_match_exemptions.py +291 -0
  106. package/scripts/tests/test_models.py +67 -2
  107. package/scripts/tests/test_package_inventory.py +6 -4
  108. package/scripts/tests/test_pester_suites_run.py +53 -0
  109. package/scripts/tests/test_policy_document.py +39 -1
  110. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  111. package/scripts/tests/test_reporting.py +18 -0
  112. package/scripts/tests/test_repository_policy.py +43 -1
  113. package/scripts/tests/test_review_closure.py +126 -0
  114. package/scripts/tests/test_review_closure_github.py +240 -0
  115. package/scripts/tests/test_review_closure_model.py +299 -0
  116. package/scripts/tests/test_tracked_secrets.py +42 -0
  117. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  118. package/scripts/repository_checks/email_exemptions.py +0 -39
  119. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -1,9 +1,9 @@
1
1
  """Constants for the session-edit stage gate hook trio.
2
2
 
3
3
  Shared by the PostToolUse tracker
4
- (``hooks/observability/session_file_edit_tracker.py``), the PreToolUse gate
5
- (``hooks/blocking/session_edit_stage_gate.py``), and the SessionStart cleanup
6
- (``hooks/session/session_edit_tracker_cleanup.py``): the per-session tracker
4
+ (``hooks/observability/session_file_edit_tracker.py``) and the SessionStart
5
+ cleanup (``hooks/session/session_edit_tracker_cleanup.py``): the per-session
6
+ tracker
7
7
  filename shape and JSON payload key, the session-id sanitize pattern, the
8
8
  SessionStart source key and the fresh-startup source value the cleanup keys its
9
9
  deletion on, the edit-tool name set, the per-session lock filename suffix and
@@ -62,7 +62,6 @@ ALL_TRACKED_UNSTAGED_FILES_COMMAND: tuple[str, ...] = (
62
62
 
63
63
  PARTIAL_COMMIT_BYPASS_MARKER: str = "# partial-commit"
64
64
  COMMIT_SUBCOMMAND_TOKEN: str = "commit"
65
- GIT_EXECUTABLE_TOKEN: str = "git"
66
65
  ENV_ASSIGNMENT_PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
67
66
  ALL_STAGING_SUBCOMMAND_TOKENS: frozenset[str] = frozenset({"add", "stage"})
68
67
  ALL_STAGE_ALL_ADD_FLAG_TOKENS: frozenset[str] = frozenset({"-A", "--all", "-u", "--update"})
@@ -40,9 +40,6 @@ ALL_COMMENT_TRANSITION_PATTERNS: list[Pattern[str]] = [
40
40
  compile(r"\bas of\b", IGNORECASE),
41
41
  ]
42
42
 
43
- BLOCK_COMMENT_OPEN_MARKER: str = "/*"
44
- BLOCK_COMMENT_CLOSE_MARKER: str = "*/"
45
-
46
43
  INLINE_CODE_PATTERN: Pattern[str] = compile(r"``[^`]+``|`[^`]+`")
47
44
  DOUBLE_QUOTED_SPAN_PATTERN: Pattern[str] = compile(r'"[^"\n]*"')
48
45
  TRIPLE_QUOTED_BLOCK_PATTERN: Pattern[str] = compile(
@@ -10,13 +10,15 @@ if str(_HOOKS_ROOT) not in sys.path:
10
10
  sys.path.insert(0, str(_HOOKS_ROOT))
11
11
 
12
12
  from blocking.state_description_blocker import _extract_comment_lines
13
+ from hooks_constants.code_rules_enforcer_constants import (
14
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER,
15
+ JAVASCRIPT_BLOCK_COMMENT_OPENER,
16
+ )
13
17
  from hooks_constants.state_description_blocker_constants import (
14
18
  ALL_BLOCK_COMMENT_EXTENSIONS,
15
19
  ALL_COMMENT_TRANSITION_PATTERNS,
16
20
  ALL_HASH_ONLY_EXTENSIONS,
17
21
  ALL_MARKDOWN_EXTENSIONS,
18
- BLOCK_COMMENT_CLOSE_MARKER,
19
- BLOCK_COMMENT_OPEN_MARKER,
20
22
  DOUBLE_QUOTE_BODY_GROUP,
21
23
  SINGLE_QUOTE_BODY_GROUP,
22
24
  TRIPLE_QUOTED_BLOCK_PATTERN,
@@ -47,19 +49,19 @@ def test_transition_patterns_do_not_match_unrelated_prose() -> None:
47
49
 
48
50
 
49
51
  def test_block_comment_markers_close_a_single_line_block_comment() -> None:
50
- text = f"{BLOCK_COMMENT_OPEN_MARKER} note {BLOCK_COMMENT_CLOSE_MARKER}\ncode();"
52
+ text = f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} note {JAVASCRIPT_BLOCK_COMMENT_CLOSER}\ncode();"
51
53
  all_comment_lines = _extract_comment_lines(text, ".js")
52
54
  assert all_comment_lines == [
53
- (1, f"{BLOCK_COMMENT_OPEN_MARKER} note {BLOCK_COMMENT_CLOSE_MARKER}")
55
+ (1, f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} note {JAVASCRIPT_BLOCK_COMMENT_CLOSER}")
54
56
  ]
55
57
 
56
58
 
57
59
  def test_block_comment_markers_carry_an_unclosed_comment_to_the_next_line() -> None:
58
- text = f"{BLOCK_COMMENT_OPEN_MARKER} still open\nclosed here {BLOCK_COMMENT_CLOSE_MARKER}"
60
+ text = f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} still open\nclosed here {JAVASCRIPT_BLOCK_COMMENT_CLOSER}"
59
61
  all_comment_lines = _extract_comment_lines(text, ".js")
60
62
  assert all_comment_lines == [
61
- (1, f"{BLOCK_COMMENT_OPEN_MARKER} still open"),
62
- (2, f"closed here {BLOCK_COMMENT_CLOSE_MARKER}"),
63
+ (1, f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} still open"),
64
+ (2, f"closed here {JAVASCRIPT_BLOCK_COMMENT_CLOSER}"),
63
65
  ]
64
66
 
65
67
 
@@ -48,27 +48,21 @@ TEST_PATH_PATTERNS: frozenset[str] = frozenset(
48
48
  HOOK_INFRASTRUCTURE_PATTERNS: frozenset[str] = frozenset(
49
49
  {
50
50
  "/.claude/hooks/",
51
- "\\.claude\\hooks\\",
52
- "\\.claude/hooks/",
53
51
  }
54
52
  )
55
53
 
56
54
  WORKFLOW_REGISTRY_PATTERNS: frozenset[str] = frozenset(
57
55
  {
58
56
  "/workflow/",
59
- "\\workflow\\",
60
57
  "_tab.py",
61
58
  "/states.py",
62
- "\\states.py",
63
59
  "/modules.py",
64
- "\\modules.py",
65
60
  }
66
61
  )
67
62
 
68
63
  MIGRATION_PATH_PATTERNS: frozenset[str] = frozenset(
69
64
  {
70
65
  "/migrations/",
71
- "\\migrations\\",
72
66
  }
73
67
  )
74
68
 
@@ -80,23 +74,14 @@ def is_test_file(file_path: str) -> bool:
80
74
 
81
75
  def is_hook_infrastructure(file_path: str) -> bool:
82
76
  path_normalized = file_path.lower().replace("\\", "/")
83
- return any(
84
- pattern.replace("\\", "/") in path_normalized
85
- for pattern in HOOK_INFRASTRUCTURE_PATTERNS
86
- )
77
+ return any(pattern in path_normalized for pattern in HOOK_INFRASTRUCTURE_PATTERNS)
87
78
 
88
79
 
89
80
  def is_workflow_registry_file(file_path: str) -> bool:
90
81
  path_normalized = file_path.lower().replace("\\", "/")
91
- return any(
92
- pattern.replace("\\", "/") in path_normalized
93
- for pattern in WORKFLOW_REGISTRY_PATTERNS
94
- )
82
+ return any(pattern in path_normalized for pattern in WORKFLOW_REGISTRY_PATTERNS)
95
83
 
96
84
 
97
85
  def is_migration_file(file_path: str) -> bool:
98
86
  path_normalized = file_path.lower().replace("\\", "/")
99
- return any(
100
- pattern.replace("\\", "/") in path_normalized
101
- for pattern in MIGRATION_PATH_PATTERNS
102
- )
87
+ return any(pattern in path_normalized for pattern in MIGRATION_PATH_PATTERNS)
@@ -14,6 +14,9 @@ _BLOCKING_DIR = str(Path(__file__).resolve().parent.parent / "blocking")
14
14
  if _BLOCKING_DIR not in sys.path:
15
15
  sys.path.insert(0, _BLOCKING_DIR)
16
16
 
17
+ import pytest # noqa: E402
18
+
19
+ from validators import exempt_paths # noqa: E402
17
20
  from validators.exempt_paths import is_config_file # noqa: E402
18
21
  from code_rules_path_utils import is_config_file as path_utils_is_config_file # noqa: E402
19
22
 
@@ -63,3 +66,20 @@ def test_is_config_file_is_identical_function_object_from_path_utils() -> None:
63
66
  assert is_config_file is path_utils_is_config_file, (
64
67
  "exempt_paths.is_config_file must be imported from code_rules_path_utils, not re-defined"
65
68
  )
69
+
70
+
71
+ @pytest.mark.parametrize(
72
+ "normalized_pattern_set_name",
73
+ [
74
+ "HOOK_INFRASTRUCTURE_PATTERNS",
75
+ "WORKFLOW_REGISTRY_PATTERNS",
76
+ "MIGRATION_PATH_PATTERNS",
77
+ ],
78
+ )
79
+ def test_normalized_pattern_set_carries_no_backslash_entry(
80
+ normalized_pattern_set_name: str,
81
+ ) -> None:
82
+ """Each set's consumer normalizes the path with .replace("\\\\", "/") but
83
+ not the pattern, so a backslash entry can never match."""
84
+ pattern_set = getattr(exempt_paths, normalized_pattern_set_name)
85
+ assert all("\\" not in each_pattern for each_pattern in pattern_set)
@@ -150,6 +150,18 @@ class TestPrintInCliEntryPoint:
150
150
  tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
151
151
  assert check_print_in_production(tree, "packages/foo/scripts/run_job.py") == []
152
152
 
153
+ def test_diagnostics_path_allows_print(self) -> None:
154
+ tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
155
+ assert check_print_in_production(tree, "theme_exports/tools/diagnostics/probe.py") == []
156
+
157
+ def test_windows_diagnostics_path_allows_print(self) -> None:
158
+ tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
159
+ assert check_print_in_production(tree, "theme_exports\\tools\\diagnostics\\probe.py") == []
160
+
161
+ def test_near_miss_diagnostics_name_still_flagged(self) -> None:
162
+ tree = ast.parse(NEAR_MISS_WITH_PRINT)
163
+ assert len(check_print_in_production(tree, "run_diagnostics.py")) == 1
164
+
153
165
  def test_near_miss_name_still_flagged(self) -> None:
154
166
  tree = ast.parse(NEAR_MISS_WITH_PRINT)
155
167
  assert len(check_print_in_production(tree, "publicli.py")) == 1
@@ -180,6 +192,8 @@ class TestCrossSurfaceConsistency:
180
192
  "stp_preview/renderer.py",
181
193
  "publicli.py",
182
194
  "packages/foo/scripts/run_job.py",
195
+ "theme_exports/tools/diagnostics/probe.py",
196
+ "run_diagnostics.py",
183
197
  ):
184
198
  production_allows = check_print_in_production(tree, path) == []
185
199
  library_allows = check_library_print(source, path) == []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.6.1",
3
+ "version": "8.8.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,54 @@
1
+ # The Agent Merges Its Own Green Pull Request
2
+
3
+ **When this applies:** Any pull request an agent opened or was asked to drive, once its checks report.
4
+
5
+ ## Rule
6
+
7
+ The agent that drives a pull request merges it. A pull request that is green, carries no open review thread, and sits at a merge state of `clean` is merged in the same run that brought it there. Waiting for the owner to type "merge" parks finished work on the person the work was done for.
8
+
9
+ Three things stay with the owner, and nothing else does:
10
+
11
+ - A pull request the owner asked to hold.
12
+ - A change the owner said they want to read first.
13
+ - A repository whose branch rule requires an approving review the agent cannot give.
14
+
15
+ Where the branch rule requires zero approvals and one status check, that check is the gate, and the agent merges on its verdict.
16
+
17
+ Validation is the precondition, and green means every check reported on the exact head commit. A branch rule that requires one status check names the floor a merge needs; a pull request whose other checks are red or still running is held until they report.
18
+
19
+ ## The precondition is mechanical
20
+
21
+ One command prints the verdict:
22
+
23
+ ```
24
+ python packages/claude-dev-env/scripts/agent_merge_check.py <owner>/<name> <number>
25
+ ```
26
+
27
+ It prints `MERGE` and exits 0 when the pull request is ready. It prints `HOLD` with the reason and exits 1 for a draft, for a head behind or conflicting with the base, for a required check that is not passing, for a check still running or red, and for an open review thread. It exits 2 when the state could not be read.
28
+
29
+ Each hold reason names its own repair, and each repair belongs to the agent:
30
+
31
+ | Hold | What the agent does |
32
+ |---|---|
33
+ | Head behind the base | Merge the base branch in and push |
34
+ | Head conflicts with the base | Merge the base branch in, resolve, push |
35
+ | A required check is red | Read the failing check, fix it, push |
36
+ | A check is red or still running | Fix it, or wait for it, then read the verdict again |
37
+ | A review thread is open | Answer it, push the fix, resolve the thread |
38
+ | The pull request is a draft | Mark it ready once the checks pass |
39
+
40
+ ## When a gate elsewhere holds the merge command
41
+
42
+ A session working inside another repository can sit behind that repository's own pre-merge gate, which reads the checkout the session works in and refuses a merge command whatever repository the pull request belongs to. That session hands the merge to the session that owns this repository's pull requests, by message, naming the pull request. The receiving session reads the verdict above and merges. The hand-off carries the work; it never lands on the owner.
43
+
44
+ ## After the merge
45
+
46
+ Delete nothing by hand. The repository deletes the head branch on merge.
47
+
48
+ ## Sibling rules
49
+
50
+ | Rule | Role |
51
+ |---|---|
52
+ | [`git-workflow.md`](git-workflow.md) | Draft first, and confirm each required context fired after the push |
53
+ | [`ci-owns-the-gate.md`](ci-owns-the-gate.md) | The gate runs once, and it runs on CI |
54
+ | [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
@@ -43,6 +43,16 @@ Record a row in the archive manifest for each file: why it went, the `git mv`
43
43
  that restores it, and every other edit a restore has to undo. A restore that has
44
44
  to be reconstructed from the diff is not recoverable in any useful sense.
45
45
 
46
+ ## Exempt files
47
+
48
+ The "Never archived" section of `rules-archived/ARCHIVE-MANIFEST.md` names the
49
+ files this procedure leaves alone. Read that section before a prune moves
50
+ anything. A pass that reaches an exempt file stops there and reports which file
51
+ it was, so the prune's other moves still land.
52
+
53
+ `rules/correction-lens.md` is on that list today. Editing an exempt file to
54
+ sharpen it is ordinary work.
55
+
46
56
  ## Make the archive stick
47
57
 
48
58
  Two things do not follow the file on their own.
@@ -27,6 +27,7 @@ BDD is the outer process and TDD is the inner loop: [`bdd.md`](bdd.md) discovers
27
27
 
28
28
  | Concern | Rule file |
29
29
  |---|---|
30
+ | Handling a correction from the user | [`correction-lens.md`](correction-lens.md) |
30
31
  | Task tracking / worker completion | [`workers-done-before-complete.md`](workers-done-before-complete.md) |
31
32
  | Multi-step task list | skill `task-build` (see agents catalog) |
32
33
 
@@ -0,0 +1,134 @@
1
+ # Correction Lens
2
+
3
+ **Standing rule. This file is never archived and never removed.** The permanence
4
+ clause at the end of this document states what that binds.
5
+
6
+ **When this applies:** Every correction the user gives. A "no, do it this way", a
7
+ "stop writing that word", a repeated review comment, a fix the user makes by hand
8
+ after an agent hands work back. Each one is evidence that a control is missing,
9
+ and the correction is handled by building that control.
10
+
11
+ ## Rule
12
+
13
+ A correction runs through five layers, in this order, and the lesson lands at the
14
+ highest layer that can hold it. The change opens in the same run as the
15
+ correction.
16
+
17
+ | Priority | Layer | What holds the lesson | The lesson lands as |
18
+ |---|---|---|---|
19
+ | 1 | Codebase | The mistake is impossible by how the code is written | A type, a signature, a data structure, an API shape, a deleted branch |
20
+ | 2 | Static analysis | A program reads the tree and decides | A lint rule, an enforcer check, a paired test, a CI gate |
21
+ | 3 | Review tooling | A reviewer or a review bot reads the criterion | A `CODE_RULES.md` row, a `.cursor/BUGBOT.md` pointer, a Graphite or BugBot rule |
22
+ | 4 | Skill | An agent follows a procedure | A skill under the agents home |
23
+ | 5 | Style guide | Word choice and prose shape | A row in a `rules/*.md` file or a style document |
24
+
25
+ Layer 1 is the goal every time. A lesson encoded there needs no reader, no run,
26
+ and no agent to remember it. The wrong call has nowhere to live.
27
+
28
+ ## Choosing the layer
29
+
30
+ Three sentences close every correction:
31
+
32
+ 1. The layer chosen.
33
+ 2. Why each higher layer cannot hold this lesson.
34
+ 3. The change opened at the chosen layer, by pull request.
35
+
36
+ A layer holds a lesson when its own test passes:
37
+
38
+ - **Layer 1 holds it** when a type, a signature, or a data structure can make the
39
+ wrong call fail to compile, fail to construct, or fail to exist.
40
+ - **Layer 2 holds it** when a program reading the tree can separate right from
41
+ wrong with no judgment call.
42
+ - **Layer 3 holds it** when a reader needs the criterion in front of them and a
43
+ program cannot decide it.
44
+ - **Layer 4 holds it** when the lesson is a procedure with steps an agent runs.
45
+ - **Layer 5 holds it** when the lesson is word choice or the shape of prose.
46
+
47
+ Effort rules out no layer. A layer is ruled out when it lacks the capability to
48
+ hold the lesson, and the sentence that rules it out says which capability is
49
+ missing. "A lint for this would take a day" leaves layer 2 in play.
50
+
51
+ Two layers often hold one correction. Take both: the shape at layer 1 and the
52
+ check at layer 2 cost one run together and each covers what the other misses.
53
+
54
+ ## A repeated correction moves up a layer
55
+
56
+ The same correction arriving a second time is the measurement. The layer chosen
57
+ the first time was too low. Move the lesson one layer up and say so in the same
58
+ run.
59
+
60
+ A review comment that repeats across pull requests reads the same way. Each
61
+ repetition names the layer below the reviewer as the one that needs the control.
62
+ Count the repetitions.
63
+
64
+ ## Where the control lands
65
+
66
+ A control lands in this package, so every session that loads this environment
67
+ carries it, on any machine, under any model. A control written into one
68
+ project's instructions reaches that project alone, and a control written into a
69
+ chat reply reaches that conversation alone.
70
+
71
+ Memory holds the decision. The control holds the behavior. A correction that
72
+ produced a memory file and nothing else has been recorded and never encoded, so
73
+ the rule still binds. Open the control.
74
+
75
+ ## Trust grows as the controls catch the common mistakes
76
+
77
+ Add agents after the controls catch what agents get wrong, and start from a
78
+ workflow small enough to watch. Each repeated correction becomes a stronger
79
+ control, and the number of agents rises behind it.
80
+ [`docs/high-trust-agent-delivery.md`](../docs/high-trust-agent-delivery.md)
81
+ carries the model this comes from, including the layered-controls picture and
82
+ the inner and outer delivery loops.
83
+
84
+ ## Permanence
85
+
86
+ This file stays in `rules/`. A prune pass, an archive sweep, a consolidation, or
87
+ a rewrite that would move it into `rules-archived/` stops at this line. The
88
+ "Never archived" section of
89
+ [`packages/claude-dev-env/rules-archived/ARCHIVE-MANIFEST.md`](../rules-archived/ARCHIVE-MANIFEST.md)
90
+ names it, and
91
+ [`archiving-agent-config.md`](archiving-agent-config.md) names the same
92
+ exemption from the archiving procedure's side.
93
+
94
+ An edit that sharpens this rule is welcome. The file stays.
95
+
96
+ ## Excerpt for repository-instruction sessions
97
+
98
+ Codex reads its repository `AGENTS.md`; this excerpt supplies the standalone
99
+ contract.
100
+
101
+ ```
102
+ Correction handling for this run, from rules/correction-lens.md.
103
+
104
+ Every correction the user gives becomes a control. Run it through five layers
105
+ and land it at the highest one that can hold it:
106
+
107
+ 1. Codebase. The mistake is impossible by how the code is written: a type, a
108
+ signature, a data structure, an API shape.
109
+ 2. Static analysis. A program reads the tree and decides: a lint rule, an
110
+ enforcer check, a paired test, a CI gate.
111
+ 3. Review tooling. A reviewer or a review bot reads the criterion: a code-rules
112
+ row, a BUGBOT pointer, a Graphite rule.
113
+ 4. Skill. An agent follows a procedure.
114
+ 5. Style guide. Word choice and prose shape.
115
+
116
+ Close the correction with three sentences: the layer chosen, why each higher
117
+ layer cannot hold the lesson, and the change opened at that layer in this run.
118
+ Effort rules out no layer; a missing capability does, and you name which one.
119
+
120
+ The same correction arriving twice means the layer was too low. Move it up one
121
+ layer and say so.
122
+
123
+ Land the control in the shared environment package, so every session carries
124
+ it. A memory file records the decision and encodes nothing.
125
+ ```
126
+
127
+ ## Sibling rules
128
+
129
+ | Rule | Role |
130
+ |---|---|
131
+ | [`flag-non-breaking-findings.md`](flag-non-breaking-findings.md) | A gate blocks on a breaking finding and records a smell |
132
+ | [`code-standards.md`](code-standards.md) | The layer map of contract, pointer, enforcer, lint, session rules |
133
+ | [`archiving-agent-config.md`](archiving-agent-config.md) | How a rule leaves service, and which files are exempt |
134
+ | [`falsify-before-green.md`](falsify-before-green.md) | A new layer-2 check counts once it has run red on a named break |
@@ -97,6 +97,17 @@ loading, so the gate fails. A non-canonical filename and a Git mode other than
97
97
  `SEVERITY_BY_RULE_ID` in that module is the whole declaration. Add a check,
98
98
  add its row.
99
99
 
100
+ `scripts/repository_policy.py` splits the committed-tree checks the same way.
101
+ `SEVERITY_BY_CHECK_ID` in `repository_checks/config/constants.py` carries one
102
+ row per check id, and a check id with no row reads as breaking. The
103
+ `package-inventory` check is the one smell there. A production file whose
104
+ package inventory omits its row still imports and still runs, so the check
105
+ prints its finding with an `advisory:` prefix, records it in the ledger, and
106
+ leaves the tree passing. Every other committed-tree check blocks: a `CLAUDE.md`
107
+ naming a file that does not exist, an env-var row naming a file that never
108
+ reads the variable, a test outside the testpaths allowlist, and a tracked
109
+ secret.
110
+
100
111
  ## Sibling rules
101
112
 
102
113
  | Rule | Role |
@@ -18,6 +18,17 @@ A pause is a request you deliver, not a state you enter. Put it in the channel t
18
18
 
19
19
  Before you end any turn, read your last paragraph. If it is a plan, an analysis, a list of next steps, or a statement of intent ("I'll run the tests", "next I'll wire it up"), do that work with tool calls before you stop. End the turn only when the task is done or you are blocked on input only the user can give.
20
20
 
21
+ Four turn endings leave owed work unstarted. Each one is a stop to remove:
22
+
23
+ - A summary of the work so far that closes by naming the next step, with no tool call to start it.
24
+ - An offer to continue unless the user objects.
25
+ - A list of decisions for the user when none of them blocks the remaining work.
26
+ - A pause chosen because the turn ran long or a milestone finished.
27
+
28
+ A status note or a recommendation on an open decision rides in the same message as the next tool call. The stops that stay are the two where nothing can move: the next step needs the user, or the blocker is a control that is protected from you on purpose. Confirmation before a risky or destructive action still applies.
29
+
30
+ A harness or routine that drives an unattended run reads a turn that ends in text as a report. When open checklist items remain and the turn names no blocker, it sends one short message naming those items. It stops after two or three such continuations on one task, so a stuck run ends where a reader can review it. [Prompting Claude Opus 5.5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5-5#unattended-agentic-runs) is the source for this list and this loop.
31
+
21
32
  In an autonomous pipeline the user cannot answer mid-task. For reversible actions that follow from the original request, act without asking; save any follow-up offers for after the task is done.
22
33
 
23
34
  Authority the task already granted stays granted. A later preference about tone, brevity, or reply format changes how you report, never what you are allowed to do. Re-asking for permission the task already gave hands the work back to the user, who then does it by hand.
@@ -0,0 +1,68 @@
1
+ # No Contrast Framing
2
+
3
+ **When this applies:** Every sentence a person reads. A chat reply, a commit
4
+ message, a pull request title or body, a review comment, an issue, a rule file,
5
+ a document, a plan, a memory file.
6
+
7
+ ## Rule
8
+
9
+ State what is true. Leave the rejected reading out.
10
+
11
+ A contrast defines its subject against something the writer already decided
12
+ against, so the reader carries two readings where one would do, and the second
13
+ one is the writer's own discarded draft. The sentence says more and shows less.
14
+
15
+ Six forms carry the whole ban. Each row names the form the check reports.
16
+
17
+ | Form | Shape | Write this |
18
+ |---|---|---|
19
+ | `trailing-comma-not` | `a diff regression, not shared infrastructure` | `a diff regression` |
20
+ | `corrective-it-is-not` | `it is not a flake, it is a bug` | `a bug`, then the evidence that shows it |
21
+ | `substitution-rather-than` | `park it rather than fix it` | `park it` |
22
+ | `additive-not-just` | `not just for today` | the point the sentence was building toward |
23
+ | `comparative-ranking` | `being precise matters more than trying to cover them` | `be precise about these three` |
24
+ | `substitution-as-opposed-to` | `a warning as opposed to a failure` | `a warning` |
25
+
26
+ A comparison of quantities stays. "The function runs more than 30 lines" counts
27
+ lines. The ban covers the comparison that ranks one course of action over
28
+ another the writer is turning down.
29
+
30
+ Three shapes stay quiet by design. A backticked span or a fenced block carries
31
+ an example, so the check blanks it first. A line opening with `>` quotes
32
+ someone else, whose words are theirs to write. A `, not` clause reports only
33
+ when no bare `not` already sits earlier in the line, so a list of clauses
34
+ reports its first `, not` and stays quiet on the rest, as in
35
+ `a diff regression, not shared code, not a workaround`. A list whose own
36
+ opening word is a bare `not`, as in "not in chat, not in a commit message, not
37
+ in a body", keeps even its first `, not` quiet. A release automation body
38
+ passes the post linter untouched, since the bot builds it from commit
39
+ subjects and reads it back as machine input.
40
+
41
+ When the sentence thins after the contrast comes out, the missing piece is
42
+ evidence. Name it: the failing check, the log line, the measured number, the
43
+ file and the line.
44
+
45
+ ## Where it is enforced
46
+
47
+ | Surface | What runs |
48
+ |---|---|
49
+ | Authored Markdown in the repository | The staged policy lint's `contrast-framing` rule, on every changed file, graded against the file's prior text |
50
+ | A pull request or issue title and body, and a comment | `scripts/durable_post_lint.py`, which every post passes through before it reaches GitHub |
51
+ | A chat reply | The writer, reading the sentence back before sending |
52
+
53
+ The pattern list lives in
54
+ `scripts/dev_env_scripts_constants/contrast_framing_constants.py`, and both
55
+ lints read that one list. A synchronization test requires a row in the table
56
+ above for every form the list carries.
57
+
58
+ A chat reply reaches no check, so the same list is what the writer reads the
59
+ sentence against: a comma followed by `not`, a `rather than`, a `not just`, a
60
+ ranking of one thing over another.
61
+
62
+ ## Sibling rules
63
+
64
+ | Rule | Role |
65
+ |---|---|
66
+ | [`asd-ste100-language.md`](asd-ste100-language.md) | Plain word choice, sentence style, and tone |
67
+ | [`correction-lens.md`](correction-lens.md) | Every correction becomes a control at the highest layer that can hold it |
68
+ | [`research-mode.md`](research-mode.md) | A claim carries its source |
@@ -0,0 +1,44 @@
1
+ # Review Closure Is a Check
2
+
3
+ **When this applies:** Any pull request an agent drives, from the first review comment on it to the merge.
4
+
5
+ ## Rule
6
+
7
+ A review finding on the head is answered before the pull request merges. The agent driving it replies, pushes the fix, or both. The check named `Review closure` reads that state on every push and on every review event, and reports red while a finding waits.
8
+
9
+ One command prints the same verdict:
10
+
11
+ ```
12
+ python packages/claude-dev-env/scripts/review_closure.py <owner>/<name> <number>
13
+ ```
14
+
15
+ It prints `CLOSED` and exits 0 when every finding on the head is answered. It prints `OPEN`, one line per waiting finding, and exits 1. It exits 2 when the state could not be read.
16
+
17
+ ## What closes a finding
18
+
19
+ | The thread | Closed by |
20
+ |---|---|
21
+ | A review comment on code | A push that replaced the code it points at |
22
+ | A review comment on code | A reply from the account driving the pull request |
23
+ | A review comment on code | Resolution, where the comment carries no red circle |
24
+ | A red-circle finding | A reply from the driving account, or a push that replaced the code |
25
+ | A thread the driving account opened | Itself |
26
+ | A blocking `Claude Approvals` row | A push, which moves the head the check reports on |
27
+
28
+ A red circle marks a finding a review states as blocking, so resolution in silence leaves it open. The reply says what changed or why the finding stands, and the reviewer reads it beside the diff.
29
+
30
+ The driving account is the one that opened the pull request. Where the agent comments under a second login, `--driver-login <login>` names it, repeatably.
31
+
32
+ ## Where the check runs
33
+
34
+ `.github/workflows/review-closure.yml` runs it here on a push to a pull request, on a submitted or dismissed review, and on a review comment. Each run reports on the pull request's head commit, so a finding posted after the last push still turns the check red.
35
+
36
+ `Echo-Visuals-Inc/python-automation` runs the same command from its own workflow, against the revision of this package that its workflow pins.
37
+
38
+ ## Sibling rules
39
+
40
+ | Rule | Role |
41
+ |---|---|
42
+ | [`agent-merges-its-own-green-pull-request.md`](agent-merges-its-own-green-pull-request.md) | The agent that drives a pull request merges it once its gate passes |
43
+ | [`git-workflow.md`](git-workflow.md) | Draft first, and confirm each required context fired after the push |
44
+ | [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ Run every Pester suite under a tests directory and exit on its verdict.
5
+
6
+ .DESCRIPTION
7
+ check.ps1 calls this script as a child process so Invoke-Tool reads a
8
+ process exit code the way it does for ruff, mypy and pytest. Pester is a
9
+ cmdlet and leaves $LASTEXITCODE alone, so running it in-process would
10
+ report every suite as passing.
11
+
12
+ A missing Pester module exits non-zero. A gate that cannot run its check
13
+ has not passed it, and a skip here would leave the suites reporting
14
+ nothing while the tree reads as covered.
15
+
16
+ .PARAMETER TestsRoot
17
+ Directory holding the *.Tests.ps1 suites.
18
+
19
+ .OUTPUTS
20
+ Pester's own detailed output. Exit code 0 when every suite passes.
21
+ #>
22
+ [CmdletBinding()]
23
+ param(
24
+ [Parameter(Mandatory = $true)]
25
+ [string]$TestsRoot
26
+ )
27
+
28
+ $ErrorActionPreference = 'Stop'
29
+
30
+ if (-not (Get-Module -ListAvailable -Name Pester)) {
31
+ Write-Error "Pester is not installed, so the suites under $TestsRoot cannot run."
32
+ exit 1
33
+ }
34
+
35
+ Import-Module Pester -MinimumVersion 5.0
36
+
37
+ $suiteConfiguration = New-PesterConfiguration
38
+ $suiteConfiguration.Run.Path = $TestsRoot
39
+ $suiteConfiguration.Run.Exit = $true
40
+ $suiteConfiguration.Output.Verbosity = 'Detailed'
41
+
42
+ Invoke-Pester -Configuration $suiteConfiguration