@reservine/dx 1.0.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.
@@ -0,0 +1,221 @@
1
+ ---
2
+ name: reservine-dx:cherry-pick-pr
3
+ description: Extract specific commits from the current branch into a clean PR against the base development branch
4
+ argument-hint: [commit hashes or description]
5
+ allowed-tools:
6
+ - Read
7
+ - Bash
8
+ - AskUserQuestion
9
+ ---
10
+
11
+ # Cherry-Pick PR
12
+
13
+ Extract specific commits from the current branch into a clean PR against the base development branch using a temporary worktree.
14
+
15
+ ## Configuration
16
+
17
+ Read project config before starting:
18
+
19
+ ```bash
20
+ cat .claude/config 2>/dev/null
21
+ cat .claude/config.local 2>/dev/null
22
+ ```
23
+
24
+ Use `DEVELOPMENT_BRANCH` as `<base>` throughout this workflow.
25
+ Use `GITHUB_REPO` for PR URLs and `gh` commands.
26
+
27
+ If no config file exists, fall back to detection:
28
+ ```bash
29
+ git rev-parse --verify origin/main &>/dev/null && echo "main" || echo "dev"
30
+ ```
31
+
32
+ ## Workflow
33
+
34
+ ### Step 1: Identify Commits
35
+
36
+ Ask the user which commits to cherry-pick, or infer from conversation context. Show candidates with:
37
+
38
+ ```bash
39
+ git log --oneline <base>..HEAD
40
+ ```
41
+
42
+ Confirm the selected commits with the user before proceeding. Display them clearly:
43
+
44
+ ```
45
+ Commits to cherry-pick:
46
+ abc1234 feat: add booking validation
47
+ def5678 fix: handle null employee
48
+ ```
49
+
50
+ **Existing PR detection:** If the user references an existing PR (e.g., "into #281", "again into 281", "into PR 281"), store the PR number and look up its branch:
51
+
52
+ ```bash
53
+ gh pr view <pr-number> --json headRefName --jq '.headRefName'
54
+ ```
55
+
56
+ When an existing PR is detected:
57
+ - Use the returned branch name as `<branch-name>` throughout
58
+ - **Skip Step 2** (branch name already known)
59
+ - **Skip Step 7** (PR already exists)
60
+
61
+ ### Step 2: Derive Branch Name
62
+
63
+ > **Skipped when targeting an existing PR** — the branch name comes from `gh pr view`.
64
+
65
+ Generate a branch name from the commit messages using `<type>/<short-description>` convention:
66
+
67
+ | Commit prefix | Branch type |
68
+ |---------------|-------------|
69
+ | `feat:` | `feat/` |
70
+ | `fix:` | `fix/` |
71
+ | `chore:` | `chore/` |
72
+ | `refactor:` | `refactor/` |
73
+ | `docs:` | `docs/` |
74
+ | Mixed types | Use the dominant type, or `chore/` as fallback |
75
+
76
+ Examples:
77
+ - Single commit `feat: add SMS toggle` -> `feat/add-sms-toggle`
78
+ - Multiple commits about config -> `chore/config-system-updates`
79
+
80
+ Confirm the branch name with the user. They may want to adjust it.
81
+
82
+ ### Step 3: Pre-flight Checks
83
+
84
+ **New PR flow:**
85
+
86
+ ```bash
87
+ # Fetch latest base branch
88
+ git fetch origin <base>
89
+
90
+ # Check if branch already exists on remote
91
+ git ls-remote --heads origin <branch-name>
92
+ ```
93
+
94
+ **If branch exists on remote:** Warn the user and ask how to proceed (rename branch, force-push, or abort).
95
+
96
+ **Existing PR flow:**
97
+
98
+ ```bash
99
+ # Get branch name from existing PR (already done in Step 1)
100
+ gh pr view <pr-number> --json headRefName --jq '.headRefName'
101
+
102
+ # Fetch the PR branch
103
+ git fetch origin <branch-name>
104
+ ```
105
+
106
+ ### Step 4: Create Worktree
107
+
108
+ **New PR flow:**
109
+
110
+ ```bash
111
+ git worktree add .worktrees/<branch-name> origin/<base> -b <branch-name>
112
+ ```
113
+
114
+ **Existing PR flow** — check out the existing PR branch instead of creating from base:
115
+
116
+ ```bash
117
+ git worktree add .worktrees/<branch-name> origin/<branch-name> -B <branch-name>
118
+ ```
119
+
120
+ ### Step 5: Cherry-Pick
121
+
122
+ ```bash
123
+ cd .worktrees/<branch-name>
124
+ git cherry-pick <hash1> <hash2> ...
125
+ ```
126
+
127
+ **If conflicts occur:**
128
+ 1. Stop immediately
129
+ 2. Show the conflicted files: `git diff --name-only --diff-filter=U`
130
+ 3. Show the conflict content for each file
131
+ 4. Ask the user how to resolve
132
+ 5. After resolution: `git add .` then `git cherry-pick --continue`
133
+
134
+ **If cherry-pick succeeds cleanly:** Continue to Step 6.
135
+
136
+ ### Step 6: Push
137
+
138
+ ```bash
139
+ cd .worktrees/<branch-name>
140
+ git push -u origin <branch-name>
141
+ ```
142
+
143
+ ### Step 7: Create PR
144
+
145
+ > **Skipped when targeting an existing PR** — the PR already exists, no need to create one.
146
+
147
+ **New PR flow only:**
148
+
149
+ ```bash
150
+ cd .worktrees/<branch-name>
151
+ gh pr create --base <base> --title "<title>" --body "$(cat <<'EOF'
152
+ ## Summary
153
+ <bullet points summarizing the cherry-picked changes>
154
+
155
+ ## Cherry-picked from
156
+ Branch: `<source-branch>`
157
+ Commits:
158
+ - `<hash1>` <message1>
159
+ - `<hash2>` <message2>
160
+
161
+ ## Test plan
162
+ - [ ] <relevant test items based on the changes>
163
+ EOF
164
+ )"
165
+ ```
166
+
167
+ **PR title:** Derive from commits. For single commit, use the commit message. For multiple, write a concise summary.
168
+
169
+ ### Step 8: Cleanup
170
+
171
+ **Always run cleanup**, even if a previous step failed:
172
+
173
+ ```bash
174
+ # Return to the original repo root first
175
+ cd <original-repo-root>
176
+ git worktree remove .worktrees/<branch-name>
177
+ ```
178
+
179
+ If worktree removal fails (e.g., uncommitted changes from conflict resolution):
180
+
181
+ ```bash
182
+ git worktree remove --force .worktrees/<branch-name>
183
+ ```
184
+
185
+ ### Step 9: Report
186
+
187
+ Return the PR URL and a summary. Always include a full clickable URL.
188
+
189
+ **New PR** (`gh pr create` returns the URL):
190
+
191
+ ```
192
+ PR created: <url>
193
+ Branch: <branch-name>
194
+ Commits: <count> cherry-picked from <source-branch>
195
+ ```
196
+
197
+ **Existing PR** (construct the URL from `GITHUB_REPO` and the PR number):
198
+
199
+ ```
200
+ PR updated: https://github.com/<GITHUB_REPO>/pull/<pr-number>
201
+ Branch: <branch-name>
202
+ Commits: <count> cherry-picked from <source-branch>
203
+ ```
204
+
205
+ ## Error Recovery
206
+
207
+ | Failure point | Recovery |
208
+ |---------------|----------|
209
+ | Worktree creation fails (branch exists locally) | `git branch -D <branch-name>` then retry |
210
+ | Cherry-pick conflicts | Show conflicts, ask user, resolve, `--continue` |
211
+ | Push fails (branch exists on remote) | Ask user: rename, force-push, or abort |
212
+ | PR creation fails | Show error, suggest manual creation with `gh pr create` |
213
+ | Any failure | Always clean up the worktree before stopping |
214
+
215
+ ## Important Notes
216
+
217
+ - **Never force-push without explicit user approval**
218
+ - **Always return to the original repo root** before worktree cleanup
219
+ - The worktree lives in `.worktrees/` (gitignored)
220
+ - Cherry-pick preserves original commit messages and authorship
221
+ - If the user is on a long-running branch, remind them that the PR targets `<base>` (the main development branch)
@@ -0,0 +1,297 @@
1
+ ---
2
+ name: reservine-dx:cleanup
3
+ description: >
4
+ Clean up all artifacts for a finished feature: worktrees (FE + BE), running processes
5
+ (nx serve, Docker stacks), local and remote branches. Verifies PRs are merged before
6
+ deleting. Auto-discovers sibling worktrees across repos.
7
+ Triggers: "cleanup", "clean worktree", "remove worktree", "tear down", "feature done".
8
+ argument-hint: "[branch-name]"
9
+ allowed-tools:
10
+ - Bash
11
+ - Read
12
+ - AskUserQuestion
13
+ ---
14
+
15
+ # Feature Cleanup
16
+
17
+ Clean up all artifacts for a finished feature branch across both FE and BE repos.
18
+
19
+ ## Step 1: Detect Branch & Resolve Repos
20
+
21
+ 1. Read `.claude/config` + `.claude/config.local`:
22
+ ```bash
23
+ cat .claude/config 2>/dev/null
24
+ cat .claude/config.local 2>/dev/null
25
+ ```
26
+ Extract: `GITHUB_REPO`, `DEVELOPMENT_BRANCH`, `BACKEND_DIR`/`FRONTEND_DIR`
27
+
28
+ 2. Auto-detect current repo:
29
+ ```bash
30
+ if [[ -f "angular.json" ]] || [[ -f "nx.json" ]]; then
31
+ CURRENT_REPO="FE"
32
+ else
33
+ CURRENT_REPO="BE"
34
+ fi
35
+ ```
36
+
37
+ 3. Resolve both repo paths (always need both for cross-repo cleanup):
38
+ - FE: from `FRONTEND_DIR` or `FRONTEND_DIR_ABSOLUTE` in config
39
+ - BE: from `BACKEND_DIR` or `BACKEND_DIR_ABSOLUTE` in config
40
+ - Resolve `FE_GITHUB_REPO` and `BE_GITHUB_REPO` from each repo's `.claude/config`
41
+ - Fallbacks: `LEFTEQ/reservine` (FE), `genesiscz/ReservineBack` (BE)
42
+
43
+ 4. Determine the target branch:
44
+ - **If `$ARGUMENTS` provided** → use as branch name
45
+ - **Else if current dir is inside `.worktrees/`** → extract branch from path
46
+ - **Else if on a feature branch** (not main/dev/master/release) → use current branch
47
+ - **Else** → list existing worktrees and `AskUserQuestion`:
48
+ ```bash
49
+ echo "=== FE worktrees ===" && git -C <FE_DIR> worktree list
50
+ echo "=== BE worktrees ===" && git -C <BE_DIR> worktree list
51
+ ```
52
+ Present as options: the branch names from all worktrees (deduplicated).
53
+
54
+ ## Step 2: Discover All Artifacts
55
+
56
+ Run all discovery in parallel (single bash block), collecting results:
57
+
58
+ ### FE artifacts:
59
+ ```bash
60
+ FE_DIR="<resolved FE path>"
61
+
62
+ # Worktree
63
+ FE_WORKTREE=$(git -C "$FE_DIR" worktree list | grep "<branch>" | awk '{print $1}')
64
+
65
+ # Processes (check ports 4200-4210, 1111, 1112, 1113)
66
+ FE_PIDS=""
67
+ if [[ -n "$FE_WORKTREE" ]]; then
68
+ for port in 4200 4201 4202 4203 4204 4205 1112; do
69
+ PID=$(lsof -t -i :$port 2>/dev/null)
70
+ if [[ -n "$PID" ]]; then
71
+ # Verify the process is actually serving from this worktree
72
+ CMD=$(ps -p $PID -o command= 2>/dev/null)
73
+ if echo "$CMD" | grep -q "$FE_WORKTREE\|port.*$port"; then
74
+ FE_PIDS="$FE_PIDS $PID:$port"
75
+ fi
76
+ fi
77
+ done
78
+ fi
79
+
80
+ # Branch (local + remote)
81
+ FE_LOCAL_BRANCH=$(git -C "$FE_DIR" branch --list "<branch>" 2>/dev/null)
82
+ FE_REMOTE_BRANCH=$(git -C "$FE_DIR" ls-remote --heads origin "<branch>" 2>/dev/null)
83
+
84
+ # PR
85
+ FE_PR=$(gh pr list --head "<branch>" --repo "$FE_GITHUB_REPO" --state all --json number,state,mergedAt,url --jq '.[0]' 2>/dev/null)
86
+ ```
87
+
88
+ ### BE artifacts:
89
+ ```bash
90
+ BE_DIR="<resolved BE path>"
91
+
92
+ # Worktree
93
+ BE_WORKTREE=$(git -C "$BE_DIR" worktree list | grep "<branch>" | awk '{print $1}')
94
+
95
+ # Docker isolated stack
96
+ BRANCH_SLUG=$(echo "<branch>" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
97
+ BE_DOCKER=$(docker ps --filter "name=wt-${BRANCH_SLUG}" --format "{{.Names}}" 2>/dev/null)
98
+ BE_COMPOSE=""
99
+ if [[ -n "$BE_WORKTREE" ]] && [[ -f "$BE_WORKTREE/docker-compose.isolated.yaml" ]]; then
100
+ BE_COMPOSE="$BE_WORKTREE/docker-compose.isolated.yaml"
101
+ fi
102
+
103
+ # Branch (local + remote)
104
+ BE_LOCAL_BRANCH=$(git -C "$BE_DIR" branch --list "<branch>" 2>/dev/null)
105
+ BE_REMOTE_BRANCH=$(git -C "$BE_DIR" ls-remote --heads origin "<branch>" 2>/dev/null)
106
+
107
+ # PR
108
+ BE_PR=$(gh pr list --head "<branch>" --repo "$BE_GITHUB_REPO" --state all --json number,state,mergedAt,url --jq '.[0]' 2>/dev/null)
109
+ ```
110
+
111
+ ### Uncommitted changes check:
112
+ ```bash
113
+ if [[ -n "$FE_WORKTREE" ]]; then
114
+ FE_DIRTY=$(git -C "$FE_WORKTREE" status --porcelain 2>/dev/null | grep -v '^??' | head -5)
115
+ fi
116
+ if [[ -n "$BE_WORKTREE" ]]; then
117
+ BE_DIRTY=$(git -C "$BE_WORKTREE" status --porcelain 2>/dev/null | grep -v '^??' | head -5)
118
+ fi
119
+ ```
120
+
121
+ ## Step 3: Check PR Status & Build Summary
122
+
123
+ Parse the PR JSON for each repo. Build a summary string:
124
+
125
+ For each repo:
126
+ - **MERGED**: `✓ PR #<num> — merged <relative-time>`
127
+ - **OPEN**: `⚠ PR #<num> — still OPEN`
128
+ - **CLOSED** (not merged): `⚠ PR #<num> — closed without merge`
129
+ - **No PR found**: `⚠ No PR found for this branch`
130
+
131
+ Determine safety level:
132
+ - All PRs merged → **safe** (recommend "Clean everything")
133
+ - Any PR open/missing → **unsafe** (recommend "Abort", require explicit override)
134
+
135
+ ## Step 4: Present Summary & Confirm
136
+
137
+ Present the full discovery as a formatted message, then `AskUserQuestion`:
138
+
139
+ ```
140
+ Feature Cleanup: <branch>
141
+ ═══════════════════════════
142
+
143
+ FE (<FE_GITHUB_REPO>):
144
+ <PR status line>
145
+ <worktree path or "No worktree found">
146
+ <processes or "No running processes">
147
+ <branches: local + remote status>
148
+ <dirty warning if uncommitted changes>
149
+
150
+ BE (<BE_GITHUB_REPO>):
151
+ <PR status line>
152
+ <worktree path or "No worktree found">
153
+ <Docker containers or "No isolated stack">
154
+ <branches: local + remote status>
155
+ <dirty warning if uncommitted changes>
156
+ ```
157
+
158
+ Options (if all PRs merged and no dirty files):
159
+ - **Clean everything** (Recommended)
160
+ - **Skip FE** — only clean BE
161
+ - **Skip BE** — only clean FE
162
+ - **Abort**
163
+
164
+ Options (if any PR not merged OR dirty files):
165
+ - **Abort** (Recommended) — with warning about what's at risk
166
+ - **Force clean everything** — delete despite warnings
167
+ - **Skip FE** / **Skip BE**
168
+
169
+ ## Step 5: Execute Cleanup
170
+
171
+ Execute in this exact order (processes first, then containers, then files, then git):
172
+
173
+ ### 5a: Kill FE processes
174
+ ```bash
175
+ for pid_port in $FE_PIDS; do
176
+ PID=${pid_port%:*}; PORT=${pid_port#*:}
177
+ kill $PID 2>/dev/null && echo "✓ Killed process PID $PID on :$PORT"
178
+ done
179
+ ```
180
+
181
+ ### 5b: Teardown BE Docker
182
+
183
+ **CRITICAL:** Docker teardown MUST complete fully before the worktree directory is touched.
184
+ Use a two-phase approach: try compose first, then force-remove by slug as mandatory cleanup.
185
+
186
+ ```bash
187
+ SLUG=$(echo "<branch>" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
188
+
189
+ # Phase 1: Try graceful compose down (if compose file still exists)
190
+ if [[ -n "$BE_COMPOSE" ]] && [[ -f "$BE_COMPOSE" ]]; then
191
+ cd "$BE_WORKTREE"
192
+ docker compose -f docker-compose.isolated.yaml down -v 2>&1
193
+ echo "✓ Docker stack torn down via compose"
194
+ fi
195
+
196
+ # Phase 2: ALWAYS force-remove by slug (catches orphans, partially-stopped stacks, missing compose files)
197
+ REMAINING=$(docker ps -aq --filter "name=wt-${SLUG}" 2>/dev/null)
198
+ if [[ -n "$REMAINING" ]]; then
199
+ docker rm -f $REMAINING 2>/dev/null
200
+ echo "✓ Force-removed remaining Docker containers"
201
+ fi
202
+
203
+ # Phase 3: Remove Docker volumes by project name pattern
204
+ docker volume ls -q 2>/dev/null | grep "wt.*${SLUG}" | xargs docker volume rm 2>/dev/null
205
+ # Also try the compose project name format
206
+ docker volume ls -q 2>/dev/null | grep "wt-${SLUG}" | xargs docker volume rm 2>/dev/null
207
+ ```
208
+
209
+ ### 5c: Remove FE worktree
210
+ ```bash
211
+ if [[ -n "$FE_WORKTREE" ]]; then
212
+ cd "$FE_DIR"
213
+ git worktree remove --force "$FE_WORKTREE" 2>&1
214
+ git worktree prune
215
+ echo "✓ Removed FE worktree"
216
+ fi
217
+ ```
218
+
219
+ ### 5d: Remove BE worktree
220
+ ```bash
221
+ if [[ -n "$BE_WORKTREE" ]]; then
222
+ cd "$BE_DIR"
223
+ # Docker may have created files owned by root — force remove the directory first
224
+ rm -rf "$BE_WORKTREE" 2>/dev/null
225
+ git worktree prune
226
+ echo "✓ Removed BE worktree"
227
+ fi
228
+ ```
229
+
230
+ ### 5e: Delete local branches
231
+ ```bash
232
+ if [[ -n "$FE_LOCAL_BRANCH" ]]; then
233
+ git -C "$FE_DIR" branch -D "<branch>" 2>&1
234
+ echo "✓ Deleted FE local branch"
235
+ fi
236
+ if [[ -n "$BE_LOCAL_BRANCH" ]]; then
237
+ git -C "$BE_DIR" branch -D "<branch>" 2>&1
238
+ echo "✓ Deleted BE local branch"
239
+ fi
240
+ ```
241
+
242
+ ### 5f: Delete remote branches (only if PR was merged)
243
+ ```bash
244
+ # FE
245
+ if [[ "$FE_PR_STATE" == "MERGED" ]] && [[ -n "$FE_REMOTE_BRANCH" ]]; then
246
+ git -C "$FE_DIR" push origin --delete "<branch>" 2>/dev/null \
247
+ && echo "✓ Deleted FE remote branch" \
248
+ || echo "ℹ FE remote branch already deleted"
249
+ fi
250
+ # BE
251
+ if [[ "$BE_PR_STATE" == "MERGED" ]] && [[ -n "$BE_REMOTE_BRANCH" ]]; then
252
+ git -C "$BE_DIR" push origin --delete "<branch>" 2>/dev/null \
253
+ && echo "✓ Deleted BE remote branch" \
254
+ || echo "ℹ BE remote branch already deleted"
255
+ fi
256
+ ```
257
+
258
+ ### 5g: Prune remote refs
259
+ ```bash
260
+ git -C "$FE_DIR" remote prune origin 2>/dev/null
261
+ git -C "$BE_DIR" remote prune origin 2>/dev/null
262
+ ```
263
+
264
+ ## Step 6: Report
265
+
266
+ Print the final cleanup report:
267
+
268
+ ```
269
+ Cleanup Complete
270
+ ═════════════════
271
+
272
+ FE (<FE_GITHUB_REPO>):
273
+ <✓/ℹ for each action taken or skipped>
274
+
275
+ BE (<BE_GITHUB_REPO>):
276
+ <✓/ℹ for each action taken or skipped>
277
+ ```
278
+
279
+ ## Edge Case Handling
280
+
281
+ | Scenario | Action |
282
+ |----------|--------|
283
+ | Worktree has uncommitted changes | Show dirty files in summary, default to Abort |
284
+ | Docker teardown fails (permission) | Log error, suggest manual `rm -rf` + `docker compose down -v` |
285
+ | Branch doesn't exist locally | Skip with `ℹ` note |
286
+ | Remote branch already deleted | Skip with `ℹ` note (don't error) |
287
+ | No worktree found for branch | Skip with `ℹ` note, still clean branches |
288
+ | Only one repo has artifacts | Clean what exists, report the other as "nothing to clean" |
289
+ | Running from inside the worktree being deleted | `cd` to repo root first before removing |
290
+ | Branch name has `#` characters | Sanitize for Docker slug, use raw name for git ops |
291
+
292
+ ## Notes
293
+
294
+ - Always `cd` to repo root before `git worktree remove` (can't remove the worktree you're standing in)
295
+ - BE worktrees may have Docker-owned files — use `rm -rf` before `git worktree prune` rather than `git worktree remove`
296
+ - Remote branch deletion is idempotent — GitHub's auto-delete may have already removed it
297
+ - The `lsof` port scan only checks common worktree ports (4200-4205, 1112) — if the user served on a custom port, it won't be detected (acceptable tradeoff)
@@ -0,0 +1,118 @@
1
+ # Claude Command: Commit
2
+
3
+ When this command is invoked, you MUST spawn a sub-agent to handle all commit work. Do NOT run any git commands yourself — delegate everything.
4
+
5
+ ## Instructions
6
+
7
+ 1. Spawn a single `general-purpose` sub-agent with `mode: "auto"` using the prompt below
8
+ 2. Pass through any user arguments (e.g. `$ARGUMENTS`) into the agent prompt where indicated
9
+ 3. When the agent returns, relay its summary to the user verbatim — do NOT add your own commentary
10
+
11
+ ## Agent Prompt
12
+
13
+ Use this as the agent's prompt (replace `{{ARGUMENTS}}` with `$ARGUMENTS`):
14
+
15
+ ---
16
+
17
+ You are a commit assistant. Your job is to create well-formatted git commits.
18
+
19
+ **User arguments:** `{{ARGUMENTS}}`
20
+
21
+ ## Steps
22
+
23
+ ### 1. Detect stack & pre-commit checks
24
+
25
+ **First, detect the stack** by checking files in the working directory:
26
+
27
+ ```bash
28
+ if [[ -f "angular.json" ]] || [[ -f "nx.json" ]]; then
29
+ STACK="angular"
30
+ elif [[ -f "artisan" ]] || [[ -f "composer.json" ]]; then
31
+ STACK="laravel"
32
+ fi
33
+ ```
34
+
35
+ **Then, unless the user passed `--no-verify`, run stack-specific checks:**
36
+
37
+ **Angular (FE):**
38
+ - `nx lint reservine` — lint check
39
+ - `bun run build:reservine:prod` — production build
40
+
41
+ **Laravel (BE):**
42
+ - `vendor/bin/pint --test` — code style check
43
+ - `vendor/bin/phpstan analyze <changed-php-files> --memory-limit=512M` — static analysis on changed files only
44
+
45
+ To get the changed PHP files for PHPStan:
46
+ ```bash
47
+ git diff --cached --name-only --diff-filter=ACMR -- '*.php' | tr '\n' ' '
48
+ ```
49
+ If no PHP files are staged, skip PHPStan.
50
+
51
+ **Unknown stack:** Skip pre-commit checks with a warning.
52
+
53
+ If any check fails, report the failure and stop. Do NOT proceed to commit.
54
+
55
+ If the user passed `--no-verify`, skip all checks entirely.
56
+
57
+ ### 2. Gather context
58
+
59
+ Run these in parallel:
60
+ - `git status` (to see staged/unstaged/untracked files — never use `-uall`)
61
+ - `git diff --cached` and `git diff` (to see all changes)
62
+ - `git log --oneline -10` (to see recent commit style and detect task IDs)
63
+
64
+ ### 3. Stage files
65
+
66
+ - If files are already staged, use only those
67
+ - If nothing is staged, run `git add` for all modified and new files (prefer specific filenames over `git add -A`)
68
+ - NEVER stage files that look like secrets (`.env`, credentials, tokens)
69
+
70
+ ### 4. Analyze changes
71
+
72
+ Review the staged diff. Determine:
73
+ - What type of change this is (feat, fix, docs, style, refactor, perf, test, chore, ci, revert, wip)
74
+ - Whether a task ID can be inferred from the branch name or recent commits
75
+ - Whether the changes should be split into multiple commits (different concerns, different types, different logical groups)
76
+
77
+ ### 5. Commit
78
+
79
+ For each logical commit:
80
+ 1. Stage only the relevant files for that commit
81
+ 2. Create the commit with the format below
82
+ 3. Use a HEREDOC for the message to preserve formatting
83
+
84
+ **Commit message format:**
85
+
86
+ With task ID: `[TASK_ID] - type(scope): description`
87
+ Without task ID: `type(scope): description`
88
+
89
+ Scope is optional — use it when changes are clearly scoped to a module/area.
90
+
91
+ Task ID patterns to detect:
92
+ - Branch name like `feat/something-123` or `fix/PROJ-456` → extract the ID
93
+ - Recent commits with `[#123]` or `[PROJ-123]` → reuse the pattern
94
+ - If no ID is apparent, omit it
95
+
96
+ Rules:
97
+ - Present tense, imperative mood ("add" not "added")
98
+ - First line under 72 characters (excluding task ID)
99
+ - Focus on "why" not "what"
100
+ - Each commit should be atomic — one logical change
101
+
102
+ ### 6. Verify
103
+
104
+ Run `git status` after committing to confirm success.
105
+
106
+ ### 7. Return summary
107
+
108
+ Return a concise summary in this format:
109
+
110
+ ```
111
+ Committed N change(s):
112
+ - <commit hash short> <commit message>
113
+ - <commit hash short> <commit message>
114
+
115
+ Files: <number> changed, <insertions> insertions(+), <deletions> deletions(-)
116
+ ```
117
+
118
+ If the commit was split into multiple commits, list each one. Keep it brief.