claude-dev-env 2.0.2 → 2.2.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/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_rules_shared.py +126 -47
- package/hooks/blocking/config/CLAUDE.md +2 -0
- 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_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_verified_commit_config_bootstrap.py +18 -0
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
- 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/harness_scratchpad_constants.py +10 -9
- package/hooks/hooks_constants/mypy_integration_constants.py +16 -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 +1 -0
- package/rules/verified-commit-gate-skip.md +28 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -13
- package/skills/_shared/pr-loop/portable-driver.md +150 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +3 -0
- package/skills/_shared/pr-loop/scripts/build_converge_task_list.py +310 -0
- package/skills/_shared/pr-loop/scripts/portable_converge_driver.py +1637 -0
- package/skills/_shared/pr-loop/scripts/select_converge_pacer.py +215 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +3 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +56 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +47 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +181 -0
- package/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +140 -0
- package/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +1119 -0
- package/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +207 -0
- 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 +6 -3
- package/skills/autoconverge/SKILL.md +421 -346
- package/skills/autoconverge/reference/CLAUDE.md +1 -0
- package/skills/autoconverge/reference/convergence.md +5 -5
- package/skills/autoconverge/reference/copilot-findings.md +51 -0
- package/skills/autoconverge/reference/multi-pr.md +33 -2
- package/skills/autoconverge/reference/stop-conditions.md +9 -6
- package/skills/autoconverge/test_portable_pacer_gate.py +54 -0
- package/skills/closeout/SKILL.md +7 -9
- package/skills/copilot-finding-triage/SKILL.md +21 -11
- 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/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/pr-converge/CLAUDE.md +4 -3
- package/skills/pr-converge/SKILL.md +469 -422
- package/skills/pr-converge/reference/CLAUDE.md +1 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +5 -1
- package/skills/pr-converge/reference/per-tick.md +51 -24
- package/skills/pr-converge/reference/progress-checklist.md +168 -0
- package/skills/pr-converge/test_portable_pacer_gate.py +67 -0
- package/skills/pr-converge/test_step5_host_branch.py +8 -6
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -3
- 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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copilot-review subagent prompt template
|
|
2
|
+
|
|
3
|
+
The background watcher prompt for the `copilot-review` skill. The hub
|
|
4
|
+
([`../SKILL.md`](../SKILL.md) Step 3) passes the fenced block below to the
|
|
5
|
+
subagent word for word, with the bracketed values ([NUMBER], [OWNER], [REPO],
|
|
6
|
+
[BRANCH], [HEAD_SHA]) filled in from Step 1.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
You are babysitting the GitHub Copilot reviewer on PR **#[NUMBER]** at **[OWNER]/[REPO]** (branch `[BRANCH]`, current HEAD `[HEAD_SHA]`). Your job: keep the loop running until Copilot returns a clean review against the current HEAD, then stop.
|
|
10
|
+
|
|
11
|
+
**Per-tick work** (do this now, then on each wakeup):
|
|
12
|
+
|
|
13
|
+
1. Resolve current HEAD: `pull_request_read(method="get", pullNumber=[NUMBER], owner="[OWNER]", repo="[REPO]")` and extract `.head.sha`.
|
|
14
|
+
2. Fetch latest Copilot review via `pull_request_read(method="get_reviews", pullNumber=[NUMBER], owner="[OWNER]", repo="[REPO]")`.
|
|
15
|
+
Capture `commit_id`, `state`, `submitted_at`, `id`.
|
|
16
|
+
3. Decide the branch:
|
|
17
|
+
- **No review exists:** increment `no_review_count` (see escalation rule below), re-request (step 4), schedule next wakeup, return.
|
|
18
|
+
- **Latest review's `commit_id` != current HEAD:** increment `no_review_count`, re-request (step 4), schedule next wakeup, return.
|
|
19
|
+
- **Latest review's `commit_id` == current HEAD with unresolved inline findings:** reset `no_review_count` to 0, TDD-fix them, push, reply inline on each thread, resolve each addressed thread, re-request (step 4), schedule next wakeup, return.
|
|
20
|
+
- **Latest review's `commit_id` == current HEAD and clean:** report convergence to the parent with a one-sentence summary and terminate. The loop is done; skip the ScheduleWakeup call.
|
|
21
|
+
|
|
22
|
+
**Escalation rule:** `no_review_count` starts at 0, counts consecutive ticks where no Copilot review exists at the current HEAD, and resets to 0 on every push and every review sighted at HEAD. When it reaches 3, Copilot is not delivering reviews — report the stall to the parent with the count and the last request timestamp, then terminate without scheduling another wakeup.
|
|
23
|
+
4. Re-request Copilot via `request_copilot_review(owner="[OWNER]", repo="[REPO]", pullNumber=[NUMBER])`.
|
|
24
|
+
The reviewer ID **must** be `copilot-pull-request-reviewer[bot]` with the `[bot]` suffix — empirically verified: `Copilot`, `copilot`, and `github-copilot` all return `requested_reviewers: []` with no error, silently no-op.
|
|
25
|
+
5. Schedule the next wakeup with `ScheduleWakeup`:
|
|
26
|
+
- `delaySeconds: 360`
|
|
27
|
+
- `reason`: one short sentence on what you are waiting for.
|
|
28
|
+
- `prompt`: the literal sentinel `<<autonomous-loop-dynamic>>` so the next firing re-enters these instructions.
|
|
29
|
+
|
|
30
|
+
**Fix protocol** (step 3, third branch):
|
|
31
|
+
|
|
32
|
+
- Read `$HOME/.claude/skills/pr-fix-protocol/SKILL.md` and apply it — it carries the shared fix sequence, the reply-and-resolve unit, and the unresolved-thread sweep.
|
|
33
|
+
- Read each referenced file:line.
|
|
34
|
+
- Write a failing test first when the finding has behavior to test. For pure doc or comment nits that have no behavior, go straight to the fix.
|
|
35
|
+
- Stage the fix and create one new commit on the existing branch: `git add <files> && git commit -m "fix(review): ..."`.
|
|
36
|
+
- Push the new commit: `git push origin [BRANCH]`.
|
|
37
|
+
- Reply inline via `add_reply_to_pull_request_comment(owner="[OWNER]", repo="[REPO]", pullNumber=[NUMBER], body="...", commentId=<comment_id>)`, referencing the new commit SHA; then resolve each addressed thread via `pull_request_review_write(method="resolve_thread", pullNumber=[NUMBER], owner="[OWNER]", repo="[REPO]", threadId="<PRRT id>")`, harvesting the `PRRT_…` id from `pull_request_read(method="get_review_comments", ...)`. Reply first, then resolve — atomic per thread, per the protocol.
|
|
38
|
+
|
|
39
|
+
When a pre-push, pre-commit, or other hook rejects the change, solve it. Read the hook's error message, diagnose the root cause in the code or test, and fix that. Then rerun the commit or push. Hooks exist to catch real problems; treat each rejection as new evidence to act on.
|
|
40
|
+
|
|
41
|
+
**Stop conditions:**
|
|
42
|
+
|
|
43
|
+
- Convergence (clean review against HEAD): report one-sentence summary to parent and terminate.
|
|
44
|
+
- Blocker you have exhausted fix attempts on (API auth failure persists, CI regression whose root cause falls outside this PR, a hook you have investigated and cannot resolve in one commit): report the specific blocker and its diagnosis to the parent, then terminate without scheduling another wakeup.
|
|
45
|
+
- Parent sends `TaskStop`: terminate immediately.
|
|
46
|
+
- `no_review_count` reaches 3 (escalation rule above): report the stall and terminate.
|
|
47
|
+
|
|
48
|
+
**Safety cap:** after 20 ticks without convergence, stop and report. This is the total-tick runaway guard, distinct from the 3-consecutive-no-review escalation; that many rounds means something structural is wrong with the loop.
|
|
49
|
+
```
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# fresh-branch
|
|
2
2
|
|
|
3
|
-
Creates a new branch from `origin/main`
|
|
3
|
+
Creates a new branch from fresh-fetched `origin/main` inside an isolated worktree under `Temp/<agent>/<branch-name>`. Does not push, open a PR, or run `checkout -b` in the caller tree.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
A shared primitive used by other skills and directly by the user. Fetches `origin/main`, suggests 2–4 branch names via `AskUserQuestion` when a topic is available (polling recent branch naming patterns from `git branch -r`), then creates and checks out the branch with `git checkout -b <name> origin/main`.
|
|
8
|
-
|
|
9
|
-
Does not push the branch. Does not create a PR. Does not switch an existing branch. Callers that need the new branch name (for example, to open a PR) receive it as a return value.
|
|
10
|
-
|
|
11
|
-
## Key file
|
|
5
|
+
## Key files
|
|
12
6
|
|
|
13
7
|
| File | Purpose |
|
|
14
8
|
|---|---|
|
|
15
|
-
| `SKILL.md` |
|
|
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 |
|
|
11
|
+
| `scripts/test_create_fresh_branch.py` | Behavioral tests with temporary git repos |
|
|
12
|
+
| `scripts/fresh_branch_scripts_constants/` | Constants package (`fresh_branch_cli_constants`) for the CLI |
|
|
@@ -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."""
|