claude-dev-env 2.7.1 → 2.9.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 (127) hide show
  1. package/CLAUDE.md +7 -1
  2. package/_shared/CLAUDE.md +1 -0
  3. package/_shared/advisor/advisor-protocol.md +19 -9
  4. package/_shared/pr-loop/audit-contract.md +4 -4
  5. package/_shared/pr-loop/precatch-rubric.md +2 -2
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/clean-coder.md +9 -19
  13. package/agents/code-quality-agent.md +6 -5
  14. package/agents/deep-research.md +7 -24
  15. package/agents/docs-agent.md +1 -27
  16. package/agents/issue-tracker.md +1 -7
  17. package/agents/skill-writer-agent.md +1 -2
  18. package/agents/test_agent_frontmatter.py +335 -12
  19. package/docs/CODE_RULES.md +4 -2
  20. package/docs/references/CLAUDE.md +2 -2
  21. package/docs/references/advisor-tool.md +44 -6
  22. package/docs/references/team-advisor-skill.md +14 -8
  23. package/hooks/blocking/CLAUDE.md +2 -0
  24. package/hooks/blocking/fable_spawn_gate.py +187 -0
  25. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  26. package/hooks/blocking/plain_language_blocker.py +287 -15
  27. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  28. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  29. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  30. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  31. package/hooks/git-hooks/CLAUDE.md +2 -2
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  33. package/hooks/git-hooks/pre_push.py +343 -54
  34. package/hooks/git-hooks/test_pre_push.py +852 -6
  35. package/hooks/hooks.json +9 -19
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  39. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  40. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  41. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  43. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  46. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  47. package/output-styles/CLAUDE.md +17 -0
  48. package/output-styles/caveman-agent.md +37 -0
  49. package/package.json +2 -1
  50. package/rules/CLAUDE.md +1 -0
  51. package/rules/ask-user-question-required.md +26 -0
  52. package/rules/claims-as-quotes.md +65 -0
  53. package/rules/code-standards.md +33 -7
  54. package/rules/eli11-replies.md +1 -1
  55. package/scripts/CLAUDE.md +3 -3
  56. package/scripts/_code_review_test_support.py +6 -0
  57. package/scripts/check.ps1 +18 -5
  58. package/scripts/claude_chain_runner.py +203 -31
  59. package/scripts/codec_forwarding_test_support.py +2 -0
  60. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  61. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  62. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  63. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  64. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
  65. package/scripts/grok_headless_runner.py +16 -83
  66. package/scripts/grok_patch_artifacts.py +123 -0
  67. package/scripts/grok_run_ledger.py +318 -0
  68. package/scripts/spawn_grok_batch.py +591 -10
  69. package/scripts/test_claude_chain_runner.py +358 -0
  70. package/scripts/test_grok_headless_runner.py +18 -63
  71. package/scripts/test_grok_patch_artifacts.py +82 -0
  72. package/scripts/test_grok_run_ledger.py +116 -0
  73. package/scripts/test_invoke_code_review.py +298 -0
  74. package/scripts/test_resolve_worker_spawn.py +6 -0
  75. package/scripts/test_spawn_grok_batch.py +396 -0
  76. package/scripts/tests/CLAUDE.md +1 -0
  77. package/scripts/tests/test_grok_worker_constants.py +59 -0
  78. package/skills/CLAUDE.md +4 -2
  79. package/skills/_shared/CLAUDE.md +37 -4
  80. package/skills/_shared/advisor/CLAUDE.md +9 -0
  81. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  82. package/skills/_shared/advisor/scripts/README.md +9 -0
  83. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  84. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  85. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  86. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  87. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  88. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  89. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  90. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  91. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  92. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  93. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  94. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  95. package/skills/_shared/pr-loop/state-schema.md +5 -0
  96. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  98. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  99. package/skills/autoconverge/workflow/converge.mjs +66 -33
  100. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  101. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  102. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  103. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  104. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  105. package/skills/e-code-review/SKILL.md +15 -9
  106. package/skills/e-code-review/reference/fix.md +29 -7
  107. package/skills/e-code-review/reference/loop.md +230 -14
  108. package/skills/e-code-review/reference/low.md +33 -15
  109. package/skills/e-code-review/reference/medium.md +55 -21
  110. package/skills/e-code-review/reference/runner-selection.md +40 -0
  111. package/skills/e-code-review/reference/xhigh.md +30 -12
  112. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  113. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  114. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  115. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  116. package/skills/fresh-branch/CLAUDE.md +5 -5
  117. package/skills/fresh-branch/SKILL.md +14 -6
  118. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  119. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  120. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  121. package/skills/grok-spawn/SKILL.md +10 -0
  122. package/skills/orchestrator/SKILL.md +9 -1
  123. package/skills/orchestrator-refresh/SKILL.md +5 -1
  124. package/skills/task-build/reference/tool-routing.md +3 -0
  125. package/skills/team-advisor/SKILL.md +23 -41
  126. package/system-prompts/software-engineer.xml +6 -3
  127. package/skills/test_markdown_link_integrity.py +0 -107
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ """Grok medium-review discovery and verification orchestration.
3
+
4
+ Dispatches exactly eight named finder angles at one target head, binds each
5
+ finder to an isolated identity (worktree path, leader socket, advisor session),
6
+ deduplicates candidates, verifies them, and retains CONFIRMED plus unresolved
7
+ PLAUSIBLE findings with severity. Head drift rejects the whole batch.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+
14
+ from e_code_review_scripts_constants.grok_code_review_constants import (
15
+ ALL_MEDIUM_FINDER_ANGLES,
16
+ ALL_SEVERITIES,
17
+ ALL_VERIFICATION_VERDICTS,
18
+ MEDIUM_REVIEW_FINDER_COUNT,
19
+ MEDIUM_REVIEW_SCHEMA_VERSION,
20
+ VERDICT_CONFIRMED,
21
+ VERDICT_PLAUSIBLE,
22
+ VERDICT_REFUTED,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FinderCandidate:
28
+ """One candidate finding from a discovery angle."""
29
+
30
+ angle: str
31
+ file_path: str
32
+ line_number: int
33
+ mechanism: str
34
+ scenario: str
35
+ worktree_path: str
36
+ leader_socket: str
37
+ advisor_session_id: str
38
+ reviewed_head: str
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class VerifiedFinding:
43
+ """A retained finding after verification."""
44
+
45
+ file_path: str
46
+ line_number: int
47
+ mechanism: str
48
+ scenario: str
49
+ verdict: str
50
+ severity: str
51
+ reviewed_head: str
52
+
53
+
54
+ @dataclass
55
+ class MediumReviewBatch:
56
+ """Result of one medium-review run at a single head."""
57
+
58
+ schema_version: str = MEDIUM_REVIEW_SCHEMA_VERSION
59
+ target_head: str = ""
60
+ diff_base: str = ""
61
+ all_finder_angles: tuple[str, ...] = ALL_MEDIUM_FINDER_ANGLES
62
+ all_retained_findings: list[VerifiedFinding] = field(default_factory=list)
63
+ is_rejected: bool = False
64
+ rejection_reason: str | None = None
65
+
66
+
67
+ def require_exact_finder_set(all_angles: tuple[str, ...] | list[str]) -> None:
68
+ """Require exactly the eight named finder angles.
69
+
70
+ Args:
71
+ all_angles: Angle names present in a run.
72
+
73
+ Raises:
74
+ ValueError: When the set is not exactly the eight angles.
75
+ """
76
+ angle_set = set(all_angles)
77
+ expected = set(ALL_MEDIUM_FINDER_ANGLES)
78
+ if angle_set != expected or len(all_angles) != MEDIUM_REVIEW_FINDER_COUNT:
79
+ raise ValueError(
80
+ f"finder angles must be exactly {MEDIUM_REVIEW_FINDER_COUNT}: "
81
+ f"got {sorted(angle_set)}"
82
+ )
83
+
84
+
85
+ def deduplicate_candidates(
86
+ all_candidates: list[FinderCandidate],
87
+ ) -> list[FinderCandidate]:
88
+ """Keep the most concrete scenario per file/line/mechanism.
89
+
90
+ Args:
91
+ all_candidates: Raw finder outputs.
92
+
93
+ Returns:
94
+ Deduplicated candidates preserving the longest scenario text.
95
+ """
96
+ best_by_key: dict[tuple[str, int, str], FinderCandidate] = {}
97
+ for each_candidate in all_candidates:
98
+ key = (
99
+ each_candidate.file_path,
100
+ each_candidate.line_number,
101
+ each_candidate.mechanism,
102
+ )
103
+ existing = best_by_key.get(key)
104
+ if existing is None or len(each_candidate.scenario) > len(existing.scenario):
105
+ best_by_key[key] = each_candidate
106
+ return list(best_by_key.values())
107
+
108
+
109
+ def retain_verified_findings(
110
+ *,
111
+ all_candidates: list[FinderCandidate],
112
+ verdict_by_key: dict[tuple[str, int, str], str],
113
+ severity_by_key: dict[tuple[str, int, str], str],
114
+ ) -> list[VerifiedFinding]:
115
+ """Retain CONFIRMED and unresolved PLAUSIBLE findings with severity.
116
+
117
+ Args:
118
+ all_candidates: Deduplicated candidates.
119
+ verdict_by_key: Verification verdict per file/line/mechanism.
120
+ severity_by_key: Severity per file/line/mechanism.
121
+
122
+ Returns:
123
+ Retained findings.
124
+
125
+ Raises:
126
+ ValueError: When a retained finding lacks a legal severity or verdict.
127
+ """
128
+ all_retained: list[VerifiedFinding] = []
129
+ for each_candidate in all_candidates:
130
+ key = (
131
+ each_candidate.file_path,
132
+ each_candidate.line_number,
133
+ each_candidate.mechanism,
134
+ )
135
+ verdict = verdict_by_key.get(key)
136
+ if verdict not in ALL_VERIFICATION_VERDICTS:
137
+ raise ValueError(f"missing or illegal verdict for {key}")
138
+ if verdict == VERDICT_REFUTED:
139
+ continue
140
+ if verdict not in {VERDICT_CONFIRMED, VERDICT_PLAUSIBLE}:
141
+ continue
142
+ severity = severity_by_key.get(key)
143
+ if severity not in ALL_SEVERITIES:
144
+ raise ValueError(f"missing or illegal severity for {key}")
145
+ all_retained.append(
146
+ VerifiedFinding(
147
+ file_path=each_candidate.file_path,
148
+ line_number=each_candidate.line_number,
149
+ mechanism=each_candidate.mechanism,
150
+ scenario=each_candidate.scenario,
151
+ verdict=verdict,
152
+ severity=severity,
153
+ reviewed_head=each_candidate.reviewed_head,
154
+ )
155
+ )
156
+ return all_retained
157
+
158
+
159
+ def run_medium_review(
160
+ *,
161
+ target_head: str,
162
+ diff_base: str,
163
+ all_finder_candidates: list[FinderCandidate],
164
+ verdict_by_key: dict[tuple[str, int, str], str],
165
+ severity_by_key: dict[tuple[str, int, str], str],
166
+ live_head: str,
167
+ is_any_advisor_blocked: bool = False,
168
+ ) -> MediumReviewBatch:
169
+ """Run discovery and verification for one medium-review head.
170
+
171
+ Args:
172
+ target_head: Reviewed head OID.
173
+ diff_base: Diff base OID.
174
+ all_finder_candidates: Outputs from the eight finder angles.
175
+ verdict_by_key: Verification results keyed by file/line/mechanism.
176
+ severity_by_key: Severities for retained findings.
177
+ live_head: Live head OID; must match target_head.
178
+ is_any_advisor_blocked: True when any finder ended advisor_blocked.
179
+
180
+ Raises:
181
+ ValueError: When finder angles are incomplete or duplicated.
182
+
183
+ Returns:
184
+ The medium-review batch (possibly rejected).
185
+ """
186
+ batch = MediumReviewBatch(target_head=target_head, diff_base=diff_base)
187
+ if is_any_advisor_blocked:
188
+ batch.is_rejected = True
189
+ batch.rejection_reason = "advisor_blocked"
190
+ return batch
191
+ if live_head != target_head:
192
+ batch.is_rejected = True
193
+ batch.rejection_reason = "head_drift"
194
+ return batch
195
+ for each_candidate in all_finder_candidates:
196
+ if each_candidate.reviewed_head != target_head:
197
+ batch.is_rejected = True
198
+ batch.rejection_reason = "finder_head_mismatch"
199
+ return batch
200
+ all_angles = [each.angle for each in all_finder_candidates]
201
+ all_sockets = {each.leader_socket for each in all_finder_candidates}
202
+ all_sessions = {each.advisor_session_id for each in all_finder_candidates}
203
+ all_worktrees = {each.worktree_path for each in all_finder_candidates}
204
+ if (
205
+ len(all_sockets) != len(all_finder_candidates)
206
+ or len(all_sessions) != len(all_finder_candidates)
207
+ or len(all_worktrees) != len(all_finder_candidates)
208
+ ):
209
+ batch.is_rejected = True
210
+ batch.rejection_reason = "non_unique_finder_identity"
211
+ return batch
212
+ require_exact_finder_set(tuple(sorted(set(all_angles))))
213
+ if len(set(all_angles)) != MEDIUM_REVIEW_FINDER_COUNT:
214
+ raise ValueError("each finder angle must run exactly once")
215
+ deduped = deduplicate_candidates(all_finder_candidates)
216
+ batch.all_retained_findings = retain_verified_findings(
217
+ all_candidates=deduped,
218
+ verdict_by_key=verdict_by_key,
219
+ severity_by_key=severity_by_key,
220
+ )
221
+ return batch
@@ -0,0 +1,212 @@
1
+ """Behavioral tests for Grok medium-review discovery and verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS))
13
+
14
+ from e_code_review_scripts_constants.grok_code_review_constants import ( # noqa: E402
15
+ ALL_MEDIUM_FINDER_ANGLES,
16
+ MEDIUM_REVIEW_FINDER_COUNT,
17
+ VERDICT_CONFIRMED,
18
+ VERDICT_PLAUSIBLE,
19
+ VERDICT_REFUTED,
20
+ )
21
+ from grok_code_review import ( # noqa: E402
22
+ FinderCandidate,
23
+ deduplicate_candidates,
24
+ require_exact_finder_set,
25
+ retain_verified_findings,
26
+ run_medium_review,
27
+ )
28
+
29
+
30
+ def _candidate(
31
+ angle: str,
32
+ *,
33
+ file_path: str = "a.py",
34
+ line_number: int = 1,
35
+ mechanism: str = "m",
36
+ scenario: str = "short",
37
+ suffix: str = "",
38
+ head: str = "head1",
39
+ ) -> FinderCandidate:
40
+ token = suffix or angle
41
+ return FinderCandidate(
42
+ angle=angle,
43
+ file_path=file_path,
44
+ line_number=line_number,
45
+ mechanism=mechanism,
46
+ scenario=scenario,
47
+ worktree_path=f"/wt/{token}",
48
+ leader_socket=f"sock-{token}",
49
+ advisor_session_id=f"adv-{token}",
50
+ reviewed_head=head,
51
+ )
52
+
53
+
54
+ def test_require_exact_finder_set_accepts_all_angles() -> None:
55
+ require_exact_finder_set(ALL_MEDIUM_FINDER_ANGLES)
56
+ with pytest.raises(ValueError):
57
+ require_exact_finder_set(ALL_MEDIUM_FINDER_ANGLES[:-1])
58
+
59
+
60
+ def test_deduplicate_keeps_most_concrete_scenario() -> None:
61
+ short = _candidate("correctness", scenario="short")
62
+ long = _candidate("security", scenario="much more concrete failure path")
63
+ # same key different angles - force same file/line/mech
64
+ short = FinderCandidate(
65
+ angle="correctness",
66
+ file_path="a.py",
67
+ line_number=3,
68
+ mechanism="null",
69
+ scenario="x",
70
+ worktree_path="/wt/a",
71
+ leader_socket="s1",
72
+ advisor_session_id="a1",
73
+ reviewed_head="h",
74
+ )
75
+ long = FinderCandidate(
76
+ angle="security",
77
+ file_path="a.py",
78
+ line_number=3,
79
+ mechanism="null",
80
+ scenario="concrete long scenario",
81
+ worktree_path="/wt/b",
82
+ leader_socket="s2",
83
+ advisor_session_id="a2",
84
+ reviewed_head="h",
85
+ )
86
+ kept = deduplicate_candidates([short, long])
87
+ assert len(kept) == 1
88
+ assert kept[0].scenario == "concrete long scenario"
89
+
90
+
91
+ def test_run_medium_review_happy_path() -> None:
92
+ head = "abc123"
93
+ all_finders = [
94
+ _candidate(each_angle, head=head, suffix=each_angle)
95
+ for each_angle in ALL_MEDIUM_FINDER_ANGLES
96
+ ]
97
+ # make unique keys per angle
98
+ all_finders = [
99
+ FinderCandidate(
100
+ angle=each_angle,
101
+ file_path=f"{each_angle}.py",
102
+ line_number=1,
103
+ mechanism="m",
104
+ scenario=f"scenario {each_angle}",
105
+ worktree_path=f"/wt/{each_angle}",
106
+ leader_socket=f"sock-{each_angle}",
107
+ advisor_session_id=f"adv-{each_angle}",
108
+ reviewed_head=head,
109
+ )
110
+ for each_angle in ALL_MEDIUM_FINDER_ANGLES
111
+ ]
112
+ verdicts = {
113
+ (f"{each}.py", 1, "m"): VERDICT_CONFIRMED
114
+ for each in ALL_MEDIUM_FINDER_ANGLES
115
+ }
116
+ severities = {
117
+ (f"{each}.py", 1, "m"): "medium" for each in ALL_MEDIUM_FINDER_ANGLES
118
+ }
119
+ # refute one
120
+ first = ALL_MEDIUM_FINDER_ANGLES[0]
121
+ verdicts[(f"{first}.py", 1, "m")] = VERDICT_REFUTED
122
+ batch = run_medium_review(
123
+ target_head=head,
124
+ diff_base="base",
125
+ all_finder_candidates=all_finders,
126
+ verdict_by_key=verdicts,
127
+ severity_by_key=severities,
128
+ live_head=head,
129
+ )
130
+ assert batch.is_rejected is False
131
+ assert len(batch.all_finder_angles) == MEDIUM_REVIEW_FINDER_COUNT
132
+ assert all(each.reviewed_head == head for each in batch.all_retained_findings)
133
+ assert all(each.verdict in {VERDICT_CONFIRMED, VERDICT_PLAUSIBLE} for each in batch.all_retained_findings)
134
+ assert len(batch.all_retained_findings) == MEDIUM_REVIEW_FINDER_COUNT - 1
135
+
136
+
137
+ def test_head_drift_rejects_batch() -> None:
138
+ head = "h1"
139
+ all_finders = [
140
+ FinderCandidate(
141
+ angle=each_angle,
142
+ file_path=f"{each_angle}.py",
143
+ line_number=1,
144
+ mechanism="m",
145
+ scenario="s",
146
+ worktree_path=f"/wt/{each_angle}",
147
+ leader_socket=f"sock-{each_angle}",
148
+ advisor_session_id=f"adv-{each_angle}",
149
+ reviewed_head=head,
150
+ )
151
+ for each_angle in ALL_MEDIUM_FINDER_ANGLES
152
+ ]
153
+ batch = run_medium_review(
154
+ target_head=head,
155
+ diff_base="b",
156
+ all_finder_candidates=all_finders,
157
+ verdict_by_key={},
158
+ severity_by_key={},
159
+ live_head="other",
160
+ )
161
+ assert batch.is_rejected is True
162
+ assert batch.rejection_reason == "head_drift"
163
+
164
+
165
+ def test_advisor_blocked_rejects_batch() -> None:
166
+ head = "h1"
167
+ all_finders = [
168
+ FinderCandidate(
169
+ angle=each_angle,
170
+ file_path=f"{each_angle}.py",
171
+ line_number=1,
172
+ mechanism="m",
173
+ scenario="s",
174
+ worktree_path=f"/wt/{each_angle}",
175
+ leader_socket=f"sock-{each_angle}",
176
+ advisor_session_id=f"adv-{each_angle}",
177
+ reviewed_head=head,
178
+ )
179
+ for each_angle in ALL_MEDIUM_FINDER_ANGLES
180
+ ]
181
+ batch = run_medium_review(
182
+ target_head=head,
183
+ diff_base="b",
184
+ all_finder_candidates=all_finders,
185
+ verdict_by_key={},
186
+ severity_by_key={},
187
+ live_head=head,
188
+ is_any_advisor_blocked=True,
189
+ )
190
+ assert batch.is_rejected is True
191
+ assert batch.rejection_reason == "advisor_blocked"
192
+
193
+
194
+ def test_retain_verified_findings_keeps_confirmed() -> None:
195
+ candidate = FinderCandidate(
196
+ angle="correctness",
197
+ file_path="a.py",
198
+ line_number=2,
199
+ mechanism="null",
200
+ scenario="boom",
201
+ worktree_path="/wt/x",
202
+ leader_socket="s",
203
+ advisor_session_id="a",
204
+ reviewed_head="h",
205
+ )
206
+ kept = retain_verified_findings(
207
+ all_candidates=[candidate],
208
+ verdict_by_key={("a.py", 2, "null"): VERDICT_CONFIRMED},
209
+ severity_by_key={("a.py", 2, "null"): "high"},
210
+ )
211
+ assert len(kept) == 1
212
+ assert kept[0].verdict == VERDICT_CONFIRMED
@@ -1,14 +1,14 @@
1
1
  # fresh-branch
2
2
 
3
- Creates a new branch from fresh-fetched `origin/main` inside an isolated worktree under `<repo-root>/.claude/worktrees/<agent>/<branch-name>`. Does not push, open a PR, or run `checkout -b` in the caller tree.
3
+ Creates a new branch from fresh-fetched `origin/main` inside an isolated worktree under a configured root. Default: `<repo-root>/.claude/worktrees/<agent>/<branch-name>`. Optional absolute `--worktree-root` relocates the root (agent and branch still nest under it). Relative roots and path escape fail closed. Does not push, open a PR, or run `checkout -b` in the caller tree.
4
4
 
5
5
  ## Key files
6
6
 
7
7
  | File | Purpose |
8
8
  |---|---|
9
- | `SKILL.md` | Phases, checklist, execute-vs-read for the CLI, gotchas |
10
- | `scripts/create_fresh_branch.py` | Deterministic CLI: fetch base, `git worktree add -b`, JSON stdout |
9
+ | `SKILL.md` | Phases, checklist, configured-root contract, execute-vs-read for the CLI, gotchas |
10
+ | `scripts/create_fresh_branch.py` | Deterministic CLI: resolve configured root (fail closed before fetch), fetch base, `git worktree add -b`, JSON stdout |
11
11
  | `scripts/fresh_branch_git_commands.py` | Git command helpers: fetch, ref checks, `git worktree add -b --no-track` |
12
- | `scripts/test_create_fresh_branch.py` | Behavioral tests with temporary git repos |
12
+ | `scripts/test_create_fresh_branch.py` | Behavioral tests with temporary git repos (default root, explicit root, escape, collisions) |
13
13
  | `scripts/test_fresh_branch_git_commands.py` | Behavioral tests for the git command helpers |
14
- | `scripts/fresh_branch_scripts_constants/` | Constants package (`fresh_branch_cli_constants`) for the CLI |
14
+ | `scripts/fresh_branch_scripts_constants/` | Constants package (`fresh_branch_cli_constants`) for CLI flags and error strings |
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: fresh-branch
3
3
  description: >-
4
- Fresh git branch from origin/main in an isolated worktree under the repo's .claude/worktrees/ (never checkout -b in the caller tree).
4
+ Fresh git branch from origin/main in an isolated worktree under a configured root (default: <repo>/.claude/worktrees/<agent>/; never checkout -b in the caller tree).
5
5
  Triggers: fresh branch, new branch from main, /fresh-branch, start fresh, clean branch off main,
6
6
  worktree branch, branch in temp.
7
7
  ---
8
8
 
9
9
  # fresh-branch
10
10
 
11
- Creates a new branch from a fresh-fetched `origin/main` inside an isolated git worktree under the repository's `.claude/worktrees/<agent>/` root. Shared primitive: other skills invoke `/fresh-branch` when they need a clean branch without touching the caller's dirty tree.
11
+ Creates a new branch from a fresh-fetched `origin/main` inside an isolated git worktree under a **configured isolated root**. Default root is the repository's `.claude/worktrees/`; pass `--worktree-root` for an absolute alternate root (including outside the repo). Every worktree path must resolve under that root. Shared primitive: other skills invoke `/fresh-branch` when they need a clean branch without touching the caller's dirty tree.
12
12
 
13
13
  **Announce at start:** "Creating a fresh branch from origin/main."
14
14
 
@@ -62,15 +62,21 @@ Optional flags:
62
62
  | Flag | Role |
63
63
  |------|------|
64
64
  | `--repo <path>` | Source repo (default: current directory) |
65
- | `--agent <slug>` | Worktree-root segment: `claude`, `grok`, `cursor`, `codex`, … |
65
+ | `--agent <slug>` | Agent segment under the configured root: `claude`, `grok`, `cursor`, `codex`, … |
66
66
  | `--base <ref>` | Base ref (default: `origin/main`) |
67
+ | `--worktree-root <abs-path>` | Absolute isolated root for all worktrees (default: `<repo-root>/.claude/worktrees`) |
67
68
 
68
69
  Agent resolution inside the script: `--agent` → `FRESH_BRANCH_AGENT` env → host markers → `claude`.
69
70
 
70
- Worktree path:
71
+ Configured-root contract:
71
72
 
72
- - `<repo-root>/.claude/worktrees/<agent>/<branch-name>`, on every platform
73
+ - Default configured root: `<repo-root>/.claude/worktrees`
74
+ - Explicit `--worktree-root` must be absolute; a relative value fails closed with a deterministic error before any git fetch
75
+ - Absolute roots outside the repository are legal; the agent and branch still nest under that root
76
+ - Worktree path: `<configured-root>/<agent>/<branch-name>`, on every platform
77
+ - Every allocated path must resolve under the configured root; traversal that escapes fails closed
73
78
  - If the path exists, the script suffixes `-2`, `-3`, …
79
+ - Permission-rule cleanup (`stale_worktree_rule_sweep`) only walks `~/.claude/worktrees`; external `--worktree-root` trees are outside that sweep until a follow-up ships
74
80
 
75
81
  On exit 0, stdout is one JSON object:
76
82
 
@@ -100,10 +106,12 @@ Further edits for the new branch belong in `worktree_path`, not in the caller's
100
106
 
101
107
  ## Gotchas
102
108
 
103
- - **Dirty caller cwd blocks `checkout -b` and pollutes the tree.** Phase 3 always uses `git worktree add -b` into `<repo-root>/.claude/worktrees/<agent>/…`. If you reconstruct Phase 3 by hand with `checkout -b` in the session cwd, local modifications block the checkout and leave the user on a half-switched branch.
109
+ - **Dirty caller cwd blocks `checkout -b` and pollutes the tree.** Phase 3 always uses `git worktree add -b` into `<configured-root>/<agent>/…`. If you reconstruct Phase 3 by hand with `checkout -b` in the session cwd, local modifications block the checkout and leave the user on a half-switched branch.
104
110
  - **Caller HEAD must stay put.** After success, the original repo's checked-out branch and dirty files are unchanged; only the new worktree has the new branch.
105
111
  - **Branch name collision.** If the branch already exists, the script exits non-zero with `{"error":...}`. Pick a new name; do not delete remote branches unless the user asks.
106
112
  - **Path already occupied.** A leftover folder at the preferred worktree path gets a numeric suffix (`-2`, …); report the path from JSON, not the path you assumed.
113
+ - **Relative `--worktree-root` is refused before fetch.** Only an absolute path is accepted; the default root applies when the flag is omitted. Validation runs before `git fetch`, so a bad root never touches the network or remote-tracking refs.
114
+ - **External roots and stale permission rules.** `stale_worktree_rule_sweep` only walks `~/.claude/worktrees`. Edit rules granted under an external `--worktree-root` are not swept when that tree is deleted.
107
115
 
108
116
  ## File index
109
117