claude-dev-env 2.7.1 → 2.8.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/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +1 -1
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/spawn_grok_batch.py +38 -1
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +101 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -9,8 +9,15 @@ Run `git diff @{upstream}...HEAD` (or `git diff main...HEAD` / `git diff HEAD~1`
|
|
|
9
9
|
if there's no upstream) to get the unified diff under review. If there are
|
|
10
10
|
uncommitted changes, or the range diff is empty, also run `git diff HEAD` and
|
|
11
11
|
include the working-tree changes in scope — the review often runs before the
|
|
12
|
-
commit. If a
|
|
13
|
-
|
|
12
|
+
commit. If a target was passed as an argument, review that target instead. A
|
|
13
|
+
target names one or more items, each a PR number, a branch name, a file path, or
|
|
14
|
+
`default-range` — the diff this phase gathers when no target is passed — and it
|
|
15
|
+
may mix those forms. A loop round widens a target by adding a path to whatever
|
|
16
|
+
it started as, and it names `default-range` as an item whenever the round it
|
|
17
|
+
widened was given no target argument, so the original scope stays under review.
|
|
18
|
+
When a target names more than one item, gather each item's diff and take their
|
|
19
|
+
union — a shared hunk counted once, an empty one adding nothing — as the
|
|
20
|
+
target's diff. Treat this diff as the review scope.
|
|
14
21
|
|
|
15
22
|
## Phase 1 — Find candidates (3 correctness angles + 3 cleanup angles + 1 altitude angle + 1 conventions angle)
|
|
16
23
|
|
|
@@ -116,23 +123,41 @@ Keep candidates where the vote is CONFIRMED or PLAUSIBLE.
|
|
|
116
123
|
|
|
117
124
|
Report this review's results — `{level, findings}` — through the structured
|
|
118
125
|
findings-report call: the mechanism that renders a review's results as a typed
|
|
119
|
-
list in the host UI, ranked most-severe first. Each entry
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
list in the host UI, ranked most-severe first. Each **retained** entry carries
|
|
127
|
+
every field below. Drop REFUTED candidates before this report; do not emit a
|
|
128
|
+
finding that lacks `severity` or `verdict`.
|
|
129
|
+
|
|
130
|
+
| Field | Required | Value |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `file` | yes | path under review |
|
|
133
|
+
| `line` | yes | 1-based line number |
|
|
134
|
+
| `summary` | yes | full claim |
|
|
135
|
+
| `short_summary` | yes | claim compressed to ≤60 characters, no rationale or consequence clause |
|
|
136
|
+
| `failure_scenario` | yes | concrete trigger and wrong outcome, or concrete cost for cleanup |
|
|
137
|
+
| `category` | yes | short kebab-case slug for the angle (`correctness`, `simplification`, `efficiency`, `reuse`, `altitude`, `conventions`, or a tighter slug like `test-coverage`) |
|
|
138
|
+
| `severity` | yes | one of `blocker`, `high`, `medium`, `low`, `nit` |
|
|
139
|
+
| `verdict` | yes | `CONFIRMED` or `PLAUSIBLE` from Phase 2 |
|
|
140
|
+
|
|
141
|
+
**Severity rules.** Assign exactly one token from the frozen set above. Use
|
|
142
|
+
`nit` only when all five hold: the change is localized clarity, formatting, or
|
|
143
|
+
a typo; correctness, security, data, and API behavior stay identical; test
|
|
144
|
+
coverage and operability stay identical; dependencies and rollout stay
|
|
145
|
+
identical; the fix is mechanical and bounded. Any other retained finding is
|
|
146
|
+
`low` or higher. Correctness bugs outrank cleanup, altitude, and conventions.
|
|
147
|
+
|
|
148
|
+
If nothing survives verification, make that call with an empty array. Do not
|
|
149
|
+
also print the findings as text, and do not create or publish an artifact of
|
|
150
|
+
the review — the structured call is the report.
|
|
128
151
|
|
|
129
152
|
## Applying fixes (--fix)
|
|
130
153
|
|
|
131
|
-
The `--fix` flag was passed. Follow `reference\fix.md` (relative
|
|
132
|
-
skill's folder) for the exact fix,
|
|
133
|
-
it governs which agent applies each fix, how
|
|
134
|
-
is logged, and how outcomes get reported. Do not repeat the
|
|
135
|
-
follow that document's reporting rules once fixes land.
|
|
154
|
+
The `--fix` flag was passed. Follow `reference\fix.md` (relative
|
|
155
|
+
to this skill's folder) for the exact fix, code-rules-gate, and skip-handling
|
|
156
|
+
behavior — it governs which agent applies each fix, how the code-rules gate
|
|
157
|
+
runs, how a skip is logged, and how outcomes get reported. Do not repeat the
|
|
158
|
+
findings as text; follow that document's reporting rules once fixes land.
|
|
159
|
+
|
|
160
|
+
When `loop` is also set, skip this section.
|
|
136
161
|
|
|
137
162
|
## If findings are fixed later
|
|
138
163
|
|
|
@@ -146,8 +171,17 @@ summary; the host UI's per-finding status updates only from that call.
|
|
|
146
171
|
## Looping (`loop`)
|
|
147
172
|
|
|
148
173
|
The `loop` arg was passed. Follow `reference\loop.md` (relative to this
|
|
149
|
-
skill's folder) for how to re-run Phases 0–2
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
document
|
|
174
|
+
skill's folder) for how to re-run Phases 0–2 and Output repeatedly — including
|
|
175
|
+
its exit condition and re-invocation rules. Schedule no fix pass of your own
|
|
176
|
+
here: when `--fix` is also present, `reference\loop.md`'s gate sequence owns the
|
|
177
|
+
round's fixing and loads `reference\fix.md` for the mechanics. Do not treat a
|
|
178
|
+
single pass through this document as complete while `loop` is active; hand
|
|
179
|
+
control to that document, and do not stop at Output.
|
|
180
|
+
|
|
181
|
+
That hand-off applies when this document is entered directly. When a loop round
|
|
182
|
+
is already running and has handed this document its target, the round owns the
|
|
183
|
+
loop: end at Output with the findings report and return those findings to
|
|
184
|
+
`reference\loop.md`'s gate sequence, rather than handing control to that
|
|
185
|
+
document again from here.
|
|
186
|
+
|
|
187
|
+
When `loop` was not passed, skip this section.
|
|
@@ -10,8 +10,15 @@ Run `git diff @{upstream}...HEAD` (or `git diff main...HEAD` / `git diff HEAD~1`
|
|
|
10
10
|
if there's no upstream) to get the unified diff under review. If there are
|
|
11
11
|
uncommitted changes, or the range diff is empty, also run `git diff HEAD` and
|
|
12
12
|
include the working-tree changes in scope — the review often runs before the
|
|
13
|
-
commit. If a
|
|
14
|
-
|
|
13
|
+
commit. If a target was passed as an argument, review that target instead. A
|
|
14
|
+
target names one or more items, each a PR number, a branch name, a file path, or
|
|
15
|
+
`default-range` — the diff this phase gathers when no target is passed — and it
|
|
16
|
+
may mix those forms. A loop round widens a target by adding a path to whatever
|
|
17
|
+
it started as, and it names `default-range` as an item whenever the round it
|
|
18
|
+
widened was given no target argument, so the original scope stays under review.
|
|
19
|
+
When a target names more than one item, gather each item's diff and take their
|
|
20
|
+
union — a shared hunk counted once, an empty one adding nothing — as the
|
|
21
|
+
target's diff. Treat this diff as the review scope.
|
|
15
22
|
|
|
16
23
|
## Phase 1 — Find candidates (5 correctness angles + 3 cleanup angles + 1 altitude angle + 1 conventions angle)
|
|
17
24
|
|
|
@@ -157,11 +164,13 @@ or publish an artifact of the review — the structured call is the report.
|
|
|
157
164
|
|
|
158
165
|
## Applying fixes (--fix)
|
|
159
166
|
|
|
160
|
-
The `--fix` flag was passed. Follow `reference\fix.md` (relative
|
|
161
|
-
skill's folder) for the exact fix,
|
|
162
|
-
it governs which agent applies each fix, how
|
|
163
|
-
is logged, and how outcomes get reported. Do not repeat the
|
|
164
|
-
follow that document's reporting rules once fixes land.
|
|
167
|
+
The `--fix` flag was passed. Follow `reference\fix.md` (relative
|
|
168
|
+
to this skill's folder) for the exact fix, code-rules-gate, and skip-handling
|
|
169
|
+
behavior — it governs which agent applies each fix, how the code-rules gate
|
|
170
|
+
runs, how a skip is logged, and how outcomes get reported. Do not repeat the
|
|
171
|
+
findings as text; follow that document's reporting rules once fixes land.
|
|
172
|
+
|
|
173
|
+
When `loop` is also set, skip this section.
|
|
165
174
|
|
|
166
175
|
## If findings are fixed later
|
|
167
176
|
|
|
@@ -175,8 +184,17 @@ summary; the host UI's per-finding status updates only from that call.
|
|
|
175
184
|
## Looping (`loop`)
|
|
176
185
|
|
|
177
186
|
The `loop` arg was passed. Follow `reference\loop.md` (relative to this
|
|
178
|
-
skill's folder) for how to re-run Phases 0–3
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
document
|
|
187
|
+
skill's folder) for how to re-run Phases 0–3 and Output repeatedly — including
|
|
188
|
+
its exit condition and re-invocation rules. Schedule no fix pass of your own
|
|
189
|
+
here: when `--fix` is also present, `reference\loop.md`'s gate sequence owns the
|
|
190
|
+
round's fixing and loads `reference\fix.md` for the mechanics. Do not treat a
|
|
191
|
+
single pass through this document as complete while `loop` is active; hand
|
|
192
|
+
control to that document, and do not stop at Output.
|
|
193
|
+
|
|
194
|
+
That hand-off applies when this document is entered directly. When a loop round
|
|
195
|
+
is already running and has handed this document its target, the round owns the
|
|
196
|
+
loop: end at Output with the findings report and return those findings to
|
|
197
|
+
`reference\loop.md`'s gate sequence, rather than handing control to that
|
|
198
|
+
document again from here.
|
|
199
|
+
|
|
200
|
+
When `loop` was not passed, skip this section.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# fresh-branch
|
|
2
2
|
|
|
3
|
-
Creates a new branch from fresh-fetched `origin/main` inside an isolated worktree under `<repo-root>/.claude/worktrees/<agent>/<branch-name>`. Does not push, open a PR, or run `checkout -b` in the caller tree.
|
|
3
|
+
Creates a new branch from fresh-fetched `origin/main` inside an isolated worktree under a configured root. Default: `<repo-root>/.claude/worktrees/<agent>/<branch-name>`. Optional absolute `--worktree-root` relocates the root (agent and branch still nest under it). Relative roots and path escape fail closed. Does not push, open a PR, or run `checkout -b` in the caller tree.
|
|
4
4
|
|
|
5
5
|
## Key files
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `SKILL.md` | Phases, checklist, execute-vs-read for the CLI, gotchas |
|
|
10
|
-
| `scripts/create_fresh_branch.py` | Deterministic CLI: fetch base, `git worktree add -b`, JSON stdout |
|
|
9
|
+
| `SKILL.md` | Phases, checklist, configured-root contract, execute-vs-read for the CLI, gotchas |
|
|
10
|
+
| `scripts/create_fresh_branch.py` | Deterministic CLI: resolve configured root (fail closed before fetch), fetch base, `git worktree add -b`, JSON stdout |
|
|
11
11
|
| `scripts/fresh_branch_git_commands.py` | Git command helpers: fetch, ref checks, `git worktree add -b --no-track` |
|
|
12
|
-
| `scripts/test_create_fresh_branch.py` | Behavioral tests with temporary git repos |
|
|
12
|
+
| `scripts/test_create_fresh_branch.py` | Behavioral tests with temporary git repos (default root, explicit root, escape, collisions) |
|
|
13
13
|
| `scripts/test_fresh_branch_git_commands.py` | Behavioral tests for the git command helpers |
|
|
14
|
-
| `scripts/fresh_branch_scripts_constants/` | Constants package (`fresh_branch_cli_constants`) for
|
|
14
|
+
| `scripts/fresh_branch_scripts_constants/` | Constants package (`fresh_branch_cli_constants`) for CLI flags and error strings |
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fresh-branch
|
|
3
3
|
description: >-
|
|
4
|
-
Fresh git branch from origin/main in an isolated worktree under
|
|
4
|
+
Fresh git branch from origin/main in an isolated worktree under a configured root (default: <repo>/.claude/worktrees/<agent>/; never checkout -b in the caller tree).
|
|
5
5
|
Triggers: fresh branch, new branch from main, /fresh-branch, start fresh, clean branch off main,
|
|
6
6
|
worktree branch, branch in temp.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# fresh-branch
|
|
10
10
|
|
|
11
|
-
Creates a new branch from a fresh-fetched `origin/main` inside an isolated git worktree under the repository's `.claude/worktrees
|
|
11
|
+
Creates a new branch from a fresh-fetched `origin/main` inside an isolated git worktree under a **configured isolated root**. Default root is the repository's `.claude/worktrees/`; pass `--worktree-root` for an absolute alternate root (including outside the repo). Every worktree path must resolve under that root. Shared primitive: other skills invoke `/fresh-branch` when they need a clean branch without touching the caller's dirty tree.
|
|
12
12
|
|
|
13
13
|
**Announce at start:** "Creating a fresh branch from origin/main."
|
|
14
14
|
|
|
@@ -62,15 +62,21 @@ Optional flags:
|
|
|
62
62
|
| Flag | Role |
|
|
63
63
|
|------|------|
|
|
64
64
|
| `--repo <path>` | Source repo (default: current directory) |
|
|
65
|
-
| `--agent <slug>` |
|
|
65
|
+
| `--agent <slug>` | Agent segment under the configured root: `claude`, `grok`, `cursor`, `codex`, … |
|
|
66
66
|
| `--base <ref>` | Base ref (default: `origin/main`) |
|
|
67
|
+
| `--worktree-root <abs-path>` | Absolute isolated root for all worktrees (default: `<repo-root>/.claude/worktrees`) |
|
|
67
68
|
|
|
68
69
|
Agent resolution inside the script: `--agent` → `FRESH_BRANCH_AGENT` env → host markers → `claude`.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
Configured-root contract:
|
|
71
72
|
|
|
72
|
-
- `<repo-root>/.claude/worktrees
|
|
73
|
+
- Default configured root: `<repo-root>/.claude/worktrees`
|
|
74
|
+
- Explicit `--worktree-root` must be absolute; a relative value fails closed with a deterministic error before any git fetch
|
|
75
|
+
- Absolute roots outside the repository are legal; the agent and branch still nest under that root
|
|
76
|
+
- Worktree path: `<configured-root>/<agent>/<branch-name>`, on every platform
|
|
77
|
+
- Every allocated path must resolve under the configured root; traversal that escapes fails closed
|
|
73
78
|
- If the path exists, the script suffixes `-2`, `-3`, …
|
|
79
|
+
- Permission-rule cleanup (`stale_worktree_rule_sweep`) only walks `~/.claude/worktrees`; external `--worktree-root` trees are outside that sweep until a follow-up ships
|
|
74
80
|
|
|
75
81
|
On exit 0, stdout is one JSON object:
|
|
76
82
|
|
|
@@ -100,10 +106,12 @@ Further edits for the new branch belong in `worktree_path`, not in the caller's
|
|
|
100
106
|
|
|
101
107
|
## Gotchas
|
|
102
108
|
|
|
103
|
-
- **Dirty caller cwd blocks `checkout -b` and pollutes the tree.** Phase 3 always uses `git worktree add -b` into `<
|
|
109
|
+
- **Dirty caller cwd blocks `checkout -b` and pollutes the tree.** Phase 3 always uses `git worktree add -b` into `<configured-root>/<agent>/…`. If you reconstruct Phase 3 by hand with `checkout -b` in the session cwd, local modifications block the checkout and leave the user on a half-switched branch.
|
|
104
110
|
- **Caller HEAD must stay put.** After success, the original repo's checked-out branch and dirty files are unchanged; only the new worktree has the new branch.
|
|
105
111
|
- **Branch name collision.** If the branch already exists, the script exits non-zero with `{"error":...}`. Pick a new name; do not delete remote branches unless the user asks.
|
|
106
112
|
- **Path already occupied.** A leftover folder at the preferred worktree path gets a numeric suffix (`-2`, …); report the path from JSON, not the path you assumed.
|
|
113
|
+
- **Relative `--worktree-root` is refused before fetch.** Only an absolute path is accepted; the default root applies when the flag is omitted. Validation runs before `git fetch`, so a bad root never touches the network or remote-tracking refs.
|
|
114
|
+
- **External roots and stale permission rules.** `stale_worktree_rule_sweep` only walks `~/.claude/worktrees`. Edit rules granted under an external `--worktree-root` are not swept when that tree is deleted.
|
|
107
115
|
|
|
108
116
|
## File index
|
|
109
117
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Create a fresh branch in an isolated git worktree under
|
|
2
|
+
"""Create a fresh branch in an isolated git worktree under a configured root.
|
|
3
3
|
|
|
4
4
|
::
|
|
5
5
|
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
Never runs ``git checkout -b`` in the caller's working tree. Fetches the base
|
|
11
11
|
ref, then ``git worktree add -b --no-track`` into
|
|
12
|
-
``<
|
|
13
|
-
|
|
12
|
+
``<configured-root>/<agent>/<branch>`` (default configured root:
|
|
13
|
+
``<repo-root>/.claude/worktrees``). Exit 0 prints success JSON; any failure
|
|
14
|
+
prints ``{"error": ...}`` and exits non-zero.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
@@ -33,6 +34,8 @@ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
|
33
34
|
AGENT_SLUG_PATTERN,
|
|
34
35
|
ALL_AGENT_DETECTION_MARKERS,
|
|
35
36
|
ALL_REPOSITORY_WORKTREE_ROOT_PARTS,
|
|
37
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
38
|
+
CLI_HELP_WORKTREE_ROOT,
|
|
36
39
|
DEFAULT_AGENT_SLUG,
|
|
37
40
|
DEFAULT_BASE_REF,
|
|
38
41
|
ERROR_AGENT_SLUG_INVALID,
|
|
@@ -41,6 +44,7 @@ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
|
41
44
|
ERROR_CLI_ARGUMENTS,
|
|
42
45
|
ERROR_UNIQUE_PATH_EXHAUSTED,
|
|
43
46
|
ERROR_WORKTREE_PATH_OUTSIDE_ROOT,
|
|
47
|
+
ERROR_WORKTREE_ROOT_NOT_ABSOLUTE,
|
|
44
48
|
EXIT_CODE_FAILURE,
|
|
45
49
|
EXIT_CODE_SUCCESS,
|
|
46
50
|
FRESH_BRANCH_AGENT_ENV_VAR,
|
|
@@ -91,26 +95,79 @@ def _detect_agent_slug_from_environment() -> str:
|
|
|
91
95
|
return DEFAULT_AGENT_SLUG
|
|
92
96
|
|
|
93
97
|
|
|
94
|
-
def
|
|
95
|
-
|
|
98
|
+
def resolve_configured_worktree_root(
|
|
99
|
+
repo_root: Path,
|
|
100
|
+
maybe_worktree_root: str | None = None,
|
|
101
|
+
) -> Path:
|
|
102
|
+
"""Return the isolated root that every worktree path must stay under.
|
|
103
|
+
|
|
104
|
+
::
|
|
105
|
+
|
|
106
|
+
resolve_configured_worktree_root(Path("/srv/app"))
|
|
107
|
+
# ok: /srv/app/.claude/worktrees
|
|
108
|
+
resolve_configured_worktree_root(Path("/srv/app"), "/tmp/isolated")
|
|
109
|
+
# ok: /tmp/isolated
|
|
110
|
+
resolve_configured_worktree_root(Path("/srv/app"), "relative/path")
|
|
111
|
+
# flag: ValueError --worktree-root must be an absolute path
|
|
112
|
+
|
|
113
|
+
When ``maybe_worktree_root`` is omitted, the default root is the repository's
|
|
114
|
+
``.claude/worktrees`` directory. An explicit root must be absolute; it may
|
|
115
|
+
sit outside the repository.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
repo_root: Absolute path of the repository the branch comes from.
|
|
119
|
+
maybe_worktree_root: Optional absolute configured root, or None.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Resolved absolute configured root directory.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: When ``maybe_worktree_root`` is set and is not absolute.
|
|
126
|
+
"""
|
|
127
|
+
if maybe_worktree_root is None:
|
|
128
|
+
return repo_root.joinpath(*ALL_REPOSITORY_WORKTREE_ROOT_PARTS).resolve()
|
|
129
|
+
cleaned_worktree_root = maybe_worktree_root.strip()
|
|
130
|
+
if not cleaned_worktree_root:
|
|
131
|
+
return repo_root.joinpath(*ALL_REPOSITORY_WORKTREE_ROOT_PARTS).resolve()
|
|
132
|
+
configured_root = Path(cleaned_worktree_root)
|
|
133
|
+
if not configured_root.is_absolute():
|
|
134
|
+
raise ValueError(ERROR_WORKTREE_ROOT_NOT_ABSOLUTE)
|
|
135
|
+
return configured_root.resolve()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_agent_worktree_root(
|
|
139
|
+
repo_root: Path,
|
|
140
|
+
agent_slug: str,
|
|
141
|
+
maybe_worktree_root: str | None = None,
|
|
142
|
+
) -> Path:
|
|
143
|
+
"""Return the per-agent directory under the configured worktree root.
|
|
96
144
|
|
|
97
145
|
::
|
|
98
146
|
|
|
99
147
|
# repo_root=/srv/app, agent_slug=grok
|
|
100
148
|
resolve_agent_worktree_root(Path("/srv/app"), "grok")
|
|
101
149
|
# -> /srv/app/.claude/worktrees/grok
|
|
150
|
+
resolve_agent_worktree_root(
|
|
151
|
+
Path("/srv/app"), "grok", maybe_worktree_root="/tmp/isolated",
|
|
152
|
+
)
|
|
153
|
+
# -> /tmp/isolated/grok
|
|
102
154
|
|
|
103
|
-
|
|
104
|
-
|
|
155
|
+
Each agent keeps its own subdirectory under the configured root so concurrent
|
|
156
|
+
hosts do not share worktree folders.
|
|
105
157
|
|
|
106
158
|
Args:
|
|
107
159
|
repo_root: Absolute path of the repository the branch comes from.
|
|
108
160
|
agent_slug: Short host label (one path segment).
|
|
161
|
+
maybe_worktree_root: Optional absolute configured root, or None.
|
|
109
162
|
|
|
110
163
|
Returns:
|
|
111
164
|
Directory that should hold per-branch worktree folders.
|
|
112
165
|
"""
|
|
113
|
-
|
|
166
|
+
configured_root = resolve_configured_worktree_root(
|
|
167
|
+
repo_root,
|
|
168
|
+
maybe_worktree_root=maybe_worktree_root,
|
|
169
|
+
)
|
|
170
|
+
return configured_root / agent_slug
|
|
114
171
|
|
|
115
172
|
|
|
116
173
|
def resolve_unique_worktree_path(preferred_path: Path) -> Path:
|
|
@@ -170,38 +227,55 @@ def create_fresh_branch(
|
|
|
170
227
|
repo_path: Path,
|
|
171
228
|
agent_slug: str,
|
|
172
229
|
base_ref: str,
|
|
230
|
+
maybe_worktree_root: str | None = None,
|
|
173
231
|
) -> dict[str, str]:
|
|
174
232
|
"""Fetch base_ref and create an isolated worktree branch with no upstream.
|
|
175
233
|
|
|
176
234
|
::
|
|
177
235
|
|
|
178
236
|
create_fresh_branch("fix/x", Path("."), "grok", "origin/main")
|
|
237
|
+
# worktree under <repo>/.claude/worktrees/grok/fix/x
|
|
238
|
+
create_fresh_branch(
|
|
239
|
+
"fix/x", Path("."), "grok", "origin/main",
|
|
240
|
+
maybe_worktree_root="/tmp/isolated",
|
|
241
|
+
)
|
|
242
|
+
# worktree under /tmp/isolated/grok/fix/x
|
|
179
243
|
|
|
180
|
-
The caller's working tree is never checked out. The
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
The caller's working tree is never checked out. The configured root is
|
|
245
|
+
validated before any git fetch so a relative ``--worktree-root`` fails
|
|
246
|
+
closed with no network I/O. The worktree lands under
|
|
247
|
+
``<configured-root>/<agent>/<branch>``, suffixed ``-2``, ``-3``, … when
|
|
248
|
+
that path is already taken. Every allocated path must resolve beneath the
|
|
249
|
+
configured root.
|
|
183
250
|
|
|
184
251
|
Args:
|
|
185
252
|
branch_name: Branch to create; must be a safe relative path.
|
|
186
253
|
repo_path: Any path inside the repository the branch comes from.
|
|
187
254
|
agent_slug: Short host label naming the worktree-root subdirectory.
|
|
188
255
|
base_ref: Ref the branch starts from, such as ``origin/main``.
|
|
256
|
+
maybe_worktree_root: Optional absolute configured root, or None for
|
|
257
|
+
the default ``<repo>/.claude/worktrees``.
|
|
189
258
|
|
|
190
259
|
Returns:
|
|
191
260
|
The success payload: branch, worktree_path, base_ref, base_commit,
|
|
192
261
|
agent, and repo_root.
|
|
193
262
|
|
|
194
263
|
Raises:
|
|
195
|
-
ValueError: When the branch name or
|
|
264
|
+
ValueError: When the branch name, agent slug, or worktree root is unsafe.
|
|
196
265
|
RuntimeError: When git refuses the fetch or the worktree add.
|
|
197
266
|
"""
|
|
198
267
|
cleaned_branch = _require_safe_branch_name(branch_name)
|
|
199
268
|
normalized_agent_slug = _normalize_agent_slug(agent_slug)
|
|
200
|
-
|
|
201
|
-
|
|
269
|
+
repo_root = resolve_repo_root(repo_path)
|
|
270
|
+
configured_root = resolve_configured_worktree_root(
|
|
271
|
+
repo_root,
|
|
272
|
+
maybe_worktree_root=maybe_worktree_root,
|
|
202
273
|
)
|
|
274
|
+
resolved_base_ref, base_commit = _fetch_resolved_base(repo_root, base_ref)
|
|
203
275
|
worktree_path = _allocate_worktree_path(
|
|
204
|
-
cleaned_branch,
|
|
276
|
+
cleaned_branch,
|
|
277
|
+
normalized_agent_slug,
|
|
278
|
+
configured_root,
|
|
205
279
|
)
|
|
206
280
|
create_worktree_branch(
|
|
207
281
|
repo_root,
|
|
@@ -215,15 +289,11 @@ def create_fresh_branch(
|
|
|
215
289
|
)
|
|
216
290
|
|
|
217
291
|
|
|
218
|
-
def
|
|
219
|
-
repo_path: Path,
|
|
220
|
-
base_ref: str,
|
|
221
|
-
) -> tuple[str, Path, str]:
|
|
292
|
+
def _fetch_resolved_base(repo_root: Path, base_ref: str) -> tuple[str, str]:
|
|
222
293
|
resolved_base_ref = normalize_base_ref(base_ref)
|
|
223
|
-
repo_root = resolve_repo_root(repo_path)
|
|
224
294
|
fetch_base_ref(repo_root, resolved_base_ref)
|
|
225
295
|
base_commit = resolve_base_commit(repo_root, resolved_base_ref)
|
|
226
|
-
return resolved_base_ref,
|
|
296
|
+
return resolved_base_ref, base_commit
|
|
227
297
|
|
|
228
298
|
|
|
229
299
|
def main() -> int:
|
|
@@ -233,14 +303,7 @@ def main() -> int:
|
|
|
233
303
|
Process exit code (0 success, 1 failure).
|
|
234
304
|
"""
|
|
235
305
|
try:
|
|
236
|
-
|
|
237
|
-
agent_slug = resolve_agent_slug(parsed_arguments.agent)
|
|
238
|
-
success_payload = create_fresh_branch(
|
|
239
|
-
branch_name=parsed_arguments.branch_name,
|
|
240
|
-
repo_path=Path(parsed_arguments.repo).resolve(),
|
|
241
|
-
agent_slug=agent_slug,
|
|
242
|
-
base_ref=parsed_arguments.base,
|
|
243
|
-
)
|
|
306
|
+
success_payload = _create_fresh_branch_from_cli_arguments()
|
|
244
307
|
print(json.dumps(success_payload))
|
|
245
308
|
return EXIT_CODE_SUCCESS
|
|
246
309
|
except (ValueError, RuntimeError, OSError) as error:
|
|
@@ -248,6 +311,18 @@ def main() -> int:
|
|
|
248
311
|
return EXIT_CODE_FAILURE
|
|
249
312
|
|
|
250
313
|
|
|
314
|
+
def _create_fresh_branch_from_cli_arguments() -> dict[str, str]:
|
|
315
|
+
parsed_arguments = _parse_arguments()
|
|
316
|
+
agent_slug = resolve_agent_slug(parsed_arguments.agent)
|
|
317
|
+
return create_fresh_branch(
|
|
318
|
+
branch_name=parsed_arguments.branch_name,
|
|
319
|
+
repo_path=Path(parsed_arguments.repo).resolve(),
|
|
320
|
+
agent_slug=agent_slug,
|
|
321
|
+
base_ref=parsed_arguments.base,
|
|
322
|
+
maybe_worktree_root=parsed_arguments.worktree_root,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
251
326
|
def _require_safe_branch_name(branch_name: str) -> str:
|
|
252
327
|
cleaned_branch = branch_name.strip()
|
|
253
328
|
if not cleaned_branch:
|
|
@@ -257,19 +332,21 @@ def _require_safe_branch_name(branch_name: str) -> str:
|
|
|
257
332
|
|
|
258
333
|
|
|
259
334
|
def _allocate_worktree_path(
|
|
260
|
-
branch_name: str,
|
|
335
|
+
branch_name: str,
|
|
336
|
+
agent_slug: str,
|
|
337
|
+
configured_root: Path,
|
|
261
338
|
) -> Path:
|
|
262
|
-
agent_worktree_root =
|
|
339
|
+
agent_worktree_root = configured_root / agent_slug
|
|
263
340
|
agent_worktree_root.mkdir(parents=True, exist_ok=True)
|
|
264
341
|
preferred_path = agent_worktree_root / branch_name
|
|
265
|
-
|
|
342
|
+
_assert_path_is_under_configured_root(
|
|
266
343
|
candidate_path=preferred_path,
|
|
267
|
-
|
|
344
|
+
configured_root=configured_root,
|
|
268
345
|
)
|
|
269
346
|
worktree_path = resolve_unique_worktree_path(preferred_path)
|
|
270
|
-
|
|
347
|
+
_assert_path_is_under_configured_root(
|
|
271
348
|
candidate_path=worktree_path,
|
|
272
|
-
|
|
349
|
+
configured_root=configured_root,
|
|
273
350
|
)
|
|
274
351
|
return worktree_path
|
|
275
352
|
|
|
@@ -309,12 +386,12 @@ def _validate_branch_name_for_worktree_path(branch_name: str) -> None:
|
|
|
309
386
|
assert_git_accepts_branch_name(branch_name)
|
|
310
387
|
|
|
311
388
|
|
|
312
|
-
def
|
|
389
|
+
def _assert_path_is_under_configured_root(
|
|
313
390
|
candidate_path: Path,
|
|
314
|
-
|
|
391
|
+
configured_root: Path,
|
|
315
392
|
) -> None:
|
|
316
393
|
resolved_candidate = candidate_path.resolve()
|
|
317
|
-
resolved_root =
|
|
394
|
+
resolved_root = configured_root.resolve()
|
|
318
395
|
if resolved_candidate == resolved_root:
|
|
319
396
|
return
|
|
320
397
|
try:
|
|
@@ -342,13 +419,19 @@ def _parse_arguments() -> argparse.Namespace:
|
|
|
342
419
|
parser.add_argument(
|
|
343
420
|
"--agent",
|
|
344
421
|
default=None,
|
|
345
|
-
help="Host label for
|
|
422
|
+
help="Host label for <configured-root>/<agent>/ (default: detect from environment).",
|
|
346
423
|
)
|
|
347
424
|
parser.add_argument(
|
|
348
425
|
"--base",
|
|
349
426
|
default=DEFAULT_BASE_REF,
|
|
350
427
|
help=f"Base ref to fetch and branch from (default: {DEFAULT_BASE_REF}).",
|
|
351
428
|
)
|
|
429
|
+
parser.add_argument(
|
|
430
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
431
|
+
default=None,
|
|
432
|
+
dest="worktree_root",
|
|
433
|
+
help=CLI_HELP_WORKTREE_ROOT,
|
|
434
|
+
)
|
|
352
435
|
try:
|
|
353
436
|
return parser.parse_args()
|
|
354
437
|
except SystemExit as exit_error:
|
package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Constants for the fresh-branch worktree creator.
|
|
2
2
|
|
|
3
|
-
Groups: default refs, agent detection, path layout, JSON payload
|
|
4
|
-
exit codes, and unique-path suffix limits.
|
|
3
|
+
Groups: default refs, agent detection, path layout, CLI flags, JSON payload
|
|
4
|
+
keys, exit codes, and unique-path suffix limits.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -25,6 +25,12 @@ PATH_SEGMENT_PARENT = ".."
|
|
|
25
25
|
|
|
26
26
|
ALL_REPOSITORY_WORKTREE_ROOT_PARTS = (".claude", "worktrees")
|
|
27
27
|
|
|
28
|
+
CLI_FLAG_WORKTREE_ROOT = "--worktree-root"
|
|
29
|
+
CLI_HELP_WORKTREE_ROOT = (
|
|
30
|
+
"Absolute isolated root for worktrees "
|
|
31
|
+
"(default: <repo>/.claude/worktrees). Agent and branch nest under it."
|
|
32
|
+
)
|
|
33
|
+
|
|
28
34
|
MAXIMUM_UNIQUE_PATH_ATTEMPTS = 100
|
|
29
35
|
UNIQUE_PATH_SUFFIX_START = 2
|
|
30
36
|
|
|
@@ -60,7 +66,8 @@ ERROR_BRANCH_NAME_REQUIRED = "branch name is required"
|
|
|
60
66
|
ERROR_BRANCH_NAME_UNSAFE = (
|
|
61
67
|
"branch name must be a relative path without '.' or '..' segments"
|
|
62
68
|
)
|
|
63
|
-
ERROR_WORKTREE_PATH_OUTSIDE_ROOT = "worktree path escapes
|
|
69
|
+
ERROR_WORKTREE_PATH_OUTSIDE_ROOT = "worktree path escapes configured root: %s"
|
|
70
|
+
ERROR_WORKTREE_ROOT_NOT_ABSOLUTE = "--worktree-root must be an absolute path"
|
|
64
71
|
ERROR_CLI_ARGUMENTS = "invalid or missing command-line arguments"
|
|
65
72
|
ERROR_AGENT_SLUG_INVALID = "agent slug must be lowercase letters, digits, or hyphens"
|
|
66
73
|
ERROR_REPO_NOT_GIT = "path is not inside a git repository: %s"
|