claude-dev-env 2.3.0 → 2.4.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.
- package/CLAUDE.md +3 -2
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +3 -3
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +13 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -903
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +27 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +2 -2
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"base_commit": "abc...", "agent": "claude", "repo_root": "..."}
|
|
9
9
|
|
|
10
10
|
Never runs ``git checkout -b`` in the caller's working tree. Fetches the base
|
|
11
|
-
ref, then ``git worktree add -b`` into ``Temp/<agent>/<branch>``
|
|
12
|
-
``gettempdir()/<agent>/<branch>`` elsewhere. Exit 0 prints success
|
|
13
|
-
failure prints ``{"error": ...}`` and exits non-zero.
|
|
11
|
+
ref, then ``git worktree add -b --no-track`` into ``Temp/<agent>/<branch>``
|
|
12
|
+
(Windows) or ``gettempdir()/<agent>/<branch>`` elsewhere. Exit 0 prints success
|
|
13
|
+
JSON; any failure prints ``{"error": ...}`` and exits non-zero.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
@@ -19,11 +19,17 @@ import argparse
|
|
|
19
19
|
import json
|
|
20
20
|
import os
|
|
21
21
|
import re
|
|
22
|
-
import subprocess
|
|
23
22
|
import sys
|
|
24
23
|
import tempfile
|
|
25
24
|
from pathlib import Path
|
|
26
25
|
|
|
26
|
+
from fresh_branch_git_commands import (
|
|
27
|
+
assert_git_accepts_branch_name,
|
|
28
|
+
create_worktree_branch,
|
|
29
|
+
fetch_base_ref,
|
|
30
|
+
resolve_base_commit,
|
|
31
|
+
resolve_repo_root,
|
|
32
|
+
)
|
|
27
33
|
from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
28
34
|
AGENT_SLUG_PATTERN,
|
|
29
35
|
ALL_AGENT_DETECTION_MARKERS,
|
|
@@ -31,34 +37,15 @@ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
|
31
37
|
DEFAULT_AGENT_SLUG,
|
|
32
38
|
DEFAULT_BASE_REF,
|
|
33
39
|
ERROR_AGENT_SLUG_INVALID,
|
|
34
|
-
ERROR_BASE_COMMIT_LOOKUP,
|
|
35
|
-
ERROR_BASE_REF_MISSING,
|
|
36
40
|
ERROR_BRANCH_NAME_REQUIRED,
|
|
37
41
|
ERROR_BRANCH_NAME_UNSAFE,
|
|
38
42
|
ERROR_CLI_ARGUMENTS,
|
|
39
|
-
ERROR_FETCH_FAILED,
|
|
40
|
-
ERROR_REPO_NOT_GIT,
|
|
41
43
|
ERROR_UNIQUE_PATH_EXHAUSTED,
|
|
42
|
-
ERROR_WORKTREE_FAILED,
|
|
43
44
|
ERROR_WORKTREE_PATH_OUTSIDE_ROOT,
|
|
44
45
|
EXIT_CODE_FAILURE,
|
|
45
46
|
EXIT_CODE_SUCCESS,
|
|
46
47
|
FRESH_BRANCH_AGENT_ENV_VAR,
|
|
47
|
-
GIT_BRANCH_FLAG,
|
|
48
|
-
GIT_BRANCH_FORMAT_FLAG,
|
|
49
|
-
GIT_CHECK_REF_FORMAT,
|
|
50
|
-
GIT_COMMAND,
|
|
51
|
-
GIT_FETCH,
|
|
52
|
-
GIT_QUIET_FLAG,
|
|
53
|
-
GIT_REFS_REMOTES_PREFIX,
|
|
54
|
-
GIT_REMOTE_NAME_ORIGIN,
|
|
55
48
|
GIT_REMOTE_PREFIX,
|
|
56
|
-
GIT_REV_PARSE,
|
|
57
|
-
GIT_SHOW_REF,
|
|
58
|
-
GIT_SHOW_TOPLEVEL,
|
|
59
|
-
GIT_VERIFY_FLAG,
|
|
60
|
-
GIT_WORKTREE,
|
|
61
|
-
GIT_WORKTREE_ADD,
|
|
62
49
|
MAXIMUM_UNIQUE_PATH_ATTEMPTS,
|
|
63
50
|
PATH_SEGMENT_CURRENT,
|
|
64
51
|
PATH_SEGMENT_PARENT,
|
|
@@ -80,13 +67,7 @@ def resolve_agent_slug(maybe_flag_agent: str | None) -> str:
|
|
|
80
67
|
|
|
81
68
|
::
|
|
82
69
|
|
|
83
|
-
resolve_agent_slug("Grok") # ok: "grok"
|
|
84
|
-
# FRESH_BRANCH_AGENT=codex, no flag -> "codex"
|
|
85
|
-
# CURSOR_TRACE_ID set, no flag/env -> "cursor"
|
|
86
|
-
# no flag, env, or markers -> "claude"
|
|
87
|
-
|
|
88
|
-
Order: ``--agent`` flag, then ``FRESH_BRANCH_AGENT``, then the first set
|
|
89
|
-
marker in ``ALL_AGENT_DETECTION_MARKERS``, else ``DEFAULT_AGENT_SLUG``.
|
|
70
|
+
resolve_agent_slug("Grok") # ok: "grok"
|
|
90
71
|
|
|
91
72
|
Args:
|
|
92
73
|
maybe_flag_agent: Explicit ``--agent`` value, or None.
|
|
@@ -102,6 +83,10 @@ def resolve_agent_slug(maybe_flag_agent: str | None) -> str:
|
|
|
102
83
|
explicit_agent = os.environ.get(FRESH_BRANCH_AGENT_ENV_VAR)
|
|
103
84
|
if explicit_agent is not None and explicit_agent.strip():
|
|
104
85
|
return _normalize_agent_slug(explicit_agent)
|
|
86
|
+
return _detect_agent_slug_from_environment()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _detect_agent_slug_from_environment() -> str:
|
|
105
90
|
for each_env_name, each_agent_slug in ALL_AGENT_DETECTION_MARKERS:
|
|
106
91
|
maybe_marker = os.environ.get(each_env_name)
|
|
107
92
|
if maybe_marker is not None and str(maybe_marker).strip():
|
|
@@ -171,9 +156,6 @@ def normalize_base_ref(base_ref: str) -> str:
|
|
|
171
156
|
normalize_base_ref("origin/main") # ok: "origin/main"
|
|
172
157
|
normalize_base_ref("upstream/dev") # ok: "upstream/dev"
|
|
173
158
|
|
|
174
|
-
Bare names expand to ``origin/<name>`` so worktree start-points use the
|
|
175
|
-
remote-tracking tip that ``git fetch`` updates, not a stale local branch.
|
|
176
|
-
|
|
177
159
|
Args:
|
|
178
160
|
base_ref: User-supplied base ref or bare branch name.
|
|
179
161
|
|
|
@@ -194,61 +176,39 @@ def create_fresh_branch(
|
|
|
194
176
|
agent_slug: str,
|
|
195
177
|
base_ref: str,
|
|
196
178
|
) -> dict[str, str]:
|
|
197
|
-
"""Fetch base_ref and create an isolated worktree branch.
|
|
179
|
+
"""Fetch base_ref and create an isolated worktree branch with no upstream.
|
|
198
180
|
|
|
199
181
|
::
|
|
200
182
|
|
|
201
183
|
create_fresh_branch("fix/x", Path("."), "grok", "origin/main")
|
|
202
|
-
# ok: worktree at Temp/grok/fix/x; source checkout stays on its branch
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
branch_name: New branch to create.
|
|
206
|
-
repo_path: Path inside the source repository.
|
|
207
|
-
agent_slug: Host label for the scratch root segment.
|
|
208
|
-
base_ref: Base ref (default ``origin/main``).
|
|
209
|
-
|
|
210
|
-
Returns:
|
|
211
|
-
Success payload mapping.
|
|
212
|
-
|
|
213
|
-
Raises:
|
|
214
|
-
ValueError: When branch_name is empty or path-unsafe, or agent_slug
|
|
215
|
-
fails the path-safety pattern.
|
|
216
|
-
RuntimeError: On git or path failures.
|
|
217
184
|
"""
|
|
218
|
-
cleaned_branch = branch_name
|
|
219
|
-
if not cleaned_branch:
|
|
220
|
-
raise ValueError(ERROR_BRANCH_NAME_REQUIRED)
|
|
221
|
-
_validate_branch_name_for_worktree_path(cleaned_branch)
|
|
185
|
+
cleaned_branch = _require_safe_branch_name(branch_name)
|
|
222
186
|
normalized_agent_slug = _normalize_agent_slug(agent_slug)
|
|
223
|
-
resolved_base_ref =
|
|
224
|
-
|
|
225
|
-
_fetch_base_ref(repo_root, resolved_base_ref)
|
|
226
|
-
base_commit = _resolve_base_commit(repo_root, resolved_base_ref)
|
|
227
|
-
agent_worktree_root = resolve_agent_worktree_root(normalized_agent_slug)
|
|
228
|
-
preferred_path = agent_worktree_root / cleaned_branch
|
|
229
|
-
_assert_path_is_under_agent_root(
|
|
230
|
-
candidate_path=preferred_path,
|
|
231
|
-
agent_worktree_root=agent_worktree_root,
|
|
232
|
-
)
|
|
233
|
-
worktree_path = resolve_unique_worktree_path(preferred_path)
|
|
234
|
-
_assert_path_is_under_agent_root(
|
|
235
|
-
candidate_path=worktree_path,
|
|
236
|
-
agent_worktree_root=agent_worktree_root,
|
|
187
|
+
resolved_base_ref, repo_root, base_commit = _resolve_branch_base(
|
|
188
|
+
repo_path, base_ref,
|
|
237
189
|
)
|
|
238
|
-
|
|
190
|
+
worktree_path = _allocate_worktree_path(cleaned_branch, normalized_agent_slug)
|
|
191
|
+
create_worktree_branch(
|
|
239
192
|
repo_root,
|
|
240
193
|
branch_name=cleaned_branch,
|
|
241
194
|
worktree_path=worktree_path,
|
|
242
195
|
base_ref=resolved_base_ref,
|
|
243
196
|
)
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
197
|
+
return _build_success_payload(
|
|
198
|
+
cleaned_branch, worktree_path, resolved_base_ref, base_commit,
|
|
199
|
+
normalized_agent_slug, repo_root,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _resolve_branch_base(
|
|
204
|
+
repo_path: Path,
|
|
205
|
+
base_ref: str,
|
|
206
|
+
) -> tuple[str, Path, str]:
|
|
207
|
+
resolved_base_ref = normalize_base_ref(base_ref)
|
|
208
|
+
repo_root = resolve_repo_root(repo_path)
|
|
209
|
+
fetch_base_ref(repo_root, resolved_base_ref)
|
|
210
|
+
base_commit = resolve_base_commit(repo_root, resolved_base_ref)
|
|
211
|
+
return resolved_base_ref, repo_root, base_commit
|
|
252
212
|
|
|
253
213
|
|
|
254
214
|
def main() -> int:
|
|
@@ -273,6 +233,47 @@ def main() -> int:
|
|
|
273
233
|
return EXIT_CODE_FAILURE
|
|
274
234
|
|
|
275
235
|
|
|
236
|
+
def _require_safe_branch_name(branch_name: str) -> str:
|
|
237
|
+
cleaned_branch = branch_name.strip()
|
|
238
|
+
if not cleaned_branch:
|
|
239
|
+
raise ValueError(ERROR_BRANCH_NAME_REQUIRED)
|
|
240
|
+
_validate_branch_name_for_worktree_path(cleaned_branch)
|
|
241
|
+
return cleaned_branch
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _allocate_worktree_path(branch_name: str, agent_slug: str) -> Path:
|
|
245
|
+
agent_worktree_root = resolve_agent_worktree_root(agent_slug)
|
|
246
|
+
preferred_path = agent_worktree_root / branch_name
|
|
247
|
+
_assert_path_is_under_agent_root(
|
|
248
|
+
candidate_path=preferred_path,
|
|
249
|
+
agent_worktree_root=agent_worktree_root,
|
|
250
|
+
)
|
|
251
|
+
worktree_path = resolve_unique_worktree_path(preferred_path)
|
|
252
|
+
_assert_path_is_under_agent_root(
|
|
253
|
+
candidate_path=worktree_path,
|
|
254
|
+
agent_worktree_root=agent_worktree_root,
|
|
255
|
+
)
|
|
256
|
+
return worktree_path
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _build_success_payload(
|
|
260
|
+
branch_name: str,
|
|
261
|
+
worktree_path: Path,
|
|
262
|
+
base_ref: str,
|
|
263
|
+
base_commit: str,
|
|
264
|
+
agent_slug: str,
|
|
265
|
+
repo_root: Path,
|
|
266
|
+
) -> dict[str, str]:
|
|
267
|
+
return {
|
|
268
|
+
PAYLOAD_KEY_BRANCH: branch_name,
|
|
269
|
+
PAYLOAD_KEY_WORKTREE_PATH: str(worktree_path.resolve()),
|
|
270
|
+
PAYLOAD_KEY_BASE_REF: base_ref,
|
|
271
|
+
PAYLOAD_KEY_BASE_COMMIT: base_commit,
|
|
272
|
+
PAYLOAD_KEY_AGENT: agent_slug,
|
|
273
|
+
PAYLOAD_KEY_REPO_ROOT: str(repo_root),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
276
277
|
def _normalize_agent_slug(raw_agent: str) -> str:
|
|
277
278
|
agent_slug = raw_agent.strip().lower()
|
|
278
279
|
if re.fullmatch(AGENT_SLUG_PATTERN, agent_slug) is None:
|
|
@@ -287,16 +288,7 @@ def _validate_branch_name_for_worktree_path(branch_name: str) -> None:
|
|
|
287
288
|
for each_segment in branch_as_path.parts:
|
|
288
289
|
if each_segment in (PATH_SEGMENT_CURRENT, PATH_SEGMENT_PARENT):
|
|
289
290
|
raise ValueError(ERROR_BRANCH_NAME_UNSAFE)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def _assert_git_accepts_branch_name(branch_name: str) -> None:
|
|
294
|
-
completed = _run_git(
|
|
295
|
-
[GIT_CHECK_REF_FORMAT, GIT_BRANCH_FORMAT_FLAG, branch_name],
|
|
296
|
-
working_directory=Path.cwd(),
|
|
297
|
-
)
|
|
298
|
-
if completed.returncode != 0:
|
|
299
|
-
raise ValueError(ERROR_BRANCH_NAME_UNSAFE)
|
|
291
|
+
assert_git_accepts_branch_name(branch_name)
|
|
300
292
|
|
|
301
293
|
|
|
302
294
|
def _assert_path_is_under_agent_root(
|
|
@@ -315,63 +307,6 @@ def _assert_path_is_under_agent_root(
|
|
|
315
307
|
) from error
|
|
316
308
|
|
|
317
309
|
|
|
318
|
-
def _resolve_repo_root(repo_path: Path) -> Path:
|
|
319
|
-
completed = _run_git(
|
|
320
|
-
[GIT_REV_PARSE, GIT_SHOW_TOPLEVEL],
|
|
321
|
-
working_directory=repo_path,
|
|
322
|
-
)
|
|
323
|
-
if completed.returncode != 0:
|
|
324
|
-
raise RuntimeError(ERROR_REPO_NOT_GIT % repo_path)
|
|
325
|
-
return Path(completed.stdout.strip())
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def _fetch_base_ref(repo_root: Path, base_ref: str) -> None:
|
|
329
|
-
remote_name, remote_branch = _split_remote_ref(base_ref)
|
|
330
|
-
completed = _run_git(
|
|
331
|
-
[GIT_FETCH, remote_name, remote_branch],
|
|
332
|
-
working_directory=repo_root,
|
|
333
|
-
)
|
|
334
|
-
if completed.returncode != 0:
|
|
335
|
-
stderr_text = completed.stderr.strip() or completed.stdout.strip()
|
|
336
|
-
raise RuntimeError(ERROR_FETCH_FAILED % (base_ref, stderr_text))
|
|
337
|
-
if not _is_ref_present(repo_root, base_ref):
|
|
338
|
-
raise RuntimeError(ERROR_BASE_REF_MISSING % base_ref)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _resolve_base_commit(repo_root: Path, base_ref: str) -> str:
|
|
342
|
-
completed = _run_git(
|
|
343
|
-
[GIT_REV_PARSE, base_ref],
|
|
344
|
-
working_directory=repo_root,
|
|
345
|
-
)
|
|
346
|
-
if completed.returncode != 0:
|
|
347
|
-
stderr_text = completed.stderr.strip() or completed.stdout.strip()
|
|
348
|
-
raise RuntimeError(ERROR_BASE_COMMIT_LOOKUP % (base_ref, stderr_text))
|
|
349
|
-
return completed.stdout.strip()
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def _create_worktree_branch(
|
|
353
|
-
repo_root: Path,
|
|
354
|
-
branch_name: str,
|
|
355
|
-
worktree_path: Path,
|
|
356
|
-
base_ref: str,
|
|
357
|
-
) -> None:
|
|
358
|
-
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
359
|
-
completed = _run_git(
|
|
360
|
-
[
|
|
361
|
-
GIT_WORKTREE,
|
|
362
|
-
GIT_WORKTREE_ADD,
|
|
363
|
-
GIT_BRANCH_FLAG,
|
|
364
|
-
branch_name,
|
|
365
|
-
str(worktree_path),
|
|
366
|
-
base_ref,
|
|
367
|
-
],
|
|
368
|
-
working_directory=repo_root,
|
|
369
|
-
)
|
|
370
|
-
if completed.returncode != 0:
|
|
371
|
-
stderr_text = completed.stderr.strip() or completed.stdout.strip()
|
|
372
|
-
raise RuntimeError(ERROR_WORKTREE_FAILED % stderr_text)
|
|
373
|
-
|
|
374
|
-
|
|
375
310
|
def _parse_arguments() -> argparse.Namespace:
|
|
376
311
|
parser = argparse.ArgumentParser(
|
|
377
312
|
description="Create a fresh branch in an isolated git worktree.",
|
|
@@ -404,42 +339,5 @@ def _parse_arguments() -> argparse.Namespace:
|
|
|
404
339
|
raise ValueError(ERROR_CLI_ARGUMENTS) from exit_error
|
|
405
340
|
|
|
406
341
|
|
|
407
|
-
def _split_remote_ref(base_ref: str) -> tuple[str, str]:
|
|
408
|
-
if base_ref.startswith(GIT_REMOTE_PREFIX):
|
|
409
|
-
return GIT_REMOTE_NAME_ORIGIN, base_ref[len(GIT_REMOTE_PREFIX) :]
|
|
410
|
-
if "/" in base_ref:
|
|
411
|
-
remote_name, remote_branch = base_ref.split("/", 1)
|
|
412
|
-
return remote_name, remote_branch
|
|
413
|
-
return GIT_REMOTE_NAME_ORIGIN, base_ref
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _is_ref_present(repo_root: Path, base_ref: str) -> bool:
|
|
417
|
-
remote_ref = f"{GIT_REFS_REMOTES_PREFIX}{base_ref}"
|
|
418
|
-
completed = _run_git(
|
|
419
|
-
[GIT_SHOW_REF, GIT_VERIFY_FLAG, GIT_QUIET_FLAG, remote_ref],
|
|
420
|
-
working_directory=repo_root,
|
|
421
|
-
)
|
|
422
|
-
if completed.returncode == 0:
|
|
423
|
-
return True
|
|
424
|
-
completed = _run_git(
|
|
425
|
-
[GIT_REV_PARSE, GIT_VERIFY_FLAG, GIT_QUIET_FLAG, base_ref],
|
|
426
|
-
working_directory=repo_root,
|
|
427
|
-
)
|
|
428
|
-
return completed.returncode == 0
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def _run_git(
|
|
432
|
-
all_git_arguments: list[str],
|
|
433
|
-
working_directory: Path,
|
|
434
|
-
) -> subprocess.CompletedProcess[str]:
|
|
435
|
-
return subprocess.run(
|
|
436
|
-
[GIT_COMMAND, *all_git_arguments],
|
|
437
|
-
cwd=str(working_directory),
|
|
438
|
-
check=False,
|
|
439
|
-
capture_output=True,
|
|
440
|
-
text=True,
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
|
|
444
342
|
if __name__ == "__main__":
|
|
445
343
|
sys.exit(main())
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Git commands behind the fresh-branch worktree creator.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
create_worktree_branch(repo_root, "fix/x", worktree_path, "origin/main")
|
|
6
|
+
# ok: git worktree add -b fix/x --no-track <path> origin/main
|
|
7
|
+
# flag: git worktree add -b fix/x <path> origin/main
|
|
8
|
+
|
|
9
|
+
Git auto-tracks a remote-tracking start point, leaving the new branch with
|
|
10
|
+
``branch.<name>.merge = refs/heads/main``. Under ``push.default = upstream`` a
|
|
11
|
+
bare ``git push`` from that branch fast-forwards straight onto main, so every
|
|
12
|
+
worktree here is created with ``GIT_NO_TRACK_FLAG``.
|
|
13
|
+
|
|
14
|
+
The fetch, resolve, and worktree-add commands raise on failure. ``run_git``
|
|
15
|
+
returns the completed process without raising, and ``is_ref_present`` reads its
|
|
16
|
+
return code to answer a bool.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
25
|
+
ERROR_BASE_COMMIT_LOOKUP,
|
|
26
|
+
ERROR_BASE_REF_MISSING,
|
|
27
|
+
ERROR_BRANCH_NAME_UNSAFE,
|
|
28
|
+
ERROR_FETCH_FAILED,
|
|
29
|
+
ERROR_REPO_NOT_GIT,
|
|
30
|
+
ERROR_WORKTREE_FAILED,
|
|
31
|
+
GIT_BRANCH_FLAG,
|
|
32
|
+
GIT_BRANCH_FORMAT_FLAG,
|
|
33
|
+
GIT_CHECK_REF_FORMAT,
|
|
34
|
+
GIT_COMMAND,
|
|
35
|
+
GIT_FETCH,
|
|
36
|
+
GIT_NO_TRACK_FLAG,
|
|
37
|
+
GIT_QUIET_FLAG,
|
|
38
|
+
GIT_REFS_REMOTES_PREFIX,
|
|
39
|
+
GIT_REMOTE_NAME_ORIGIN,
|
|
40
|
+
GIT_REMOTE_PREFIX,
|
|
41
|
+
GIT_REV_PARSE,
|
|
42
|
+
GIT_SHOW_REF,
|
|
43
|
+
GIT_SHOW_TOPLEVEL,
|
|
44
|
+
GIT_VERIFY_FLAG,
|
|
45
|
+
GIT_WORKTREE,
|
|
46
|
+
GIT_WORKTREE_ADD,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_git(
|
|
51
|
+
all_git_arguments: list[str],
|
|
52
|
+
working_directory: Path,
|
|
53
|
+
) -> subprocess.CompletedProcess[str]:
|
|
54
|
+
"""Run one git command and capture its output without raising.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
all_git_arguments: Arguments that follow the ``git`` executable.
|
|
58
|
+
working_directory: Directory the command runs in.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The completed process, carrying return code and captured text.
|
|
62
|
+
"""
|
|
63
|
+
return subprocess.run(
|
|
64
|
+
[GIT_COMMAND, *all_git_arguments],
|
|
65
|
+
cwd=str(working_directory),
|
|
66
|
+
check=False,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def assert_git_accepts_branch_name(branch_name: str) -> None:
|
|
73
|
+
"""Raise when git rejects branch_name as a branch ref format.
|
|
74
|
+
|
|
75
|
+
::
|
|
76
|
+
|
|
77
|
+
assert_git_accepts_branch_name("fix/x") # ok: returns None
|
|
78
|
+
assert_git_accepts_branch_name("fix..x") # flag: ValueError
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
branch_name: Candidate branch name.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: When ``git check-ref-format`` rejects the name.
|
|
85
|
+
"""
|
|
86
|
+
completed = run_git(
|
|
87
|
+
[GIT_CHECK_REF_FORMAT, GIT_BRANCH_FORMAT_FLAG, branch_name],
|
|
88
|
+
working_directory=Path.cwd(),
|
|
89
|
+
)
|
|
90
|
+
if completed.returncode != 0:
|
|
91
|
+
raise ValueError(ERROR_BRANCH_NAME_UNSAFE)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_repo_root(repo_path: Path) -> Path:
|
|
95
|
+
"""Return the git toplevel directory holding repo_path.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
repo_path: Any path inside the source repository.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The repository root directory.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RuntimeError: When repo_path is not inside a git repository.
|
|
105
|
+
"""
|
|
106
|
+
completed = run_git(
|
|
107
|
+
[GIT_REV_PARSE, GIT_SHOW_TOPLEVEL],
|
|
108
|
+
working_directory=repo_path,
|
|
109
|
+
)
|
|
110
|
+
if completed.returncode != 0:
|
|
111
|
+
raise RuntimeError(ERROR_REPO_NOT_GIT % repo_path)
|
|
112
|
+
return Path(completed.stdout.strip())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fetch_base_ref(repo_root: Path, base_ref: str) -> None:
|
|
116
|
+
"""Fetch base_ref into repo_root and confirm the ref then exists.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
repo_root: Source repository root.
|
|
120
|
+
base_ref: Ref to fetch, such as ``origin/main``.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
RuntimeError: When the fetch fails or the ref is still absent.
|
|
124
|
+
"""
|
|
125
|
+
remote_name, remote_branch = split_remote_ref(base_ref)
|
|
126
|
+
completed = run_git(
|
|
127
|
+
[GIT_FETCH, remote_name, remote_branch],
|
|
128
|
+
working_directory=repo_root,
|
|
129
|
+
)
|
|
130
|
+
if completed.returncode != 0:
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
ERROR_FETCH_FAILED % (base_ref, read_failure_text(completed)),
|
|
133
|
+
)
|
|
134
|
+
if not is_ref_present(repo_root, base_ref):
|
|
135
|
+
raise RuntimeError(ERROR_BASE_REF_MISSING % base_ref)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_base_commit(repo_root: Path, base_ref: str) -> str:
|
|
139
|
+
"""Return the commit SHA that base_ref points at.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
repo_root: Source repository root.
|
|
143
|
+
base_ref: Ref to resolve, such as ``origin/main``.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The full commit SHA at base_ref.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
RuntimeError: When git cannot resolve the ref to a commit.
|
|
150
|
+
"""
|
|
151
|
+
completed = run_git(
|
|
152
|
+
[GIT_REV_PARSE, base_ref],
|
|
153
|
+
working_directory=repo_root,
|
|
154
|
+
)
|
|
155
|
+
if completed.returncode != 0:
|
|
156
|
+
raise RuntimeError(
|
|
157
|
+
ERROR_BASE_COMMIT_LOOKUP % (base_ref, read_failure_text(completed)),
|
|
158
|
+
)
|
|
159
|
+
return completed.stdout.strip()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_worktree_branch(
|
|
163
|
+
repo_root: Path,
|
|
164
|
+
branch_name: str,
|
|
165
|
+
worktree_path: Path,
|
|
166
|
+
base_ref: str,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Create branch_name at base_ref in a new worktree, with no upstream.
|
|
169
|
+
|
|
170
|
+
::
|
|
171
|
+
|
|
172
|
+
create_worktree_branch(repo_root, "fix/x", path, "origin/main")
|
|
173
|
+
# ok: branch.fix/x.merge is unset
|
|
174
|
+
# flag: branch.fix/x.merge = refs/heads/main (auto-tracked)
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
repo_root: Source repository root.
|
|
178
|
+
branch_name: New branch to create.
|
|
179
|
+
worktree_path: Directory that will hold the new worktree.
|
|
180
|
+
base_ref: Start point for the new branch.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
RuntimeError: When ``git worktree add`` fails.
|
|
184
|
+
"""
|
|
185
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
completed = run_git(
|
|
187
|
+
build_worktree_add_arguments(branch_name, worktree_path, base_ref),
|
|
188
|
+
working_directory=repo_root,
|
|
189
|
+
)
|
|
190
|
+
if completed.returncode != 0:
|
|
191
|
+
raise RuntimeError(ERROR_WORKTREE_FAILED % read_failure_text(completed))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def build_worktree_add_arguments(
|
|
195
|
+
branch_name: str,
|
|
196
|
+
worktree_path: Path,
|
|
197
|
+
base_ref: str,
|
|
198
|
+
) -> list[str]:
|
|
199
|
+
"""Return the ``git worktree add`` argument list, including --no-track.
|
|
200
|
+
|
|
201
|
+
::
|
|
202
|
+
|
|
203
|
+
build_worktree_add_arguments("fix/x", Path("/tmp/x"), "origin/main")
|
|
204
|
+
# ok: ["worktree", "add", "-b", "fix/x", "--no-track", "/tmp/x",
|
|
205
|
+
# "origin/main"]
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
branch_name: New branch to create.
|
|
209
|
+
worktree_path: Directory that will hold the new worktree.
|
|
210
|
+
base_ref: Start point for the new branch.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Arguments to pass to ``run_git``.
|
|
214
|
+
"""
|
|
215
|
+
return [
|
|
216
|
+
GIT_WORKTREE,
|
|
217
|
+
GIT_WORKTREE_ADD,
|
|
218
|
+
GIT_BRANCH_FLAG,
|
|
219
|
+
branch_name,
|
|
220
|
+
GIT_NO_TRACK_FLAG,
|
|
221
|
+
str(worktree_path),
|
|
222
|
+
base_ref,
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def split_remote_ref(base_ref: str) -> tuple[str, str]:
|
|
227
|
+
"""Split base_ref into its remote name and branch name.
|
|
228
|
+
|
|
229
|
+
::
|
|
230
|
+
|
|
231
|
+
split_remote_ref("origin/main") # ok: ("origin", "main")
|
|
232
|
+
split_remote_ref("upstream/dev") # ok: ("upstream", "dev")
|
|
233
|
+
split_remote_ref("main") # ok: ("origin", "main")
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
base_ref: Base ref or bare branch name.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The remote name and the branch name to fetch.
|
|
240
|
+
"""
|
|
241
|
+
if base_ref.startswith(GIT_REMOTE_PREFIX):
|
|
242
|
+
return GIT_REMOTE_NAME_ORIGIN, base_ref[len(GIT_REMOTE_PREFIX) :]
|
|
243
|
+
if "/" in base_ref:
|
|
244
|
+
remote_name, remote_branch = base_ref.split("/", 1)
|
|
245
|
+
return remote_name, remote_branch
|
|
246
|
+
return GIT_REMOTE_NAME_ORIGIN, base_ref
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def is_ref_present(repo_root: Path, base_ref: str) -> bool:
|
|
250
|
+
"""Report whether base_ref resolves in repo_root.
|
|
251
|
+
|
|
252
|
+
Checks the remote-tracking ref first, then base_ref as written, so both
|
|
253
|
+
``origin/main`` and a local tag or branch answer correctly.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
repo_root: Source repository root.
|
|
257
|
+
base_ref: Ref to look for.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
True when git resolves the ref.
|
|
261
|
+
"""
|
|
262
|
+
remote_ref = f"{GIT_REFS_REMOTES_PREFIX}{base_ref}"
|
|
263
|
+
completed = run_git(
|
|
264
|
+
[GIT_SHOW_REF, GIT_VERIFY_FLAG, GIT_QUIET_FLAG, remote_ref],
|
|
265
|
+
working_directory=repo_root,
|
|
266
|
+
)
|
|
267
|
+
if completed.returncode == 0:
|
|
268
|
+
return True
|
|
269
|
+
completed = run_git(
|
|
270
|
+
[GIT_REV_PARSE, GIT_VERIFY_FLAG, GIT_QUIET_FLAG, base_ref],
|
|
271
|
+
working_directory=repo_root,
|
|
272
|
+
)
|
|
273
|
+
return completed.returncode == 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def read_failure_text(completed: subprocess.CompletedProcess[str]) -> str:
|
|
277
|
+
"""Return the message a failed git command left behind.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
completed: A completed git process with a non-zero return code.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Trimmed stderr, falling back to trimmed stdout.
|
|
284
|
+
"""
|
|
285
|
+
return completed.stderr.strip() or completed.stdout.strip()
|