claude-dev-env 2.0.1 → 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 (127) hide show
  1. package/CLAUDE.md +1 -1
  2. package/agents/clean-coder.md +31 -1
  3. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  4. package/bin/install.test.mjs +2 -2
  5. package/docs/CLAUDE.md +6 -3
  6. package/docs/CODE_RULES.md +5 -1
  7. package/docs/agent-spawn-protocol.md +39 -0
  8. package/docs/nas-ssh-invocation.md +23 -0
  9. package/docs/worker-completion-gate.md +33 -0
  10. package/hooks/blocking/CLAUDE.md +1 -0
  11. package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
  12. package/hooks/blocking/code_rules_shared.py +126 -47
  13. package/hooks/blocking/config/CLAUDE.md +2 -0
  14. package/hooks/blocking/config/verified_commit_constants.py +8 -1
  15. package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
  16. package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
  17. package/hooks/blocking/conftest.py +36 -30
  18. package/hooks/blocking/convergence_gate_blocker.py +76 -8
  19. package/hooks/blocking/plain_language_blocker.py +8 -0
  20. package/hooks/blocking/state_description_blocker.py +4 -1
  21. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
  23. package/hooks/blocking/test_code_rules_shared.py +120 -20
  24. package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
  25. package/hooks/blocking/test_plain_language_blocker.py +15 -0
  26. package/hooks/blocking/test_state_description_blocker.py +15 -0
  27. package/hooks/blocking/test_verification_verdict_store.py +12 -0
  28. package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
  29. package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
  30. package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
  31. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
  32. package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
  33. package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
  34. package/hooks/blocking/verified_commit_gate.py +113 -568
  35. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
  36. package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
  37. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
  38. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
  39. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
  40. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
  41. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
  42. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
  43. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
  44. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
  45. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
  46. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
  47. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
  48. package/hooks/hooks_constants/CLAUDE.md +4 -1
  49. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  50. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
  51. package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
  52. package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
  53. package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
  54. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  55. package/hooks/validators/mypy_integration.py +145 -24
  56. package/hooks/validators/python_antipattern_checks.py +16 -0
  57. package/hooks/validators/run_all_validators.py +9 -3
  58. package/hooks/validators/test_mypy_integration.py +154 -0
  59. package/hooks/validators/test_python_antipattern_checks.py +112 -1
  60. package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
  61. package/package.json +1 -1
  62. package/rules/CLAUDE.md +3 -3
  63. package/rules/agent-spawn-protocol.md +5 -43
  64. package/rules/code-standards.md +1 -36
  65. package/rules/env-var-table-code-drift.md +2 -21
  66. package/rules/hook-prose-matches-detector.md +5 -16
  67. package/rules/nas-ssh-invocation.md +3 -15
  68. package/rules/no-historical-clutter.md +7 -49
  69. package/rules/no-inline-destructive-literals.md +3 -5
  70. package/rules/package-inventory-stale-entry.md +7 -32
  71. package/rules/re-stage-before-commit.md +6 -23
  72. package/rules/shell-invocation-policy.md +1 -1
  73. package/rules/vault-context.md +3 -3
  74. package/rules/verified-commit-gate-skip.md +28 -0
  75. package/rules/workers-done-before-complete.md +2 -30
  76. package/scripts/claude_chain_runner.py +39 -3
  77. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
  78. package/scripts/test_claude_chain_runner.py +112 -0
  79. package/scripts/test_grok_headless_runner.py +0 -1
  80. package/skills/auditing-claude-config/CLAUDE.md +2 -1
  81. package/skills/auditing-claude-config/SKILL.md +114 -176
  82. package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
  83. package/skills/autoconverge/CLAUDE.md +1 -0
  84. package/skills/autoconverge/SKILL.md +310 -346
  85. package/skills/autoconverge/reference/CLAUDE.md +1 -0
  86. package/skills/autoconverge/reference/copilot-findings.md +51 -0
  87. package/skills/closeout/SKILL.md +7 -9
  88. package/skills/copilot-finding-triage/SKILL.md +5 -7
  89. package/skills/copilot-review/CLAUDE.md +3 -2
  90. package/skills/copilot-review/SKILL.md +119 -155
  91. package/skills/copilot-review/templates/subagent-prompt.md +49 -0
  92. package/skills/everything-search/SKILL.md +7 -1
  93. package/skills/fresh-branch/CLAUDE.md +6 -9
  94. package/skills/fresh-branch/SKILL.md +84 -33
  95. package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
  96. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
  97. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
  98. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
  99. package/skills/orchestrator/SKILL.md +161 -147
  100. package/skills/pr-converge/SKILL.md +277 -422
  101. package/skills/pr-converge/reference/CLAUDE.md +1 -0
  102. package/skills/pr-converge/reference/progress-checklist.md +168 -0
  103. package/skills/pr-converge/test_step5_host_branch.py +8 -6
  104. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  105. package/skills/privacy-hygiene/SKILL.md +68 -115
  106. package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
  107. package/skills/session-log/CLAUDE.md +2 -1
  108. package/skills/session-log/SKILL.md +4 -26
  109. package/skills/session-log/templates/frontmatter.md +40 -0
  110. package/skills/skill-builder/CLAUDE.md +8 -7
  111. package/skills/skill-builder/SKILL.md +26 -11
  112. package/skills/skill-builder/references/CLAUDE.md +3 -1
  113. package/skills/skill-builder/references/delegation-map.md +21 -12
  114. package/skills/skill-builder/references/description-field.md +9 -11
  115. package/skills/skill-builder/references/deterministic-elements.md +218 -0
  116. package/skills/skill-builder/references/self-audit-checklist.md +62 -45
  117. package/skills/skill-builder/references/skill-modularity.md +8 -9
  118. package/skills/skill-builder/templates/CLAUDE.md +2 -2
  119. package/skills/skill-builder/templates/gap-analysis.md +15 -0
  120. package/skills/skill-builder/workflows/CLAUDE.md +5 -5
  121. package/skills/skill-builder/workflows/improve-skill.md +18 -9
  122. package/skills/skill-builder/workflows/new-skill.md +23 -15
  123. package/skills/skill-builder/workflows/polish-skill.md +28 -21
  124. package/system-prompts/CLAUDE.md +3 -3
  125. package/docs/agents-md-alignment-plan.md +0 -123
  126. package/docs/emotion-informed-prompt-design.md +0 -362
  127. package/rules/es-exe-file-search.md +0 -17
@@ -1,71 +1,122 @@
1
1
  ---
2
2
  name: fresh-branch
3
- description: Creates a fresh branch for the current repo based on origin main. Always fetches actual origin main rather than relying on local main. Suggests possible branch names via AskUserQuestion when context is available, or prompts the user to provide a name directly. Triggers on "fresh branch", "new branch from main", "/fresh-branch", "start fresh".
3
+ description: >-
4
+ Fresh git branch from origin/main in an isolated temp worktree (never checkout -b in the caller tree).
5
+ Triggers: fresh branch, new branch from main, /fresh-branch, start fresh, clean branch off main,
6
+ worktree branch, branch in temp.
4
7
  ---
5
8
 
6
9
  # fresh-branch
7
10
 
8
- ## Overview
9
-
10
- Creates a new branch from `origin/main` (always fresh-fetched, never stale local main). Designed as a shared primitive: other skills (e.g. gotcha) invoke `/fresh-branch` to create a clean branch for their own PR workflows.
11
+ Creates a new branch from a fresh-fetched `origin/main` inside an isolated git worktree under the agent temp root. Shared primitive: other skills invoke `/fresh-branch` when they need a clean branch without touching the caller's dirty tree.
11
12
 
12
13
  **Announce at start:** "Creating a fresh branch from origin/main."
13
14
 
14
- ## Instructions
15
+ ## When this applies
16
+
17
+ - User or caller wants a **new** branch based on current `origin/main`.
18
+ - Caller needs the branch path/name as a return value for a later PR step.
19
+
20
+ **Does not apply (refuse with the quoted line):**
15
21
 
16
- ### Phase 1 Fetch origin main
22
+ - Switch to an existing branch → `Use git switch / checkout for an existing branch; /fresh-branch only creates new ones.`
23
+ - Push or open a PR → `This skill only creates the branch worktree; push and PR are separate.`
17
24
 
18
- Always fetch `origin/main` directly. Do not rely on the local `main` branch, which may be stale.
25
+ ## Checklist
19
26
 
20
27
  ```
21
- git fetch origin main
28
+ - [ ] Phase 1 — confirm repo context (cwd or --repo)
29
+ - [ ] Phase 2 — resolve branch name (AskUserQuestion when needed)
30
+ - [ ] Phase 3 — execute the create script; parse JSON
31
+ - [ ] Phase 4 — report branch, worktree_path, base_commit; return to caller
22
32
  ```
23
33
 
24
- Confirm the fetch succeeded. If it fails (no network, no remote), report the error and stop.
34
+ ## Phase 1 Repo context
25
35
 
26
- ### Phase 2 Determine branch name
36
+ Work against the repository the user has open (or the path the caller names). The script resolves the git toplevel from `--repo` (default `.`). Do not invent a different clone.
27
37
 
28
- Branch names follow the repo's convention: lowercase, hyphen-separated, descriptive prefix.
38
+ ## Phase 2 Branch name (high freedom)
29
39
 
30
- **When context is available (the caller or prior conversation provides a topic):**
40
+ Branch names: lowercase, hyphen-separated, optional `prefix/description`.
31
41
 
32
- Suggest 2–4 branch names via `AskUserQuestion`. The suggestions should be short, descriptive, and follow the `prefix/description` or `description` convention visible in recent `git log` output.
33
-
34
- Poll recent branch naming patterns to inform suggestions:
42
+ **When a topic is available:** suggest 2–4 names via `AskUserQuestion`. Poll recent remote names for local convention:
35
43
 
36
44
  ```
37
- git branch -r --sort=-committerdate | head -20
45
+ git branch -r --sort=-committerdate
38
46
  ```
39
47
 
40
- **When no context is available:**
48
+ (Take a short head of that list in the shell you use; do not require Unix `head`.)
49
+
50
+ **When no topic:** ask for a name via `AskUserQuestion` (`multiSelect: false`, free-text option).
41
51
 
42
- Ask the user to provide a branch name directly via `AskUserQuestion` with `multiSelect: false` and a free-text option.
52
+ ## Phase 3 Create branch (execute the script)
43
53
 
44
- ### Phase 3 Create the branch
54
+ **Execute** the bundled CLI (do not reimplement with ad-hoc `git checkout -b`):
45
55
 
46
56
  ```
47
- git checkout -b <branch-name> origin/main
57
+ python "${CLAUDE_SKILL_DIR}/scripts/create_fresh_branch.py" --branch-name "<name>"
48
58
  ```
49
59
 
50
- The `-b` flag creates the branch and checks it out. Basing on `origin/main` (not local `main`) guarantees the branch starts from the true latest state.
60
+ Optional flags:
51
61
 
52
- Confirm success:
62
+ | Flag | Role |
63
+ |------|------|
64
+ | `--repo <path>` | Source repo (default: current directory) |
65
+ | `--agent <slug>` | Temp segment: `claude`, `grok`, `cursor`, `codex`, … |
66
+ | `--base <ref>` | Base ref (default: `origin/main`) |
53
67
 
54
- ```
55
- git rev-parse --abbrev-ref HEAD
56
- git log --oneline -1
57
- ```
68
+ Agent resolution inside the script: `--agent` → `FRESH_BRANCH_AGENT` env → host markers → `claude`.
69
+
70
+ Worktree path:
71
+
72
+ - Windows: `${USERPROFILE}/AppData/Local/Temp/<agent>/<branch-name>`
73
+ - Else: `${tmpdir}/<agent>/<branch-name>`
74
+ - If the path exists, the script suffixes `-2`, `-3`, …
58
75
 
59
- ### Phase 4 Report
76
+ On exit 0, stdout is one JSON object:
60
77
 
61
- State the new branch name and its base commit. If invoked as a subroutine by another skill, return the branch name so the caller can proceed (e.g., creating a PR from it).
78
+ | Field | Meaning |
79
+ |-------|---------|
80
+ | `branch` | Created branch name |
81
+ | `worktree_path` | Absolute path of the new worktree |
82
+ | `base_ref` | Base ref used |
83
+ | `base_commit` | SHA at that ref after fetch |
84
+ | `agent` | Host slug used in the path |
85
+ | `repo_root` | Source repository root |
62
86
 
63
- ## What this skill does NOT do
87
+ On non-zero exit, stdout is `{"error": "..."}`. Report the error and stop. Do not fall back to `git checkout -b` in the caller tree.
64
88
 
65
- - Does not push the branch. The caller decides when and whether to push.
66
- - Does not create a PR. Use `gh pr create` separately.
67
- - Does not switch an existing branch. It always creates a new one.
89
+ ## Phase 4 Report
90
+
91
+ State `branch`, `worktree_path`, and `base_commit`. When invoked as a subroutine, return those fields so the caller can continue (for example open a PR from that worktree).
92
+
93
+ Further edits for the new branch belong in `worktree_path`, not in the caller's original cwd.
94
+
95
+ ## Constraints
96
+
97
+ - Never `git checkout -b` (or equivalent) in the caller's working tree.
98
+ - Always fetch the base ref through the script (default `origin/main`).
99
+ - Do not push. Do not open a PR.
100
+ - Do not switch an existing branch; only create.
68
101
 
69
102
  ## Gotchas
70
103
 
71
- See the gotcha reference at the bottom of this file. When a new gotcha is discovered during use, invoke `/gotcha` to add it here.
104
+ - **Dirty caller cwd blocks `checkout -b` and pollutes the tree.** Phase 3 always uses `git worktree add -b` into `Temp/<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.
105
+ - **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.
106
+ - **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.
107
+ - **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.
108
+
109
+ ## File index
110
+
111
+ | File | Purpose |
112
+ |------|---------|
113
+ | `SKILL.md` | This hub: phases, checklist, constraints, gotchas |
114
+ | `CLAUDE.md` | Package map for agents browsing the skill folder |
115
+ | `scripts/create_fresh_branch.py` | **Execute** — deterministic fetch + worktree branch CLI |
116
+ | `scripts/test_create_fresh_branch.py` | Real-repo tests for agent/path/CLI behavior |
117
+ | `scripts/fresh_branch_scripts_constants/` | Named constants for the CLI |
118
+
119
+ ## Folder map
120
+
121
+ - `scripts/` — executable CLI, tests, constants package
122
+ - `scripts/fresh_branch_scripts_constants/` — importable `UPPER_SNAKE` values
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/env python3
2
+ """Create a fresh branch in an isolated git worktree under the agent scratch root.
3
+
4
+ ::
5
+
6
+ python create_fresh_branch.py --branch-name fix/example
7
+ {"branch": "fix/example", "worktree_path": "...", "base_ref": "origin/main",
8
+ "base_commit": "abc...", "agent": "claude", "repo_root": "..."}
9
+
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>`` (Windows) or
12
+ ``gettempdir()/<agent>/<branch>`` elsewhere. Exit 0 prints success JSON; any
13
+ failure prints ``{"error": ...}`` and exits non-zero.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import re
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ from pathlib import Path
26
+
27
+ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
28
+ AGENT_SLUG_PATTERN,
29
+ ALL_AGENT_DETECTION_MARKERS,
30
+ ALL_WINDOWS_USER_SCRATCH_PARTS,
31
+ DEFAULT_AGENT_SLUG,
32
+ DEFAULT_BASE_REF,
33
+ ERROR_AGENT_SLUG_INVALID,
34
+ ERROR_BASE_COMMIT_LOOKUP,
35
+ ERROR_BASE_REF_MISSING,
36
+ ERROR_BRANCH_NAME_REQUIRED,
37
+ ERROR_BRANCH_NAME_UNSAFE,
38
+ ERROR_CLI_ARGUMENTS,
39
+ ERROR_FETCH_FAILED,
40
+ ERROR_REPO_NOT_GIT,
41
+ ERROR_UNIQUE_PATH_EXHAUSTED,
42
+ ERROR_WORKTREE_FAILED,
43
+ ERROR_WORKTREE_PATH_OUTSIDE_ROOT,
44
+ EXIT_CODE_FAILURE,
45
+ EXIT_CODE_SUCCESS,
46
+ 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
+ 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
+ MAXIMUM_UNIQUE_PATH_ATTEMPTS,
63
+ PATH_SEGMENT_CURRENT,
64
+ PATH_SEGMENT_PARENT,
65
+ PAYLOAD_KEY_AGENT,
66
+ PAYLOAD_KEY_BASE_COMMIT,
67
+ PAYLOAD_KEY_BASE_REF,
68
+ PAYLOAD_KEY_BRANCH,
69
+ PAYLOAD_KEY_ERROR,
70
+ PAYLOAD_KEY_REPO_ROOT,
71
+ PAYLOAD_KEY_WORKTREE_PATH,
72
+ UNIQUE_PATH_SUFFIX_START,
73
+ USERPROFILE_ENV_VAR,
74
+ WINDOWS_PLATFORM_PREFIX,
75
+ )
76
+
77
+
78
+ def resolve_agent_slug(maybe_flag_agent: str | None) -> str:
79
+ """Return the short host label for the worktree path segment.
80
+
81
+ ::
82
+
83
+ resolve_agent_slug("Grok") # ok: "grok" (flag wins)
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``.
90
+
91
+ Args:
92
+ maybe_flag_agent: Explicit ``--agent`` value, or None.
93
+
94
+ Returns:
95
+ Lowercase agent slug safe for a single path segment.
96
+
97
+ Raises:
98
+ ValueError: When the resolved slug fails the path-safety pattern.
99
+ """
100
+ if maybe_flag_agent is not None and maybe_flag_agent.strip():
101
+ return _normalize_agent_slug(maybe_flag_agent)
102
+ explicit_agent = os.environ.get(FRESH_BRANCH_AGENT_ENV_VAR)
103
+ if explicit_agent is not None and explicit_agent.strip():
104
+ return _normalize_agent_slug(explicit_agent)
105
+ for each_env_name, each_agent_slug in ALL_AGENT_DETECTION_MARKERS:
106
+ maybe_marker = os.environ.get(each_env_name)
107
+ if maybe_marker is not None and str(maybe_marker).strip():
108
+ return each_agent_slug
109
+ return DEFAULT_AGENT_SLUG
110
+
111
+
112
+ def resolve_agent_worktree_root(agent_slug: str) -> Path:
113
+ """Return ``Temp/<agent>`` on Windows USERPROFILE, else gettempdir root.
114
+
115
+ ::
116
+
117
+ # win32 + USERPROFILE=C:/Users/x -> C:/Users/x/AppData/Local/Temp/grok
118
+ resolve_agent_worktree_root("grok")
119
+
120
+ Args:
121
+ agent_slug: Short host label (one path segment).
122
+
123
+ Returns:
124
+ Directory that should hold per-branch worktree folders.
125
+ """
126
+ if sys.platform.startswith(WINDOWS_PLATFORM_PREFIX):
127
+ user_profile = os.environ.get(USERPROFILE_ENV_VAR)
128
+ if user_profile:
129
+ return Path(user_profile).joinpath(
130
+ *ALL_WINDOWS_USER_SCRATCH_PARTS,
131
+ agent_slug,
132
+ )
133
+ return Path(tempfile.gettempdir()) / agent_slug
134
+
135
+
136
+ def resolve_unique_worktree_path(preferred_path: Path) -> Path:
137
+ """Return preferred_path, or preferred_path-N when the path already exists.
138
+
139
+ ::
140
+
141
+ resolve_unique_worktree_path(Path("/tmp/claude/fix")) # free path
142
+ # when /tmp/claude/fix exists -> /tmp/claude/fix-2
143
+
144
+ Args:
145
+ preferred_path: First-choice worktree directory.
146
+
147
+ Returns:
148
+ A path that does not exist yet.
149
+
150
+ Raises:
151
+ RuntimeError: When no free suffix remains within the attempt budget.
152
+ """
153
+ if not preferred_path.exists():
154
+ return preferred_path
155
+ for each_suffix in range(
156
+ UNIQUE_PATH_SUFFIX_START,
157
+ UNIQUE_PATH_SUFFIX_START + MAXIMUM_UNIQUE_PATH_ATTEMPTS,
158
+ ):
159
+ candidate_path = preferred_path.parent / f"{preferred_path.name}-{each_suffix}"
160
+ if not candidate_path.exists():
161
+ return candidate_path
162
+ raise RuntimeError(ERROR_UNIQUE_PATH_EXHAUSTED % preferred_path)
163
+
164
+
165
+ def normalize_base_ref(base_ref: str) -> str:
166
+ """Return a remote-tracking ref for bare branch names.
167
+
168
+ ::
169
+
170
+ normalize_base_ref("main") # ok: "origin/main"
171
+ normalize_base_ref("origin/main") # ok: "origin/main"
172
+ normalize_base_ref("upstream/dev") # ok: "upstream/dev"
173
+
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
+ Args:
178
+ base_ref: User-supplied base ref or bare branch name.
179
+
180
+ Returns:
181
+ Normalized ref used for fetch, commit lookup, and worktree add.
182
+ """
183
+ cleaned_base_ref = base_ref.strip()
184
+ if not cleaned_base_ref:
185
+ return DEFAULT_BASE_REF
186
+ if "/" not in cleaned_base_ref:
187
+ return f"{GIT_REMOTE_PREFIX}{cleaned_base_ref}"
188
+ return cleaned_base_ref
189
+
190
+
191
+ def create_fresh_branch(
192
+ branch_name: str,
193
+ repo_path: Path,
194
+ agent_slug: str,
195
+ base_ref: str,
196
+ ) -> dict[str, str]:
197
+ """Fetch base_ref and create an isolated worktree branch.
198
+
199
+ ::
200
+
201
+ 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
+ """
218
+ cleaned_branch = branch_name.strip()
219
+ if not cleaned_branch:
220
+ raise ValueError(ERROR_BRANCH_NAME_REQUIRED)
221
+ _validate_branch_name_for_worktree_path(cleaned_branch)
222
+ normalized_agent_slug = _normalize_agent_slug(agent_slug)
223
+ resolved_base_ref = normalize_base_ref(base_ref)
224
+ repo_root = _resolve_repo_root(repo_path)
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,
237
+ )
238
+ _create_worktree_branch(
239
+ repo_root,
240
+ branch_name=cleaned_branch,
241
+ worktree_path=worktree_path,
242
+ base_ref=resolved_base_ref,
243
+ )
244
+ return {
245
+ PAYLOAD_KEY_BRANCH: cleaned_branch,
246
+ PAYLOAD_KEY_WORKTREE_PATH: str(worktree_path.resolve()),
247
+ PAYLOAD_KEY_BASE_REF: resolved_base_ref,
248
+ PAYLOAD_KEY_BASE_COMMIT: base_commit,
249
+ PAYLOAD_KEY_AGENT: normalized_agent_slug,
250
+ PAYLOAD_KEY_REPO_ROOT: str(repo_root),
251
+ }
252
+
253
+
254
+ def main() -> int:
255
+ """CLI entry: create worktree branch and print JSON to stdout.
256
+
257
+ Returns:
258
+ Process exit code (0 success, 1 failure).
259
+ """
260
+ try:
261
+ parsed_arguments = _parse_arguments()
262
+ agent_slug = resolve_agent_slug(parsed_arguments.agent)
263
+ success_payload = create_fresh_branch(
264
+ branch_name=parsed_arguments.branch_name,
265
+ repo_path=Path(parsed_arguments.repo).resolve(),
266
+ agent_slug=agent_slug,
267
+ base_ref=parsed_arguments.base,
268
+ )
269
+ print(json.dumps(success_payload))
270
+ return EXIT_CODE_SUCCESS
271
+ except (ValueError, RuntimeError, OSError) as error:
272
+ print(json.dumps({PAYLOAD_KEY_ERROR: str(error)}))
273
+ return EXIT_CODE_FAILURE
274
+
275
+
276
+ def _normalize_agent_slug(raw_agent: str) -> str:
277
+ agent_slug = raw_agent.strip().lower()
278
+ if re.fullmatch(AGENT_SLUG_PATTERN, agent_slug) is None:
279
+ raise ValueError(ERROR_AGENT_SLUG_INVALID)
280
+ return agent_slug
281
+
282
+
283
+ def _validate_branch_name_for_worktree_path(branch_name: str) -> None:
284
+ branch_as_path = Path(branch_name)
285
+ if branch_as_path.is_absolute():
286
+ raise ValueError(ERROR_BRANCH_NAME_UNSAFE)
287
+ for each_segment in branch_as_path.parts:
288
+ if each_segment in (PATH_SEGMENT_CURRENT, PATH_SEGMENT_PARENT):
289
+ raise ValueError(ERROR_BRANCH_NAME_UNSAFE)
290
+ _assert_git_accepts_branch_name(branch_name)
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)
300
+
301
+
302
+ def _assert_path_is_under_agent_root(
303
+ candidate_path: Path,
304
+ agent_worktree_root: Path,
305
+ ) -> None:
306
+ resolved_candidate = candidate_path.resolve()
307
+ resolved_root = agent_worktree_root.resolve()
308
+ if resolved_candidate == resolved_root:
309
+ return
310
+ try:
311
+ resolved_candidate.relative_to(resolved_root)
312
+ except ValueError as error:
313
+ raise ValueError(
314
+ ERROR_WORKTREE_PATH_OUTSIDE_ROOT % candidate_path,
315
+ ) from error
316
+
317
+
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
+ def _parse_arguments() -> argparse.Namespace:
376
+ parser = argparse.ArgumentParser(
377
+ description="Create a fresh branch in an isolated git worktree.",
378
+ )
379
+ parser.add_argument(
380
+ "--branch-name",
381
+ required=True,
382
+ help="Name of the new branch to create.",
383
+ )
384
+ parser.add_argument(
385
+ "--repo",
386
+ default=".",
387
+ help="Path inside the source repository (default: current directory).",
388
+ )
389
+ parser.add_argument(
390
+ "--agent",
391
+ default=None,
392
+ help="Host label for Temp/<agent>/ (default: detect from environment).",
393
+ )
394
+ parser.add_argument(
395
+ "--base",
396
+ default=DEFAULT_BASE_REF,
397
+ help=f"Base ref to fetch and branch from (default: {DEFAULT_BASE_REF}).",
398
+ )
399
+ try:
400
+ return parser.parse_args()
401
+ except SystemExit as exit_error:
402
+ if exit_error.code in (EXIT_CODE_SUCCESS, None):
403
+ raise
404
+ raise ValueError(ERROR_CLI_ARGUMENTS) from exit_error
405
+
406
+
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
+ if __name__ == "__main__":
445
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Named constants for the fresh-branch skill scripts."""
@@ -0,0 +1,74 @@
1
+ """Constants for the fresh-branch worktree creator.
2
+
3
+ Groups: default refs, agent detection, path layout, JSON payload keys,
4
+ exit codes, and unique-path suffix limits.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ DEFAULT_BASE_REF = "origin/main"
10
+ DEFAULT_AGENT_SLUG = "claude"
11
+ FRESH_BRANCH_AGENT_ENV_VAR = "FRESH_BRANCH_AGENT"
12
+
13
+ ALL_AGENT_DETECTION_MARKERS: tuple[tuple[str, str], ...] = (
14
+ ("CURSOR_AGENT", "cursor"),
15
+ ("CURSOR_TRACE_ID", "cursor"),
16
+ ("CODEX_HOME", "codex"),
17
+ ("CODEX_CI", "codex"),
18
+ ("GROK_AGENT", "grok"),
19
+ ("CLAUDECODE", "claude"),
20
+ ("CLAUDE_CODE_ENTRYPOINT", "claude"),
21
+ )
22
+
23
+ PATH_SEGMENT_CURRENT = "."
24
+ PATH_SEGMENT_PARENT = ".."
25
+
26
+ WINDOWS_PLATFORM_PREFIX = "win"
27
+ ALL_WINDOWS_USER_SCRATCH_PARTS = ("AppData", "Local", "Temp")
28
+ USERPROFILE_ENV_VAR = "USERPROFILE"
29
+
30
+ MAXIMUM_UNIQUE_PATH_ATTEMPTS = 100
31
+ UNIQUE_PATH_SUFFIX_START = 2
32
+
33
+ PAYLOAD_KEY_BRANCH = "branch"
34
+ PAYLOAD_KEY_WORKTREE_PATH = "worktree_path"
35
+ PAYLOAD_KEY_BASE_REF = "base_ref"
36
+ PAYLOAD_KEY_BASE_COMMIT = "base_commit"
37
+ PAYLOAD_KEY_AGENT = "agent"
38
+ PAYLOAD_KEY_REPO_ROOT = "repo_root"
39
+ PAYLOAD_KEY_ERROR = "error"
40
+
41
+ EXIT_CODE_SUCCESS = 0
42
+ EXIT_CODE_FAILURE = 1
43
+
44
+ GIT_COMMAND = "git"
45
+ GIT_FETCH = "fetch"
46
+ GIT_REV_PARSE = "rev-parse"
47
+ GIT_SHOW_REF = "show-ref"
48
+ GIT_WORKTREE = "worktree"
49
+ GIT_WORKTREE_ADD = "add"
50
+ GIT_BRANCH_FLAG = "-b"
51
+ GIT_VERIFY_FLAG = "--verify"
52
+ GIT_QUIET_FLAG = "--quiet"
53
+ GIT_SHOW_TOPLEVEL = "--show-toplevel"
54
+ GIT_CHECK_REF_FORMAT = "check-ref-format"
55
+ GIT_BRANCH_FORMAT_FLAG = "--branch"
56
+ GIT_REMOTE_PREFIX = "origin/"
57
+ GIT_REFS_REMOTES_PREFIX = "refs/remotes/"
58
+ GIT_REMOTE_NAME_ORIGIN = "origin"
59
+
60
+ ERROR_BRANCH_NAME_REQUIRED = "branch name is required"
61
+ ERROR_BRANCH_NAME_UNSAFE = (
62
+ "branch name must be a relative path without '.' or '..' segments"
63
+ )
64
+ ERROR_WORKTREE_PATH_OUTSIDE_ROOT = "worktree path escapes agent root: %s"
65
+ ERROR_CLI_ARGUMENTS = "invalid or missing command-line arguments"
66
+ ERROR_AGENT_SLUG_INVALID = "agent slug must be lowercase letters, digits, or hyphens"
67
+ ERROR_REPO_NOT_GIT = "path is not inside a git repository: %s"
68
+ ERROR_BASE_REF_MISSING = "base ref not found after fetch: %s"
69
+ ERROR_FETCH_FAILED = "git fetch failed for %s: %s"
70
+ ERROR_WORKTREE_FAILED = "git worktree add failed: %s"
71
+ ERROR_UNIQUE_PATH_EXHAUSTED = "could not allocate a unique worktree path under %s"
72
+ ERROR_BASE_COMMIT_LOOKUP = "could not resolve base commit for %s: %s"
73
+
74
+ AGENT_SLUG_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"