claude-dev-env 2.5.0 → 2.7.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 (139) hide show
  1. package/CLAUDE.md +20 -57
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  12. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  13. package/agents/CLAUDE.md +1 -1
  14. package/agents/code-verifier.md +36 -7
  15. package/bin/codex-compat.mjs +104 -0
  16. package/bin/codex-compat.test.mjs +51 -0
  17. package/codex-capability-map.json +13 -0
  18. package/docs/CODE_RULES.md +2 -0
  19. package/docs/codex-compatibility.md +25 -0
  20. package/docs/nas-ssh-invocation.md +96 -12
  21. package/docs/references/code-review-enforcement.md +31 -6
  22. package/hooks/blocking/CLAUDE.md +3 -0
  23. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  24. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  25. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  26. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  27. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  28. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  29. package/hooks/blocking/state_description_blocker.py +1 -1
  30. package/hooks/blocking/stop_dispatcher.py +1 -1
  31. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  32. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  33. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  34. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  35. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  36. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  37. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  38. package/hooks/git-hooks/CLAUDE.md +3 -0
  39. package/hooks/git-hooks/conftest.py +30 -0
  40. package/hooks/git-hooks/gate_utils.py +2 -2
  41. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  42. package/hooks/git-hooks/pre_push.py +75 -4
  43. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  44. package/hooks/git-hooks/test_config.py +0 -15
  45. package/hooks/git-hooks/test_gate_utils.py +3 -15
  46. package/hooks/git-hooks/test_pre_commit.py +1 -15
  47. package/hooks/git-hooks/test_pre_push.py +236 -27
  48. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  49. package/hooks/hooks.json +0 -12
  50. package/hooks/hooks_constants/CLAUDE.md +5 -1
  51. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  52. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  53. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  54. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  55. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  56. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  57. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  58. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  59. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  60. package/package.json +4 -2
  61. package/rules/CLAUDE.md +17 -23
  62. package/rules/agent-spawn-protocol.md +6 -6
  63. package/rules/anti-corollary-tests.md +1 -1
  64. package/rules/bdd.md +1 -1
  65. package/rules/cleanup-temp-files.md +10 -4
  66. package/rules/code-standards.md +7 -0
  67. package/rules/conservative-action.md +1 -5
  68. package/rules/context7.md +0 -4
  69. package/rules/destructive-commands.md +47 -0
  70. package/rules/doc-inventory-integrity.md +48 -0
  71. package/rules/doc-prose-cuts.md +58 -0
  72. package/rules/docstring-prose-matches-implementation.md +10 -2
  73. package/rules/durable-post-artifacts.md +0 -4
  74. package/rules/eli11-replies.md +31 -0
  75. package/rules/explore-thoroughly.md +4 -4
  76. package/rules/falsify-before-green.md +68 -0
  77. package/rules/file-global-constants.md +1 -1
  78. package/rules/filesystem-search.md +51 -0
  79. package/rules/gh-cli-conventions.md +27 -0
  80. package/rules/git-workflow.md +26 -0
  81. package/rules/hedging-claims.md +9 -0
  82. package/rules/long-horizon-autonomy.md +0 -4
  83. package/rules/measurement-denominators.md +48 -0
  84. package/rules/nas-ssh-invocation.md +23 -5
  85. package/rules/parallel-tools.md +2 -2
  86. package/rules/plain-illustrative-docstrings.md +3 -7
  87. package/rules/plain-language.md +2 -0
  88. package/rules/proof-of-work-pr-comments.md +0 -4
  89. package/rules/re-stage-before-commit.md +2 -0
  90. package/rules/research-mode.md +10 -0
  91. package/rules/shell-invocation.md +21 -0
  92. package/rules/testing.md +4 -0
  93. package/rules/verified-commit-gate-skip.md +3 -27
  94. package/rules/verify-before-asking.md +5 -0
  95. package/rules/windows-filesystem-safe.md +1 -1
  96. package/rules/workers-done-before-complete.md +4 -0
  97. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  98. package/scripts/codex_capability_bridge.py +171 -0
  99. package/scripts/codex_compat_materializer.py +1087 -0
  100. package/scripts/codex_compat_watcher.py +502 -0
  101. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  102. package/scripts/invoke_code_review.py +11 -4
  103. package/scripts/sync_to_cursor/rules.py +0 -10
  104. package/scripts/test_invoke_code_review.py +143 -0
  105. package/scripts/test_invoke_code_review_chain.py +1 -1
  106. package/scripts/test_invoke_code_review_contract.py +1 -1
  107. package/scripts/tests/test_code_review_constants.py +80 -0
  108. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  109. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  110. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  111. package/scripts/tests/test_sync_to_cursor.py +0 -1
  112. package/skills/autoconverge/workflow/converge.mjs +1 -1
  113. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  114. package/skills/condensing-instructions/SKILL.md +42 -51
  115. package/skills/fresh-branch/CLAUDE.md +1 -1
  116. package/skills/fresh-branch/SKILL.md +5 -6
  117. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  118. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  119. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  120. package/skills/orchestrator/SKILL.md +23 -9
  121. package/skills/orchestrator-refresh/SKILL.md +20 -1
  122. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  123. package/skills/session-log/SKILL.md +1 -1
  124. package/rules/claude-md-orphan-file.md +0 -28
  125. package/rules/cleanup-command-forms.md +0 -23
  126. package/rules/code-reviews.md +0 -11
  127. package/rules/env-var-table-code-drift.md +0 -10
  128. package/rules/gh-body-file.md +0 -5
  129. package/rules/gh-paginate.md +0 -3
  130. package/rules/hook-prose-matches-detector.md +0 -15
  131. package/rules/no-historical-clutter.md +0 -26
  132. package/rules/no-inline-destructive-literals.md +0 -9
  133. package/rules/no-justification-noise.md +0 -61
  134. package/rules/package-inventory-stale-entry.md +0 -25
  135. package/rules/right-sized-engineering.md +0 -28
  136. package/rules/self-contained-docs.md +0 -17
  137. package/rules/shell-invocation-policy.md +0 -5
  138. package/rules/state-what-is.md +0 -25
  139. package/rules/tdd.md +0 -7
@@ -0,0 +1,502 @@
1
+ """Safe, optional link planning and debounced compatibility reconciliation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import filecmp
6
+ import os
7
+ import shutil
8
+ import stat
9
+ import threading
10
+ import tempfile
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Callable, Iterable, Literal
15
+
16
+ from codex_compat_materializer import (
17
+ MaterializerConfig,
18
+ MaterializerError,
19
+ build_plan,
20
+ publish_plan,
21
+ validate_target_path,
22
+ )
23
+
24
+ LinkKind = Literal["symlink", "junction", "copy", "materialize"]
25
+ LinkClassification = Literal["byte_compatible", "source_only", "unsupported"]
26
+
27
+
28
+ class _WatcherConfiguration:
29
+ materialize_link_kind = "materialize"
30
+ copy_link_kind = "copy"
31
+ symlink_link_kind = "symlink"
32
+ junction_link_kind = "junction"
33
+ all_configured_root_names = ("agents", "commands", "hooks", "rules", "skills")
34
+ all_unsafe_event_kinds = {"overflow", "ambiguous", "inaccessible", "error", "unknown"}
35
+ reparse_point_attribute = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class LinkSpec:
40
+ source: str
41
+ target: str
42
+ is_byte_compatible: bool = True
43
+ classification: LinkClassification | None = None
44
+ requested_kind: LinkKind = "symlink"
45
+ should_activate_hook: bool = False
46
+ should_activate_script: bool = False
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class LinkDecision:
51
+ source: str
52
+ target: str
53
+ kind: LinkKind
54
+ action: str
55
+ reason: str
56
+
57
+
58
+ @dataclass
59
+ class LinkPlan:
60
+ decisions: list[LinkDecision] = field(default_factory=list)
61
+ errors: list[str] = field(default_factory=list)
62
+ is_dry_run: bool = True
63
+
64
+
65
+ @dataclass
66
+ class LinkReport:
67
+ decisions: list[LinkDecision] = field(default_factory=list)
68
+ applied: int = 0
69
+ skipped: int = 0
70
+ errors: list[str] = field(default_factory=list)
71
+ is_reconcile_required: bool = False
72
+ reconcile_reasons: list[str] = field(default_factory=list)
73
+ reconcile_paths: list[str] = field(default_factory=list)
74
+
75
+ @property
76
+ def reconcile_required(self) -> bool:
77
+ return self.is_reconcile_required
78
+
79
+ @reconcile_required.setter
80
+ def reconcile_required(self, is_required: bool) -> None:
81
+ self.is_reconcile_required = is_required
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class LinkCapabilities:
86
+ symlink: bool
87
+ junction: bool
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class WatchEvent:
92
+ source_root: str
93
+ kind: str = "changed"
94
+ is_self_event: bool = False
95
+ path: str = ""
96
+
97
+
98
+ def probe_link_capabilities(
99
+ symlink_probe: Callable[[], bool] | None = None,
100
+ junction_probe: Callable[[], bool] | None = None,
101
+ should_allow_temporary_probe: bool = False,
102
+ ) -> LinkCapabilities:
103
+ """Probe link support while preserving a conservative default.
104
+
105
+ Args:
106
+ symlink_probe: Optional injected symlink capability probe.
107
+ junction_probe: Optional injected junction capability probe.
108
+ should_allow_temporary_probe: Whether injected probes may be executed.
109
+
110
+ Returns:
111
+ The observed capabilities, or unavailable capabilities by default.
112
+
113
+ """
114
+ if not should_allow_temporary_probe:
115
+ return LinkCapabilities(False, False)
116
+ if symlink_probe is None or junction_probe is None:
117
+ return LinkCapabilities(False, False)
118
+ return LinkCapabilities(
119
+ symlink_probe(),
120
+ junction_probe(),
121
+ )
122
+
123
+
124
+ def _select_kind(spec: LinkSpec, capabilities: LinkCapabilities) -> tuple[LinkKind, str]:
125
+ classification = spec.classification or ("byte_compatible" if spec.is_byte_compatible else "source_only")
126
+ if classification == "source_only":
127
+ return _WatcherConfiguration.materialize_link_kind, "converted asset requires materializer"
128
+ if classification == "unsupported":
129
+ return _WatcherConfiguration.materialize_link_kind, "unsupported asset cannot be linked"
130
+ if spec.should_activate_hook or spec.should_activate_script:
131
+ return _WatcherConfiguration.copy_link_kind, "activation is prohibited"
132
+ if spec.requested_kind == _WatcherConfiguration.junction_link_kind and capabilities.junction:
133
+ return _WatcherConfiguration.junction_link_kind, "requested capability available"
134
+ if spec.requested_kind in (_WatcherConfiguration.junction_link_kind, _WatcherConfiguration.symlink_link_kind) and capabilities.symlink:
135
+ return _WatcherConfiguration.symlink_link_kind, "requested capability unavailable; symlink available"
136
+ return _WatcherConfiguration.copy_link_kind, "link capability unavailable"
137
+
138
+
139
+ def _validate_source_path(source_root: Path, source_name: str) -> Path:
140
+ normalized_source = source_name.replace("\\", "/")
141
+ if Path(normalized_source).is_absolute():
142
+ raise ValueError("rooted path is not allowed")
143
+ return validate_target_path(source_root, source_name)
144
+
145
+
146
+ def _validate_source_root(source_root: Path) -> Path:
147
+ """Reject link-like roots and return the canonical source root."""
148
+ expanded_root = source_root.expanduser()
149
+ if expanded_root.is_symlink() or (
150
+ expanded_root.exists() and _is_reparse_point(expanded_root)
151
+ ):
152
+ raise ValueError("source_root_path cannot be a symlink or reparse point")
153
+ return expanded_root.resolve()
154
+
155
+
156
+ def _validate_publication_source(source_path: Path) -> None:
157
+ if source_path.is_symlink():
158
+ raise ValueError("publication source cannot be a symlink or reparse point")
159
+ if not source_path.exists():
160
+ raise ValueError("publication source does not exist")
161
+ if _is_reparse_point(source_path):
162
+ raise ValueError("publication source cannot be a symlink or reparse point")
163
+ if not source_path.is_dir():
164
+ return
165
+ for each_path in source_path.rglob("*"):
166
+ if each_path.is_symlink() or _is_reparse_point(each_path):
167
+ raise ValueError("publication source tree contains a symlink or reparse point")
168
+
169
+
170
+ def build_link_plan(
171
+ all_specs: Iterable[LinkSpec],
172
+ source_root: Path,
173
+ target_root: Path,
174
+ capabilities: LinkCapabilities | None = None,
175
+ should_apply: bool = False,
176
+ should_allow_links: bool = False,
177
+ should_allow_activation: bool = False,
178
+ symlink_probe: Callable[[], bool] | None = None,
179
+ junction_probe: Callable[[], bool] | None = None,
180
+ should_allow_temporary_probe: bool = False,
181
+ ) -> LinkPlan:
182
+ """Build a relative-path-only plan without changing the filesystem.
183
+
184
+ Args:
185
+ all_specs: Link specifications to classify.
186
+ source_root: Source assets are read beneath this source_root_path.
187
+ target_root: Root receiving managed targets.
188
+ capabilities: Optional precomputed link capabilities.
189
+ should_apply: Whether eligible decisions may be authorized.
190
+ should_allow_links: Whether link publication is authorized.
191
+ should_allow_activation: Whether hook or script activation is authorized.
192
+ symlink_probe: Optional injected symlink capability probe.
193
+ junction_probe: Optional injected junction capability probe.
194
+ should_allow_temporary_probe: Whether injected probes may be executed.
195
+
196
+ Returns:
197
+ A plan containing decisions and validation errors.
198
+
199
+ Raises:
200
+ ValueError: If a source path is rooted.
201
+ """
202
+ plan = LinkPlan(is_dry_run=not should_apply)
203
+ try:
204
+ source_base = _validate_source_root(source_root)
205
+ except ValueError as error:
206
+ plan.errors.append(str(error))
207
+ return plan
208
+ target_base = target_root.expanduser().resolve()
209
+ if source_base == target_base or source_base in target_base.parents or target_base in source_base.parents:
210
+ plan.errors.append("source and target roots overlap")
211
+ return plan
212
+ available = capabilities or probe_link_capabilities(
213
+ symlink_probe=symlink_probe,
214
+ junction_probe=junction_probe,
215
+ should_allow_temporary_probe=should_allow_temporary_probe,
216
+ )
217
+ all_seen_targets: set[str] = set()
218
+ for each_spec in all_specs:
219
+ source_name = each_spec.source
220
+ try:
221
+ _validate_source_path(source_base, source_name)
222
+ except ValueError as error:
223
+ if Path(source_name.replace("\\", "/")).is_absolute():
224
+ raise
225
+ plan.errors.append(str(error))
226
+ continue
227
+ try:
228
+ target_name = each_spec.target
229
+ target_path = validate_target_path(target_base, target_name)
230
+ target_key = target_path.as_posix().casefold()
231
+ if target_key in all_seen_targets or target_path.exists() or target_path.is_symlink():
232
+ raise ValueError("unmanaged target collision")
233
+ all_seen_targets.add(target_key)
234
+ selected_kind, reason = _select_kind(each_spec, available)
235
+ is_linkable = (each_spec.classification or ("byte_compatible" if each_spec.is_byte_compatible else "source_only")) == "byte_compatible"
236
+ can_apply = is_linkable and should_apply and should_allow_links and (should_allow_activation or not (each_spec.should_activate_hook or each_spec.should_activate_script))
237
+ plan.decisions.append(LinkDecision(source_name, target_name, selected_kind, "apply" if can_apply else "report", reason))
238
+ except (OSError, ValueError, MaterializerError) as error:
239
+ plan.errors.append(str(error))
240
+ return plan
241
+
242
+
243
+ def _apply_decision(
244
+ decision: LinkDecision,
245
+ source_root: Path,
246
+ target_root: Path,
247
+ copier: Callable[[Path, Path], None],
248
+ symlink_creator: Callable[[Path, Path, bool], None],
249
+ junction_creator: Callable[[Path, Path], None],
250
+ ) -> None:
251
+ source_path = _validate_source_path(source_root, decision.source)
252
+ target_path = validate_target_path(target_root, decision.target)
253
+ if decision.kind == "symlink":
254
+ symlink_creator(source_path, target_path, source_path.is_dir())
255
+ return
256
+ if decision.kind == "junction":
257
+ junction_creator(source_path, target_path)
258
+ return
259
+ copier(source_path, target_path)
260
+
261
+
262
+ def _report_unsupported_junction(source_path: Path, target_path: Path) -> None:
263
+ raise RuntimeError("Windows junction creation is unsupported without an injected junction creator")
264
+
265
+
266
+ def _is_reparse_point(file_path: Path) -> bool:
267
+ file_stat = file_path.stat(follow_symlinks=False)
268
+ return bool(
269
+ getattr(file_stat, "st_file_attributes", 0)
270
+ & _WatcherConfiguration.reparse_point_attribute
271
+ )
272
+
273
+
274
+ def _validate_copy_source(source_path: Path) -> None:
275
+ if source_path.is_symlink() or _is_reparse_point(source_path):
276
+ raise ValueError("copy source cannot be a symlink or reparse point")
277
+ if source_path.is_file():
278
+ return
279
+ if not source_path.is_dir():
280
+ raise ValueError("copy source must be a regular file or directory")
281
+ for each_path in source_path.rglob("*"):
282
+ if each_path.is_symlink() or _is_reparse_point(each_path):
283
+ raise ValueError("copy source tree contains a symlink or reparse point")
284
+ if not each_path.is_file() and not each_path.is_dir():
285
+ raise ValueError("copy source tree contains a non-regular entry")
286
+
287
+
288
+ def _verify_copy(source_path: Path, target_path: Path) -> bool:
289
+ if source_path.is_file():
290
+ return target_path.is_file() and filecmp.cmp(source_path, target_path, shallow=False)
291
+ if not target_path.is_dir():
292
+ return False
293
+ all_source_files = {each_path.relative_to(source_path) for each_path in source_path.rglob("*") if each_path.is_file()}
294
+ all_target_files = {each_path.relative_to(target_path) for each_path in target_path.rglob("*") if each_path.is_file()}
295
+ return all_source_files == all_target_files and all(
296
+ filecmp.cmp(source_path / each_file, target_path / each_file, shallow=False)
297
+ for each_file in all_source_files
298
+ )
299
+
300
+
301
+ def _copy_asset(source_path: Path, target_path: Path) -> None:
302
+ _validate_copy_source(source_path)
303
+ target_path.parent.mkdir(parents=True, exist_ok=True)
304
+ if target_path.exists() or target_path.is_symlink():
305
+ raise FileExistsError(target_path)
306
+ with tempfile.TemporaryDirectory(dir=target_path.parent) as staging_directory:
307
+ staging_path = Path(staging_directory) / target_path.name
308
+ if source_path.is_file():
309
+ shutil.copy2(source_path, staging_path)
310
+ else:
311
+ shutil.copytree(source_path, staging_path, symlinks=False)
312
+ if not _verify_copy(source_path, staging_path):
313
+ raise OSError("copied asset failed content verification")
314
+ os.rename(staging_path, target_path)
315
+
316
+
317
+ def publish_link_plan(
318
+ plan: LinkPlan,
319
+ source_root: Path,
320
+ target_root: Path,
321
+ copier: Callable[[Path, Path], None] | None = None,
322
+ symlink_creator: Callable[[Path, Path, bool], None] | None = None,
323
+ junction_creator: Callable[[Path, Path], None] | None = None,
324
+ materializer: Callable[[], object] | None = None,
325
+ ) -> LinkReport:
326
+ """Publish explicitly authorized decisions and report failures safely.
327
+
328
+ Args:
329
+ plan: Decisions produced by ``build_link_plan``.
330
+ source_root: Source assets are read beneath this source_root_path.
331
+ target_root: Root receiving managed targets.
332
+ copier: Optional injected copy operation.
333
+ symlink_creator: Optional injected symlink operation.
334
+ junction_creator: Optional injected junction operation.
335
+ materializer: Optional callback for materialization decisions.
336
+
337
+ Returns:
338
+ A report of applied, skipped, and failed decisions.
339
+
340
+ Raises:
341
+ ValueError: If a decision contains an unsafe rooted path.
342
+ """
343
+ report = LinkReport(list(plan.decisions), errors=list(plan.errors), is_reconcile_required=bool(plan.errors))
344
+ try:
345
+ source_base = _validate_source_root(source_root)
346
+ except ValueError as error:
347
+ report.errors.append(str(error))
348
+ report.is_reconcile_required = True
349
+ return report
350
+ copy_asset = copier or _copy_asset
351
+ create_symlink = symlink_creator or os.symlink
352
+ create_junction = junction_creator or _report_unsupported_junction
353
+ for each_decision in plan.decisions:
354
+ if each_decision.action != "apply":
355
+ report.skipped += 1
356
+ continue
357
+ try:
358
+ source_path = _validate_source_path(source_base, each_decision.source)
359
+ _validate_publication_source(source_path)
360
+ if each_decision.kind == _WatcherConfiguration.materialize_link_kind:
361
+ if materializer is None:
362
+ raise RuntimeError("materializer callback is required")
363
+ materializer()
364
+ else:
365
+ _apply_decision(each_decision, source_base, target_root, copy_asset, create_symlink, create_junction)
366
+ report.applied += 1
367
+ except (OSError, RuntimeError, ValueError) as error:
368
+ report.errors.append(str(error))
369
+ report.is_reconcile_required = True
370
+ report.reconcile_reasons.append(type(error).__name__)
371
+ report.reconcile_paths.append(each_decision.target)
372
+ return report
373
+
374
+
375
+ class DebouncedReconciler:
376
+ """Coalesce scoped watcher events and serialize reconciliation callbacks.
377
+
378
+ The reconciler accepts events from five configured source roots, suppresses
379
+ self-generated events first, and adds sorted unsafe-event details to reports.
380
+ """
381
+
382
+ default_debounce_seconds = 0.25
383
+
384
+ def __init__(
385
+ self,
386
+ reconcile: Callable[[], object],
387
+ debounce_seconds: float | None = None,
388
+ all_configured_roots: Iterable[str] | None = None,
389
+ ) -> None:
390
+ self._reconcile = reconcile
391
+ self._debounce_seconds = (
392
+ debounce_seconds
393
+ if debounce_seconds is not None
394
+ else self.default_debounce_seconds
395
+ )
396
+ self._lock = threading.Lock()
397
+ self._pending = False
398
+ self._last_event = 0.0
399
+ self._running = False
400
+ self._stopped = False
401
+ self._reconcile_reasons: list[str] = []
402
+ self._reconcile_paths: list[str] = []
403
+ self._configured_roots = self._normalize_configured_roots(
404
+ all_configured_roots or _WatcherConfiguration.all_configured_root_names
405
+ )
406
+
407
+ @staticmethod
408
+ def _normalize_configured_roots(all_configured_roots: Iterable[str]) -> frozenset[str]:
409
+ normalized_roots: set[str] = set()
410
+ for each_root in all_configured_roots:
411
+ normalized_roots.add(each_root.casefold())
412
+ normalized_roots.add(Path(each_root).expanduser().resolve().as_posix().casefold())
413
+ return frozenset(normalized_roots)
414
+
415
+ def start(self) -> None:
416
+ with self._lock:
417
+ self._stopped = False
418
+
419
+ def stop(self) -> None:
420
+ with self._lock:
421
+ self._stopped = True
422
+ self._pending = False
423
+ self._reconcile_reasons.clear()
424
+ self._reconcile_paths.clear()
425
+
426
+ def notify(self, event: WatchEvent) -> None:
427
+ """Record a scoped filesystem event for later reconciliation.
428
+
429
+ Args:
430
+ event: Filesystem event to coalesce or classify.
431
+ """
432
+ if event.is_self_event:
433
+ return
434
+ source_root_name = event.source_root.casefold()
435
+ source_root_path = Path(event.source_root).expanduser().resolve().as_posix().casefold()
436
+ if source_root_name not in self._configured_roots and source_root_path not in self._configured_roots:
437
+ return
438
+ if event.kind in _WatcherConfiguration.all_unsafe_event_kinds:
439
+ with self._lock:
440
+ if not self._stopped:
441
+ self._pending = True
442
+ self._reconcile_reasons.append(event.kind)
443
+ self._reconcile_paths.append(event.path or event.source_root)
444
+ self._last_event = time.monotonic()
445
+ return
446
+ with self._lock:
447
+ if not self._stopped:
448
+ self._pending = True
449
+ self._last_event = time.monotonic()
450
+
451
+ def flush(self, should_force: bool = False) -> object | None:
452
+ """Run the pending reconciliation when debounce and state allow it.
453
+
454
+ Args:
455
+ should_force: Whether to bypass the debounce interval.
456
+
457
+ Returns:
458
+ The reconciliation report, or ``None`` when no run is eligible.
459
+ """
460
+ with self._lock:
461
+ if self._stopped or not self._pending or self._running or (not should_force and time.monotonic() - self._last_event < self._debounce_seconds):
462
+ return None
463
+ self._pending = False
464
+ self._running = True
465
+ try:
466
+ reconciliation_report = self._reconcile()
467
+ return self._report_with_event_details(reconciliation_report)
468
+ except (OSError, RuntimeError, ValueError) as error:
469
+ return self._report_with_event_details({"reconcile_required": True, "errors": [str(error)]})
470
+ finally:
471
+ with self._lock:
472
+ self._running = False
473
+
474
+ def _report_with_event_details(self, reconciliation_report: object) -> object:
475
+ with self._lock:
476
+ event_reasons = sorted(self._reconcile_reasons)
477
+ event_paths = sorted(self._reconcile_paths)
478
+ self._reconcile_reasons.clear()
479
+ self._reconcile_paths.clear()
480
+ if isinstance(reconciliation_report, dict):
481
+ reconciliation_report = dict(reconciliation_report)
482
+ if event_reasons:
483
+ reconciliation_report["reconcile_required"] = True
484
+ reconciliation_report["reconcile_reasons"] = event_reasons
485
+ reconciliation_report["reconcile_paths"] = event_paths
486
+ return reconciliation_report
487
+ if not event_reasons:
488
+ return reconciliation_report
489
+ return {"reconciliation": reconciliation_report, "reconcile_required": True, "reconcile_reasons": event_reasons, "reconcile_paths": event_paths}
490
+
491
+
492
+ def reconcile_materialized_agents(materializer_config: MaterializerConfig) -> object:
493
+ """Run the materializer's complete deterministic public reconciliation.
494
+
495
+ Args:
496
+ materializer_config: Configuration for the materialized compatibility files.
497
+
498
+ Returns:
499
+ The materializer publication report.
500
+ """
501
+ planned_files, publication_report = build_plan(materializer_config)
502
+ return publish_plan(materializer_config, planned_files, publication_report)
@@ -15,10 +15,35 @@ Scalar flags, JSON keys, and mint-loop messages live here for the invoker.
15
15
  from __future__ import annotations
16
16
 
17
17
  import importlib.util
18
+ import os
18
19
  import sys
19
20
  from pathlib import Path
20
21
  from types import ModuleType
21
22
 
23
+ ROOT_EFFECTIVE_USER_ID: int = 0
24
+ """Effective user id the review binary treats as root."""
25
+
26
+ NON_ROOT_EFFECTIVE_USER_ID: int = 1000
27
+ """Effective user id stood in for a platform that reports no user id."""
28
+
29
+
30
+ def _read_effective_user_id() -> int:
31
+ """Read this process's effective user id, or a non-root stand-in.
32
+
33
+ Windows exposes no ``os.geteuid``, so reading it there raises and the
34
+ whole constants module fails to import — taking the review invoker with
35
+ it. A caller on such a platform is never the root the review binary
36
+ refuses, so it reads as an ordinary caller.
37
+ """
38
+ read_effective_user_id = getattr(os, "geteuid", None)
39
+ if read_effective_user_id is None:
40
+ return NON_ROOT_EFFECTIVE_USER_ID
41
+ return int(read_effective_user_id())
42
+
43
+
44
+ IS_ROOT_CALLER: bool = _read_effective_user_id() == ROOT_EFFECTIVE_USER_ID
45
+ """Whether this process runs as root, which limits the permission modes."""
46
+
22
47
 
23
48
  def _load_enforcement_constants_module() -> ModuleType:
24
49
  """Load the hooks enforcement constants by explicit file path.
@@ -80,6 +105,18 @@ PERMISSION_MODE_FLAG: str = "--permission-mode"
80
105
  PERMISSION_MODE_BYPASS: str = "bypassPermissions"
81
106
  """Permission-mode value that auto-approves tools for unattended chain runs."""
82
107
 
108
+ PERMISSION_MODE_ACCEPT_EDITS: str = "acceptEdits"
109
+ """Permission mode asked for when the caller is root.
110
+
111
+ The review binary refuses ``bypassPermissions`` outright for a root caller,
112
+ so asking for it there means no review runs and no stamp is ever minted.
113
+ """
114
+
115
+ REVIEW_PERMISSION_MODE: str = (
116
+ PERMISSION_MODE_ACCEPT_EDITS if IS_ROOT_CALLER else PERMISSION_MODE_BYPASS
117
+ )
118
+ """Permission mode this caller asks the review binary for."""
119
+
83
120
  MODE_IN_SESSION: str = "in_session"
84
121
  """Result mode when the host is Claude and the session already runs opus."""
85
122
 
@@ -8,7 +8,8 @@ Mode decision::
8
8
  host=ThirdParty, any model -> mode chain
9
9
 
10
10
  Chain mode runs ``run_claude`` with argv from ``build_code_review_arguments``
11
- (single-turn prompt, model opus, json output, bypassPermissions).
11
+ (single-turn prompt, model opus, json output, and the permission mode this
12
+ caller is allowed to ask the review binary for).
12
13
 
13
14
  cwd is the PR working tree and stdin is redirected from the empty stream so
14
15
  the spawn does not wait for interactive input. Result JSON on stdout only::
@@ -80,7 +81,7 @@ from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
80
81
  MAXIMUM_STAMP_MINT_PASSES,
81
82
  MODE_CHAIN,
82
83
  MODE_IN_SESSION,
83
- PERMISSION_MODE_BYPASS,
84
+ REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
84
85
  PERMISSION_MODE_FLAG,
85
86
  RECORD_STAMP_FLAG,
86
87
  RESULT_KEY_BOUND_HASH,
@@ -418,6 +419,7 @@ def is_code_review_clean_stamp_allowed(review_outcome: CodeReviewOutcome) -> boo
418
419
  return False
419
420
  return True
420
421
 
422
+
421
423
  def _run_claude_with_empty_stdin(
422
424
  all_claude_arguments: list[str],
423
425
  *,
@@ -495,6 +497,7 @@ def _failure_code_review_outcome(returncode: int) -> CodeReviewOutcome:
495
497
  is_dirty_tree=False,
496
498
  )
497
499
 
500
+
498
501
  def _run_chain_review(
499
502
  *,
500
503
  working_directory: Path,
@@ -507,6 +510,8 @@ def _run_chain_review(
507
510
  timeout_seconds=timeout_seconds,
508
511
  working_directory=working_directory,
509
512
  )
513
+ if chain_outcome.returncode != SUCCESSFUL_REVIEW_RETURNCODE and chain_outcome.stderr:
514
+ sys.stderr.write(chain_outcome.stderr.strip() + "\n")
510
515
  return _chain_outcome(chain_outcome, working_directory=working_directory)
511
516
 
512
517
 
@@ -869,9 +874,11 @@ def _mint_or_config_outcome(
869
874
  timeout_seconds=timeout_seconds,
870
875
  effort=effort,
871
876
  )
872
- except ChainConfigurationError:
877
+ except ChainConfigurationError as configuration_error:
878
+ sys.stderr.write(str(configuration_error) + "\n")
873
879
  return _no_mint_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
874
- except ValueError:
880
+ except ValueError as host_profile_error:
881
+ sys.stderr.write(str(host_profile_error) + "\n")
875
882
  return _no_mint_outcome(HOST_PROFILE_ERROR_RETURNCODE)
876
883
 
877
884
 
@@ -59,7 +59,6 @@ def _strip_code_standards_blockquote(markdown: str) -> str:
59
59
  _merged_mapping_key_order = (
60
60
  "code-standards",
61
61
  "tasklings-preferences",
62
- "right-sized-engineering",
63
62
  "bdd",
64
63
  "test-quality",
65
64
  "research-mode",
@@ -350,15 +349,6 @@ def _always_apply_mappings(rules_directory: Path, docs_directory: Path) -> tuple
350
349
  "Core code standards: naming, types, config, hook-enforced rules",
351
350
  "merge_code_standards",
352
351
  ),
353
- RuleMapping(
354
- "right-sized-engineering",
355
- (rules_directory / "right-sized-engineering.md",),
356
- "right-sized-engineering.mdc",
357
- True,
358
- None,
359
- "Right-sized engineering and complexity budget",
360
- "verbatim",
361
- ),
362
352
  RuleMapping(
363
353
  "bdd",
364
354
  (rules_directory / "bdd.md",),