claude-dev-env 2.0.2 → 2.1.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 (88) hide show
  1. package/CLAUDE.md +1 -1
  2. package/hooks/blocking/CLAUDE.md +1 -0
  3. package/hooks/blocking/code_rules_shared.py +126 -47
  4. package/hooks/blocking/config/CLAUDE.md +2 -0
  5. package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
  6. package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
  7. package/hooks/blocking/conftest.py +36 -30
  8. package/hooks/blocking/convergence_gate_blocker.py +76 -8
  9. package/hooks/blocking/plain_language_blocker.py +8 -0
  10. package/hooks/blocking/state_description_blocker.py +4 -1
  11. package/hooks/blocking/test_code_rules_shared.py +120 -20
  12. package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
  13. package/hooks/blocking/test_plain_language_blocker.py +15 -0
  14. package/hooks/blocking/test_state_description_blocker.py +15 -0
  15. package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
  16. package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
  17. package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
  18. package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
  19. package/hooks/blocking/verified_commit_gate.py +113 -568
  20. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
  21. package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
  22. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
  23. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
  24. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
  25. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
  26. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
  27. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
  28. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
  29. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
  30. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
  31. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
  32. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
  33. package/hooks/hooks_constants/CLAUDE.md +4 -1
  34. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  35. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
  36. package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
  37. package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
  38. package/hooks/validators/mypy_integration.py +145 -24
  39. package/hooks/validators/python_antipattern_checks.py +16 -0
  40. package/hooks/validators/run_all_validators.py +9 -3
  41. package/hooks/validators/test_mypy_integration.py +154 -0
  42. package/hooks/validators/test_python_antipattern_checks.py +112 -1
  43. package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
  44. package/package.json +1 -1
  45. package/rules/CLAUDE.md +1 -0
  46. package/rules/verified-commit-gate-skip.md +28 -0
  47. package/skills/auditing-claude-config/CLAUDE.md +2 -1
  48. package/skills/auditing-claude-config/SKILL.md +114 -176
  49. package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
  50. package/skills/autoconverge/CLAUDE.md +1 -0
  51. package/skills/autoconverge/SKILL.md +310 -346
  52. package/skills/autoconverge/reference/CLAUDE.md +1 -0
  53. package/skills/autoconverge/reference/copilot-findings.md +51 -0
  54. package/skills/closeout/SKILL.md +7 -9
  55. package/skills/copilot-finding-triage/SKILL.md +5 -7
  56. package/skills/copilot-review/CLAUDE.md +3 -2
  57. package/skills/copilot-review/SKILL.md +119 -155
  58. package/skills/copilot-review/templates/subagent-prompt.md +49 -0
  59. package/skills/fresh-branch/CLAUDE.md +6 -9
  60. package/skills/fresh-branch/SKILL.md +84 -33
  61. package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
  62. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
  63. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
  64. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
  65. package/skills/pr-converge/SKILL.md +277 -422
  66. package/skills/pr-converge/reference/CLAUDE.md +1 -0
  67. package/skills/pr-converge/reference/progress-checklist.md +168 -0
  68. package/skills/pr-converge/test_step5_host_branch.py +8 -6
  69. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  70. package/skills/privacy-hygiene/SKILL.md +68 -115
  71. package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
  72. package/skills/session-log/CLAUDE.md +2 -1
  73. package/skills/session-log/SKILL.md +4 -26
  74. package/skills/session-log/templates/frontmatter.md +40 -0
  75. package/skills/skill-builder/CLAUDE.md +8 -7
  76. package/skills/skill-builder/SKILL.md +26 -11
  77. package/skills/skill-builder/references/CLAUDE.md +3 -1
  78. package/skills/skill-builder/references/delegation-map.md +21 -12
  79. package/skills/skill-builder/references/description-field.md +9 -11
  80. package/skills/skill-builder/references/deterministic-elements.md +218 -0
  81. package/skills/skill-builder/references/self-audit-checklist.md +62 -45
  82. package/skills/skill-builder/references/skill-modularity.md +8 -9
  83. package/skills/skill-builder/templates/CLAUDE.md +2 -2
  84. package/skills/skill-builder/templates/gap-analysis.md +15 -0
  85. package/skills/skill-builder/workflows/CLAUDE.md +5 -5
  86. package/skills/skill-builder/workflows/improve-skill.md +18 -9
  87. package/skills/skill-builder/workflows/new-skill.md +23 -15
  88. package/skills/skill-builder/workflows/polish-skill.md +28 -21
@@ -0,0 +1,670 @@
1
+ """Behavioral tests for create_fresh_branch using real temporary git repos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+
13
+ import pytest
14
+
15
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
16
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
17
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
18
+
19
+ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
20
+ DEFAULT_AGENT_SLUG,
21
+ DEFAULT_BASE_REF,
22
+ ERROR_CLI_ARGUMENTS,
23
+ EXIT_CODE_FAILURE,
24
+ EXIT_CODE_SUCCESS,
25
+ FRESH_BRANCH_AGENT_ENV_VAR,
26
+ PAYLOAD_KEY_AGENT,
27
+ PAYLOAD_KEY_BASE_COMMIT,
28
+ PAYLOAD_KEY_BASE_REF,
29
+ PAYLOAD_KEY_BRANCH,
30
+ PAYLOAD_KEY_ERROR,
31
+ PAYLOAD_KEY_REPO_ROOT,
32
+ PAYLOAD_KEY_WORKTREE_PATH,
33
+ )
34
+
35
+ SCRIPT_PATH = SCRIPTS_DIRECTORY / "create_fresh_branch.py"
36
+ GIT_USER_NAME = "fresh-branch-test"
37
+ GIT_USER_EMAIL = "fresh-branch-test@example.com"
38
+ INITIAL_COMMIT_MESSAGE = "initial commit"
39
+ MAIN_BRANCH_NAME = "main"
40
+ REMOTE_NAME = "origin"
41
+ SEED_FILE_NAME = "README.md"
42
+ SEED_FILE_CONTENTS = "seed\n"
43
+ ALL_AGENT_ENV_MARKERS = (
44
+ FRESH_BRANCH_AGENT_ENV_VAR,
45
+ "CURSOR_AGENT",
46
+ "CURSOR_TRACE_ID",
47
+ "CODEX_HOME",
48
+ "CODEX_CI",
49
+ "GROK_AGENT",
50
+ "CLAUDECODE",
51
+ "CLAUDE_CODE_ENTRYPOINT",
52
+ )
53
+
54
+
55
+ def load_create_fresh_branch_module() -> ModuleType:
56
+ module_name = "create_fresh_branch_under_test"
57
+ spec = importlib.util.spec_from_file_location(module_name, SCRIPT_PATH)
58
+ assert spec is not None
59
+ assert spec.loader is not None
60
+ loaded_module = importlib.util.module_from_spec(spec)
61
+ sys.modules[module_name] = loaded_module
62
+ spec.loader.exec_module(loaded_module)
63
+ return loaded_module
64
+
65
+
66
+ def clear_agent_environment(monkeypatch: pytest.MonkeyPatch) -> None:
67
+ for each_env_name in ALL_AGENT_ENV_MARKERS:
68
+ monkeypatch.delenv(each_env_name, raising=False)
69
+
70
+
71
+ def run_git(
72
+ all_arguments: list[str],
73
+ working_directory: Path,
74
+ empty_hooks_path: Path | None = None,
75
+ ) -> subprocess.CompletedProcess[str]:
76
+ environment = os.environ.copy()
77
+ git_arguments = list(all_arguments)
78
+ if empty_hooks_path is not None:
79
+ git_arguments = [
80
+ "-c",
81
+ f"core.hooksPath={empty_hooks_path}",
82
+ *git_arguments,
83
+ ]
84
+ return subprocess.run(
85
+ ["git", *git_arguments],
86
+ cwd=str(working_directory),
87
+ check=True,
88
+ capture_output=True,
89
+ text=True,
90
+ env=environment,
91
+ )
92
+
93
+
94
+ def configure_git_identity(repository_path: Path, empty_hooks_path: Path) -> None:
95
+ run_git(
96
+ ["config", "user.name", GIT_USER_NAME],
97
+ repository_path,
98
+ empty_hooks_path=empty_hooks_path,
99
+ )
100
+ run_git(
101
+ ["config", "user.email", GIT_USER_EMAIL],
102
+ repository_path,
103
+ empty_hooks_path=empty_hooks_path,
104
+ )
105
+ run_git(
106
+ ["config", "commit.gpgsign", "false"],
107
+ repository_path,
108
+ empty_hooks_path=empty_hooks_path,
109
+ )
110
+
111
+
112
+ def build_repo_with_origin(workspace_path: Path) -> Path:
113
+ workspace_path.mkdir(parents=True, exist_ok=True)
114
+ empty_hooks_path = workspace_path / "empty-hooks"
115
+ empty_hooks_path.mkdir()
116
+ bare_origin = workspace_path / "origin.git"
117
+ run_git(
118
+ ["init", "--bare", str(bare_origin)],
119
+ workspace_path,
120
+ empty_hooks_path=empty_hooks_path,
121
+ )
122
+ clone_path = workspace_path / "clone"
123
+ run_git(
124
+ ["clone", str(bare_origin), str(clone_path)],
125
+ workspace_path,
126
+ empty_hooks_path=empty_hooks_path,
127
+ )
128
+ configure_git_identity(clone_path, empty_hooks_path=empty_hooks_path)
129
+ run_git(
130
+ ["checkout", "-b", MAIN_BRANCH_NAME],
131
+ clone_path,
132
+ empty_hooks_path=empty_hooks_path,
133
+ )
134
+ (clone_path / SEED_FILE_NAME).write_text(SEED_FILE_CONTENTS, encoding="utf-8")
135
+ run_git(["add", SEED_FILE_NAME], clone_path, empty_hooks_path=empty_hooks_path)
136
+ run_git(
137
+ ["commit", "-m", INITIAL_COMMIT_MESSAGE],
138
+ clone_path,
139
+ empty_hooks_path=empty_hooks_path,
140
+ )
141
+ run_git(
142
+ ["push", "-u", REMOTE_NAME, MAIN_BRANCH_NAME],
143
+ clone_path,
144
+ empty_hooks_path=empty_hooks_path,
145
+ )
146
+ return clone_path
147
+
148
+
149
+ def read_head_branch(repository_path: Path) -> str:
150
+ completed = run_git(["rev-parse", "--abbrev-ref", "HEAD"], repository_path)
151
+ return completed.stdout.strip()
152
+
153
+
154
+ def read_head_commit(repository_path: Path) -> str:
155
+ completed = run_git(["rev-parse", "HEAD"], repository_path)
156
+ return completed.stdout.strip()
157
+
158
+
159
+ class TestResolveAgentSlug:
160
+ def should_prefer_flag_over_environment(
161
+ self,
162
+ monkeypatch: pytest.MonkeyPatch,
163
+ ) -> None:
164
+ module = load_create_fresh_branch_module()
165
+ clear_agent_environment(monkeypatch)
166
+ monkeypatch.setenv(FRESH_BRANCH_AGENT_ENV_VAR, "codex")
167
+ assert module.resolve_agent_slug("Cursor") == "cursor"
168
+
169
+ def should_use_fresh_branch_agent_env_before_markers(
170
+ self,
171
+ monkeypatch: pytest.MonkeyPatch,
172
+ ) -> None:
173
+ module = load_create_fresh_branch_module()
174
+ clear_agent_environment(monkeypatch)
175
+ monkeypatch.setenv(FRESH_BRANCH_AGENT_ENV_VAR, "Grok")
176
+ monkeypatch.setenv("CURSOR_TRACE_ID", "1")
177
+ assert module.resolve_agent_slug(None) == "grok"
178
+
179
+ def should_detect_cursor_from_marker(
180
+ self,
181
+ monkeypatch: pytest.MonkeyPatch,
182
+ ) -> None:
183
+ module = load_create_fresh_branch_module()
184
+ clear_agent_environment(monkeypatch)
185
+ monkeypatch.setenv("CURSOR_TRACE_ID", "abc")
186
+ assert module.resolve_agent_slug(None) == "cursor"
187
+
188
+ def should_detect_codex_from_marker(
189
+ self,
190
+ monkeypatch: pytest.MonkeyPatch,
191
+ ) -> None:
192
+ module = load_create_fresh_branch_module()
193
+ clear_agent_environment(monkeypatch)
194
+ monkeypatch.setenv("CODEX_HOME", "/tmp/codex")
195
+ assert module.resolve_agent_slug(None) == "codex"
196
+
197
+ def should_detect_grok_from_marker(
198
+ self,
199
+ monkeypatch: pytest.MonkeyPatch,
200
+ ) -> None:
201
+ module = load_create_fresh_branch_module()
202
+ clear_agent_environment(monkeypatch)
203
+ monkeypatch.setenv("GROK_AGENT", "1")
204
+ assert module.resolve_agent_slug(None) == "grok"
205
+
206
+ def should_default_to_claude_when_no_markers(
207
+ self,
208
+ monkeypatch: pytest.MonkeyPatch,
209
+ ) -> None:
210
+ module = load_create_fresh_branch_module()
211
+ clear_agent_environment(monkeypatch)
212
+ assert module.resolve_agent_slug(None) == DEFAULT_AGENT_SLUG
213
+
214
+ def should_reject_invalid_agent_slug(
215
+ self,
216
+ monkeypatch: pytest.MonkeyPatch,
217
+ ) -> None:
218
+ module = load_create_fresh_branch_module()
219
+ clear_agent_environment(monkeypatch)
220
+ with pytest.raises(ValueError, match="agent slug"):
221
+ module.resolve_agent_slug("../evil")
222
+
223
+
224
+ class TestResolveAgentWorktreeRoot:
225
+ def should_use_userprofile_scratch_on_windows(
226
+ self,
227
+ monkeypatch: pytest.MonkeyPatch,
228
+ ) -> None:
229
+ module = load_create_fresh_branch_module()
230
+ monkeypatch.setattr(module.sys, "platform", "win32")
231
+ monkeypatch.setenv("USERPROFILE", r"C:\Users\example")
232
+ worktree_root = module.resolve_agent_worktree_root("grok")
233
+ assert worktree_root == (
234
+ Path(r"C:\Users\example") / "AppData" / "Local" / "Temp" / "grok"
235
+ )
236
+
237
+ def should_fall_back_to_gettempdir_off_windows(
238
+ self,
239
+ monkeypatch: pytest.MonkeyPatch,
240
+ tmp_path: Path,
241
+ ) -> None:
242
+ module = load_create_fresh_branch_module()
243
+ monkeypatch.setattr(module.sys, "platform", "linux")
244
+ monkeypatch.setattr(module.tempfile, "gettempdir", lambda: str(tmp_path))
245
+ worktree_root = module.resolve_agent_worktree_root("claude")
246
+ assert worktree_root == tmp_path / "claude"
247
+
248
+
249
+ class TestResolveUniqueWorktreePath:
250
+ def should_return_preferred_when_missing(self, tmp_path: Path) -> None:
251
+ module = load_create_fresh_branch_module()
252
+ preferred_path = tmp_path / "feature-one"
253
+ assert module.resolve_unique_worktree_path(preferred_path) == preferred_path
254
+
255
+ def should_suffix_when_preferred_exists(self, tmp_path: Path) -> None:
256
+ module = load_create_fresh_branch_module()
257
+ preferred_path = tmp_path / "feature-one"
258
+ preferred_path.mkdir()
259
+ unique_path = module.resolve_unique_worktree_path(preferred_path)
260
+ assert unique_path == tmp_path / "feature-one-2"
261
+
262
+
263
+ class TestNormalizeBaseRef:
264
+ def should_expand_bare_branch_name_to_origin_tracking_ref(self) -> None:
265
+ module = load_create_fresh_branch_module()
266
+ assert module.normalize_base_ref(MAIN_BRANCH_NAME) == DEFAULT_BASE_REF
267
+
268
+ def should_preserve_explicit_remote_tracking_ref(self) -> None:
269
+ module = load_create_fresh_branch_module()
270
+ assert module.normalize_base_ref(DEFAULT_BASE_REF) == DEFAULT_BASE_REF
271
+
272
+ def should_preserve_non_origin_remote_ref(self) -> None:
273
+ module = load_create_fresh_branch_module()
274
+ assert module.normalize_base_ref("upstream/dev") == "upstream/dev"
275
+
276
+
277
+ class TestAgentSlugPathSafety:
278
+ def should_reject_parent_segment_agent_slug_without_mkdir_outside(
279
+ self,
280
+ tmp_path: Path,
281
+ monkeypatch: pytest.MonkeyPatch,
282
+ ) -> None:
283
+ module = load_create_fresh_branch_module()
284
+ agent_scratch_parent = tmp_path / "agent-scratch"
285
+ agent_scratch_parent.mkdir()
286
+ escaped_agent_root = (agent_scratch_parent / ".." / "escape-agent").resolve()
287
+ monkeypatch.setattr(module.sys, "platform", "linux")
288
+ monkeypatch.setattr(
289
+ module.tempfile,
290
+ "gettempdir",
291
+ lambda: str(agent_scratch_parent),
292
+ )
293
+ with pytest.raises(ValueError, match="agent slug"):
294
+ module.create_fresh_branch(
295
+ branch_name="fix/safe-branch",
296
+ repo_path=tmp_path / "unused-repo",
297
+ agent_slug="../escape-agent",
298
+ base_ref=DEFAULT_BASE_REF,
299
+ )
300
+ assert not escaped_agent_root.exists()
301
+ assert not (agent_scratch_parent / "claude").exists()
302
+
303
+ def should_reject_empty_agent_slug(
304
+ self,
305
+ tmp_path: Path,
306
+ monkeypatch: pytest.MonkeyPatch,
307
+ ) -> None:
308
+ module = load_create_fresh_branch_module()
309
+ agent_scratch_parent = tmp_path / "agent-scratch"
310
+ agent_scratch_parent.mkdir()
311
+ monkeypatch.setattr(module.sys, "platform", "linux")
312
+ monkeypatch.setattr(
313
+ module.tempfile,
314
+ "gettempdir",
315
+ lambda: str(agent_scratch_parent),
316
+ )
317
+ with pytest.raises(ValueError, match="agent slug"):
318
+ module.create_fresh_branch(
319
+ branch_name="fix/safe-branch",
320
+ repo_path=tmp_path / "unused-repo",
321
+ agent_slug="",
322
+ base_ref=DEFAULT_BASE_REF,
323
+ )
324
+ assert not any(agent_scratch_parent.iterdir())
325
+
326
+
327
+ class TestBranchNamePathSafety:
328
+ def should_reject_parent_segments_without_mkdir_outside(
329
+ self,
330
+ tmp_path: Path,
331
+ monkeypatch: pytest.MonkeyPatch,
332
+ ) -> None:
333
+ module = load_create_fresh_branch_module()
334
+ agent_scratch_parent = tmp_path / "agent-scratch"
335
+ agent_scratch_parent.mkdir()
336
+ monkeypatch.setattr(module.sys, "platform", "linux")
337
+ monkeypatch.setattr(
338
+ module.tempfile,
339
+ "gettempdir",
340
+ lambda: str(agent_scratch_parent),
341
+ )
342
+ escaped_path = agent_scratch_parent / "escape"
343
+ with pytest.raises(ValueError, match="relative path"):
344
+ module.create_fresh_branch(
345
+ branch_name="fix/../../escape",
346
+ repo_path=tmp_path / "unused-repo",
347
+ agent_slug="claude",
348
+ base_ref=DEFAULT_BASE_REF,
349
+ )
350
+ assert not escaped_path.exists()
351
+ assert not (agent_scratch_parent / "claude").exists()
352
+
353
+ def should_reject_absolute_branch_without_mkdir_outside(
354
+ self,
355
+ tmp_path: Path,
356
+ monkeypatch: pytest.MonkeyPatch,
357
+ ) -> None:
358
+ module = load_create_fresh_branch_module()
359
+ agent_scratch_parent = tmp_path / "agent-scratch"
360
+ agent_scratch_parent.mkdir()
361
+ outside_target = tmp_path / "outside-evil"
362
+ monkeypatch.setattr(module.sys, "platform", "linux")
363
+ monkeypatch.setattr(
364
+ module.tempfile,
365
+ "gettempdir",
366
+ lambda: str(agent_scratch_parent),
367
+ )
368
+ with pytest.raises(ValueError, match="relative path"):
369
+ module.create_fresh_branch(
370
+ branch_name=str(outside_target),
371
+ repo_path=tmp_path / "unused-repo",
372
+ agent_slug="claude",
373
+ base_ref=DEFAULT_BASE_REF,
374
+ )
375
+ assert not outside_target.exists()
376
+ assert not (agent_scratch_parent / "claude").exists()
377
+
378
+
379
+ class TestCreateFreshBranchIntegration:
380
+ def should_create_worktree_branch_without_moving_caller_head(
381
+ self,
382
+ tmp_path: Path,
383
+ monkeypatch: pytest.MonkeyPatch,
384
+ ) -> None:
385
+ module = load_create_fresh_branch_module()
386
+ repository_path = build_repo_with_origin(tmp_path / "repo")
387
+ caller_head_before = read_head_branch(repository_path)
388
+ caller_commit_before = read_head_commit(repository_path)
389
+ agent_scratch_parent = tmp_path / "agent-scratch"
390
+ monkeypatch.setattr(module.sys, "platform", "linux")
391
+ monkeypatch.setattr(
392
+ module.tempfile,
393
+ "gettempdir",
394
+ lambda: str(agent_scratch_parent),
395
+ )
396
+ success_payload = module.create_fresh_branch(
397
+ branch_name="fix/example-one",
398
+ repo_path=repository_path,
399
+ agent_slug="grok",
400
+ base_ref=DEFAULT_BASE_REF,
401
+ )
402
+ worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
403
+ assert worktree_path.is_dir()
404
+ assert worktree_path == agent_scratch_parent / "grok" / "fix" / "example-one"
405
+ assert read_head_branch(worktree_path) == "fix/example-one"
406
+ assert read_head_commit(worktree_path) == caller_commit_before
407
+ assert success_payload[PAYLOAD_KEY_BRANCH] == "fix/example-one"
408
+ assert success_payload[PAYLOAD_KEY_BASE_REF] == DEFAULT_BASE_REF
409
+ assert success_payload[PAYLOAD_KEY_BASE_COMMIT] == caller_commit_before
410
+ assert success_payload[PAYLOAD_KEY_AGENT] == "grok"
411
+ assert Path(success_payload[PAYLOAD_KEY_REPO_ROOT]) == repository_path.resolve()
412
+ assert read_head_branch(repository_path) == caller_head_before
413
+ assert read_head_commit(repository_path) == caller_commit_before
414
+ assert (worktree_path / SEED_FILE_NAME).read_text(encoding="utf-8") == (
415
+ SEED_FILE_CONTENTS
416
+ )
417
+
418
+ def should_allocate_suffix_when_worktree_path_exists(
419
+ self,
420
+ tmp_path: Path,
421
+ monkeypatch: pytest.MonkeyPatch,
422
+ ) -> None:
423
+ module = load_create_fresh_branch_module()
424
+ repository_path = build_repo_with_origin(tmp_path / "repo")
425
+ agent_scratch_parent = tmp_path / "agent-scratch"
426
+ monkeypatch.setattr(module.sys, "platform", "linux")
427
+ monkeypatch.setattr(
428
+ module.tempfile,
429
+ "gettempdir",
430
+ lambda: str(agent_scratch_parent),
431
+ )
432
+ occupied_path = agent_scratch_parent / "claude" / "fix" / "collision"
433
+ occupied_path.mkdir(parents=True)
434
+ success_payload = module.create_fresh_branch(
435
+ branch_name="fix/collision",
436
+ repo_path=repository_path,
437
+ agent_slug="claude",
438
+ base_ref=DEFAULT_BASE_REF,
439
+ )
440
+ worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
441
+ assert worktree_path == agent_scratch_parent / "claude" / "fix" / "collision-2"
442
+ assert worktree_path.is_dir()
443
+ assert read_head_branch(worktree_path) == "fix/collision"
444
+
445
+ def should_fail_when_branch_already_exists(
446
+ self,
447
+ tmp_path: Path,
448
+ monkeypatch: pytest.MonkeyPatch,
449
+ ) -> None:
450
+ module = load_create_fresh_branch_module()
451
+ repository_path = build_repo_with_origin(tmp_path / "repo")
452
+ agent_scratch_parent = tmp_path / "agent-scratch"
453
+ monkeypatch.setattr(module.sys, "platform", "linux")
454
+ monkeypatch.setattr(
455
+ module.tempfile,
456
+ "gettempdir",
457
+ lambda: str(agent_scratch_parent),
458
+ )
459
+ module.create_fresh_branch(
460
+ branch_name="fix/dup",
461
+ repo_path=repository_path,
462
+ agent_slug="claude",
463
+ base_ref=DEFAULT_BASE_REF,
464
+ )
465
+ with pytest.raises(RuntimeError, match="worktree add failed"):
466
+ module.create_fresh_branch(
467
+ branch_name="fix/dup",
468
+ repo_path=repository_path,
469
+ agent_slug="claude",
470
+ base_ref=DEFAULT_BASE_REF,
471
+ )
472
+
473
+ def should_use_remote_tracking_tip_for_bare_base_name(
474
+ self,
475
+ tmp_path: Path,
476
+ monkeypatch: pytest.MonkeyPatch,
477
+ ) -> None:
478
+ module = load_create_fresh_branch_module()
479
+ repository_path = build_repo_with_origin(tmp_path / "repo")
480
+ empty_hooks_path = tmp_path / "repo" / "empty-hooks"
481
+ origin_tip_before = read_head_commit(repository_path)
482
+ (repository_path / SEED_FILE_NAME).write_text("local-only\n", encoding="utf-8")
483
+ run_git(
484
+ ["add", SEED_FILE_NAME],
485
+ repository_path,
486
+ empty_hooks_path=empty_hooks_path,
487
+ )
488
+ run_git(
489
+ ["commit", "-m", "local advance"],
490
+ repository_path,
491
+ empty_hooks_path=empty_hooks_path,
492
+ )
493
+ local_main_commit = read_head_commit(repository_path)
494
+ assert local_main_commit != origin_tip_before
495
+ agent_scratch_parent = tmp_path / "agent-scratch"
496
+ monkeypatch.setattr(module.sys, "platform", "linux")
497
+ monkeypatch.setattr(
498
+ module.tempfile,
499
+ "gettempdir",
500
+ lambda: str(agent_scratch_parent),
501
+ )
502
+ success_payload = module.create_fresh_branch(
503
+ branch_name="fix/bare-base",
504
+ repo_path=repository_path,
505
+ agent_slug="claude",
506
+ base_ref=MAIN_BRANCH_NAME,
507
+ )
508
+ worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
509
+ assert success_payload[PAYLOAD_KEY_BASE_REF] == DEFAULT_BASE_REF
510
+ assert success_payload[PAYLOAD_KEY_BASE_COMMIT] == origin_tip_before
511
+ assert read_head_commit(worktree_path) == origin_tip_before
512
+ assert read_head_commit(worktree_path) != local_main_commit
513
+
514
+
515
+ class TestMainCli:
516
+ def should_print_success_json_and_exit_zero(
517
+ self,
518
+ tmp_path: Path,
519
+ monkeypatch: pytest.MonkeyPatch,
520
+ capsys: pytest.CaptureFixture[str],
521
+ ) -> None:
522
+ module = load_create_fresh_branch_module()
523
+ repository_path = build_repo_with_origin(tmp_path / "repo")
524
+ agent_scratch_parent = tmp_path / "agent-scratch"
525
+ monkeypatch.setattr(module.sys, "platform", "linux")
526
+ monkeypatch.setattr(
527
+ module.tempfile,
528
+ "gettempdir",
529
+ lambda: str(agent_scratch_parent),
530
+ )
531
+ monkeypatch.setattr(
532
+ sys,
533
+ "argv",
534
+ [
535
+ "create_fresh_branch.py",
536
+ "--branch-name",
537
+ "feat/cli-ok",
538
+ "--agent",
539
+ "grok",
540
+ "--repo",
541
+ str(repository_path),
542
+ ],
543
+ )
544
+ exit_code = module.main()
545
+ captured = capsys.readouterr()
546
+ assert exit_code == EXIT_CODE_SUCCESS
547
+ success_payload = json.loads(captured.out)
548
+ assert success_payload[PAYLOAD_KEY_BRANCH] == "feat/cli-ok"
549
+ assert success_payload[PAYLOAD_KEY_AGENT] == "grok"
550
+ assert Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH]).is_dir()
551
+
552
+ def should_print_error_json_and_exit_nonzero_for_bad_repo(
553
+ self,
554
+ tmp_path: Path,
555
+ monkeypatch: pytest.MonkeyPatch,
556
+ capsys: pytest.CaptureFixture[str],
557
+ ) -> None:
558
+ module = load_create_fresh_branch_module()
559
+ empty_directory = tmp_path / "not-a-repo"
560
+ empty_directory.mkdir()
561
+ monkeypatch.setattr(
562
+ sys,
563
+ "argv",
564
+ [
565
+ "create_fresh_branch.py",
566
+ "--branch-name",
567
+ "feat/no-repo",
568
+ "--repo",
569
+ str(empty_directory),
570
+ "--agent",
571
+ "claude",
572
+ ],
573
+ )
574
+ exit_code = module.main()
575
+ captured = capsys.readouterr()
576
+ assert exit_code == EXIT_CODE_FAILURE
577
+ error_payload = json.loads(captured.out)
578
+ assert PAYLOAD_KEY_ERROR in error_payload
579
+ assert "git repository" in error_payload[PAYLOAD_KEY_ERROR]
580
+
581
+ def should_print_error_json_when_branch_name_flag_missing(
582
+ self,
583
+ monkeypatch: pytest.MonkeyPatch,
584
+ capsys: pytest.CaptureFixture[str],
585
+ ) -> None:
586
+ module = load_create_fresh_branch_module()
587
+ monkeypatch.setattr(sys, "argv", ["create_fresh_branch.py"])
588
+ exit_code = module.main()
589
+ captured = capsys.readouterr()
590
+ assert exit_code == EXIT_CODE_FAILURE
591
+ error_payload = json.loads(captured.out)
592
+ assert error_payload[PAYLOAD_KEY_ERROR] == ERROR_CLI_ARGUMENTS
593
+
594
+ def should_print_error_json_for_parent_segment_branch_name(
595
+ self,
596
+ tmp_path: Path,
597
+ monkeypatch: pytest.MonkeyPatch,
598
+ capsys: pytest.CaptureFixture[str],
599
+ ) -> None:
600
+ module = load_create_fresh_branch_module()
601
+ repository_path = build_repo_with_origin(tmp_path / "repo")
602
+ agent_scratch_parent = tmp_path / "agent-scratch"
603
+ agent_scratch_parent.mkdir()
604
+ escaped_path = agent_scratch_parent / "escape"
605
+ monkeypatch.setattr(module.sys, "platform", "linux")
606
+ monkeypatch.setattr(
607
+ module.tempfile,
608
+ "gettempdir",
609
+ lambda: str(agent_scratch_parent),
610
+ )
611
+ monkeypatch.setattr(
612
+ sys,
613
+ "argv",
614
+ [
615
+ "create_fresh_branch.py",
616
+ "--branch-name",
617
+ "../escape",
618
+ "--repo",
619
+ str(repository_path),
620
+ "--agent",
621
+ "claude",
622
+ ],
623
+ )
624
+ exit_code = module.main()
625
+ captured = capsys.readouterr()
626
+ assert exit_code == EXIT_CODE_FAILURE
627
+ error_payload = json.loads(captured.out)
628
+ assert PAYLOAD_KEY_ERROR in error_payload
629
+ assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
630
+ assert not escaped_path.exists()
631
+ assert not (agent_scratch_parent / "claude").exists()
632
+
633
+ def should_print_error_json_for_absolute_branch_name(
634
+ self,
635
+ tmp_path: Path,
636
+ monkeypatch: pytest.MonkeyPatch,
637
+ capsys: pytest.CaptureFixture[str],
638
+ ) -> None:
639
+ module = load_create_fresh_branch_module()
640
+ repository_path = build_repo_with_origin(tmp_path / "repo")
641
+ agent_scratch_parent = tmp_path / "agent-scratch"
642
+ agent_scratch_parent.mkdir()
643
+ outside_target = tmp_path / "outside-evil"
644
+ monkeypatch.setattr(module.sys, "platform", "linux")
645
+ monkeypatch.setattr(
646
+ module.tempfile,
647
+ "gettempdir",
648
+ lambda: str(agent_scratch_parent),
649
+ )
650
+ monkeypatch.setattr(
651
+ sys,
652
+ "argv",
653
+ [
654
+ "create_fresh_branch.py",
655
+ "--branch-name",
656
+ str(outside_target),
657
+ "--repo",
658
+ str(repository_path),
659
+ "--agent",
660
+ "claude",
661
+ ],
662
+ )
663
+ exit_code = module.main()
664
+ captured = capsys.readouterr()
665
+ assert exit_code == EXIT_CODE_FAILURE
666
+ error_payload = json.loads(captured.out)
667
+ assert PAYLOAD_KEY_ERROR in error_payload
668
+ assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
669
+ assert not outside_target.exists()
670
+ assert not (agent_scratch_parent / "claude").exists()