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.
- package/CLAUDE.md +1 -1
- package/agents/clean-coder.md +31 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/bin/install.test.mjs +2 -2
- package/docs/CLAUDE.md +6 -3
- package/docs/CODE_RULES.md +5 -1
- package/docs/agent-spawn-protocol.md +39 -0
- package/docs/nas-ssh-invocation.md +23 -0
- package/docs/worker-completion-gate.md +33 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
- package/hooks/blocking/code_rules_shared.py +126 -47
- package/hooks/blocking/config/CLAUDE.md +2 -0
- package/hooks/blocking/config/verified_commit_constants.py +8 -1
- package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
- package/hooks/blocking/conftest.py +36 -30
- package/hooks/blocking/convergence_gate_blocker.py +76 -8
- package/hooks/blocking/plain_language_blocker.py +8 -0
- package/hooks/blocking/state_description_blocker.py +4 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
- package/hooks/blocking/test_code_rules_shared.py +120 -20
- package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
- package/hooks/blocking/test_plain_language_blocker.py +15 -0
- package/hooks/blocking/test_state_description_blocker.py +15 -0
- package/hooks/blocking/test_verification_verdict_store.py +12 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
- package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
- package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
- package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
- package/hooks/blocking/verified_commit_gate.py +113 -568
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
- package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
- package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
- package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
- package/hooks/validators/mypy_integration.py +145 -24
- package/hooks/validators/python_antipattern_checks.py +16 -0
- package/hooks/validators/run_all_validators.py +9 -3
- package/hooks/validators/test_mypy_integration.py +154 -0
- package/hooks/validators/test_python_antipattern_checks.py +112 -1
- package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +3 -3
- package/rules/agent-spawn-protocol.md +5 -43
- package/rules/code-standards.md +1 -36
- package/rules/env-var-table-code-drift.md +2 -21
- package/rules/hook-prose-matches-detector.md +5 -16
- package/rules/nas-ssh-invocation.md +3 -15
- package/rules/no-historical-clutter.md +7 -49
- package/rules/no-inline-destructive-literals.md +3 -5
- package/rules/package-inventory-stale-entry.md +7 -32
- package/rules/re-stage-before-commit.md +6 -23
- package/rules/shell-invocation-policy.md +1 -1
- package/rules/vault-context.md +3 -3
- package/rules/verified-commit-gate-skip.md +28 -0
- package/rules/workers-done-before-complete.md +2 -30
- package/scripts/claude_chain_runner.py +39 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
- package/scripts/test_claude_chain_runner.py +112 -0
- package/scripts/test_grok_headless_runner.py +0 -1
- package/skills/auditing-claude-config/CLAUDE.md +2 -1
- package/skills/auditing-claude-config/SKILL.md +114 -176
- package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
- package/skills/autoconverge/CLAUDE.md +1 -0
- package/skills/autoconverge/SKILL.md +310 -346
- package/skills/autoconverge/reference/CLAUDE.md +1 -0
- package/skills/autoconverge/reference/copilot-findings.md +51 -0
- package/skills/closeout/SKILL.md +7 -9
- package/skills/copilot-finding-triage/SKILL.md +5 -7
- package/skills/copilot-review/CLAUDE.md +3 -2
- package/skills/copilot-review/SKILL.md +119 -155
- package/skills/copilot-review/templates/subagent-prompt.md +49 -0
- package/skills/everything-search/SKILL.md +7 -1
- package/skills/fresh-branch/CLAUDE.md +6 -9
- package/skills/fresh-branch/SKILL.md +84 -33
- package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
- package/skills/orchestrator/SKILL.md +161 -147
- package/skills/pr-converge/SKILL.md +277 -422
- package/skills/pr-converge/reference/CLAUDE.md +1 -0
- package/skills/pr-converge/reference/progress-checklist.md +168 -0
- package/skills/pr-converge/test_step5_host_branch.py +8 -6
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -115
- package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
- package/skills/session-log/CLAUDE.md +2 -1
- package/skills/session-log/SKILL.md +4 -26
- package/skills/session-log/templates/frontmatter.md +40 -0
- package/skills/skill-builder/CLAUDE.md +8 -7
- package/skills/skill-builder/SKILL.md +26 -11
- package/skills/skill-builder/references/CLAUDE.md +3 -1
- package/skills/skill-builder/references/delegation-map.md +21 -12
- package/skills/skill-builder/references/description-field.md +9 -11
- package/skills/skill-builder/references/deterministic-elements.md +218 -0
- package/skills/skill-builder/references/self-audit-checklist.md +62 -45
- package/skills/skill-builder/references/skill-modularity.md +8 -9
- package/skills/skill-builder/templates/CLAUDE.md +2 -2
- package/skills/skill-builder/templates/gap-analysis.md +15 -0
- package/skills/skill-builder/workflows/CLAUDE.md +5 -5
- package/skills/skill-builder/workflows/improve-skill.md +18 -9
- package/skills/skill-builder/workflows/new-skill.md +23 -15
- package/skills/skill-builder/workflows/polish-skill.md +28 -21
- package/system-prompts/CLAUDE.md +3 -3
- package/docs/agents-md-alignment-plan.md +0 -123
- package/docs/emotion-informed-prompt-design.md +0 -362
- package/rules/es-exe-file-search.md +0 -17
|
@@ -1,71 +1,122 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fresh-branch
|
|
3
|
-
description:
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
## Checklist
|
|
19
26
|
|
|
20
27
|
```
|
|
21
|
-
|
|
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
|
-
|
|
34
|
+
## Phase 1 — Repo context
|
|
25
35
|
|
|
26
|
-
|
|
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
|
-
|
|
38
|
+
## Phase 2 — Branch name (high freedom)
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
Branch names: lowercase, hyphen-separated, optional `prefix/description`.
|
|
31
41
|
|
|
32
|
-
|
|
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
|
|
45
|
+
git branch -r --sort=-committerdate
|
|
38
46
|
```
|
|
39
47
|
|
|
40
|
-
|
|
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
|
-
|
|
52
|
+
## Phase 3 — Create branch (execute the script)
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
**Execute** the bundled CLI (do not reimplement with ad-hoc `git checkout -b`):
|
|
45
55
|
|
|
46
56
|
```
|
|
47
|
-
|
|
57
|
+
python "${CLAUDE_SKILL_DIR}/scripts/create_fresh_branch.py" --branch-name "<name>"
|
|
48
58
|
```
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
Optional flags:
|
|
51
61
|
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
76
|
+
On exit 0, stdout is one JSON object:
|
|
60
77
|
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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."""
|
package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py
ADDED
|
@@ -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]+)*$"
|