Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 55 additions & 26 deletions plugins/git-pilot/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
# git-pilot — Git Workflow Guidelines

## Branch Workflow
- Always work on a feature branch, never directly on the default branch
- Name branches using the configured pattern
- Check .claude/git-pilot.json (local) or ~/.claude/git-pilot.json (global) for current config
- Before making changes, ensure you are on the correct branch

## Commit Guidelines
- Follow the configured commit message format
- Do NOT include Co-Authored-By, Generated with, or any AI attribution lines in commits
- Keep commit subjects under the configured max length
- Use imperative mood in commit descriptions ("add" not "added")
- One logical change per commit

## When Finishing Work
- Commit all remaining changes before ending the session
- Check if the user wants to push to remote
- Check if the user wants to create a merge/pull request

## Configuration
- Users can change git-pilot settings by asking in natural language
- Global config: ~/.claude/git-pilot.json
- Local config: .claude/git-pilot.json
- Local settings override global settings
- When changing config, ask whether the change should be global or project-specific
# git-pilot — Git Workflow Autopilot

git-pilot manages the full git workflow lifecycle. You MUST follow these rules throughout every session.

## Rule 1: Always act on hook messages

When you receive a system message prefixed with `[git-pilot]` from any hook (SessionStart, PostToolUse, Stop), you MUST act on it using AskUserQuestion BEFORE continuing with other work. Never ignore these messages. Present clear, concise options relevant to the prompt.

## Rule 2: Branch discipline

- Never work directly on the default branch. If you're on the default branch when starting work, follow the `/branch` skill workflow to create a feature branch before making any changes.
- Name branches using the configured pattern (default: `{{type}}/{{description}}`, kebab-case).
- Available types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `style`, `perf`, `build`, `ci`.
- If the user's request clearly implies a branch type and description, you can infer the branch name and propose it. Otherwise, ask.
- Check `.claude/git-pilot.json` (local) or `~/.claude/git-pilot.json` (global) for project-specific overrides.

## Rule 3: Commit discipline

- Follow the configured commit format (default: `{{type}}({{scope}}): {{description}}`).
- Do NOT include `Co-Authored-By`, `Generated with`, or any AI attribution lines in commits.
- Keep commit subjects under the configured max length (default: 72).
- Use imperative mood ("add" not "added").
- One logical change per commit. When you've completed a coherent unit of work, commit it — don't accumulate unrelated changes.
- **Body policy**: Check `commit.body.required` in the effective config (defaults, global, local merged). If `false`, commits MUST be subject-line only — do NOT include a body. The only exception is breaking changes: when the subject contains `!:` and `commit.breakingChange.requireBody` is `true`, a body starting with the configured `bodyPrefix` (default: `BREAKING CHANGE: `) is required.

## Rule 4: Push workflow

After every successful `git commit`, check for unpushed commits. If there are any and a remote exists:
- Use AskUserQuestion to prompt the user with two options: **"Push to \<remote\>/\<branch\>"** and **"Skip"**.
- If they choose to push, run: `git push -u <remote> <branch>`.
- Do NOT silently skip this step or just mention it in passing. The user expects an interactive prompt.

## Rule 5: Session end

When finishing a session, follow the `/finish` skill workflow:
1. Commit any remaining uncommitted changes.
2. If unpushed commits exist, prompt to push (same as Rule 4).
3. If merge request creation is enabled, offer to create one using the appropriate platform CLI (`gh` for GitHub, `glab` for GitLab) or skip silently if the CLI isn't available.

## Rule 6: Configuration

- Users can change git-pilot settings by asking in natural language (via `/configure` skill).
- Global config: `~/.claude/git-pilot.json`
- Local config: `.claude/git-pilot.json`
- Local settings override global settings which override plugin defaults.
- When changing config, ask whether the change should be global or project-specific.

## Skill reference

| Skill | When to use |
|-------|-------------|
| `/branch` | Proactively when on the default branch before making changes |
| `/finish` | When the user says they're done, or at session end |
| `/summary` | When the user asks for a recap of branch work |
| `/configure` | When the user wants to change git-pilot settings |
10 changes: 10 additions & 0 deletions plugins/git-pilot/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
"timeout": 10
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/post-bash.sh",
"timeout": 10
}
]
}
],
"Stop": [
Expand Down
73 changes: 73 additions & 0 deletions plugins/git-pilot/scripts/post-bash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# shellcheck source=./config.sh
source "$SCRIPT_DIR/config.sh"
# shellcheck source=./git-utils.sh
source "$SCRIPT_DIR/git-utils.sh"

input=$(cat)

cwd=$(echo "$input" | jq -r '.cwd // empty')
command=$(echo "$input" | jq -r '.tool_input.command // empty')

# Only care about git commit commands
if [[ -z "$command" ]] || ! echo "$command" | grep -qE 'git\s+commit'; then
echo '{"continue": true}'
exit 0
fi

if [[ -z "$cwd" ]]; then
echo '{"continue": true}'
exit 0
fi

cd "$cwd"

if ! is_git_repo; then
echo '{"continue": true}'
exit 0
fi

config=$(load_config "$cwd")
push_on_finish=$(get_config "$config" '.remote.pushOnFinish' 'ask')
auto_push=$(get_config "$config" '.remote.autoPush' 'false')

if [[ "$auto_push" == "true" ]]; then
push_on_finish="always"
fi

# Only prompt when mode is "ask" — "always" and "never" are handled elsewhere
if [[ "$push_on_finish" != "ask" ]]; then
echo '{"continue": true}'
exit 0
fi

if ! has_remote; then
echo '{"continue": true}'
exit 0
fi

current_branch=$(get_current_branch)
remote_name=$(get_config "$config" '.remote.defaultName' 'origin')

# Check for unpushed commits
unpushed=$(git log '@{u}..HEAD' --oneline 2>/dev/null || true)
if ! git rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1; then
default_branch=$(get_default_branch "$config")
unpushed=$(git log "${default_branch}..${current_branch}" --oneline 2>/dev/null || true)
fi

if [[ -z "$unpushed" ]]; then
echo '{"continue": true}'
exit 0
fi

unpushed_count=$(echo "$unpushed" | wc -l | tr -d ' ')

# Emit push prompt — CLAUDE.md instructs Claude to act on this with AskUserQuestion
message="[git-pilot] ${unpushed_count} unpushed commit(s) on '${current_branch}'. Push command: git push -u ${remote_name} ${current_branch}"

jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}'
17 changes: 16 additions & 1 deletion plugins/git-pilot/scripts/pre-commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -577,12 +577,27 @@ if [[ "$SCOPE_REQUIRED" == "true" ]]; then
fi
fi

# --- Breaking change handling ---
# --- Body policy ---
BODY_REQUIRED=$(echo "$CONFIG" | jq -r '.commit.body.required // false')
IS_BREAKING=false
if [[ "$SUBJECT" =~ !: ]]; then
IS_BREAKING=true
fi

# Detect if a body is present
HAS_BODY=false
if [[ "$COMMIT_MSG" == *$'\n'* ]]; then
BODY_TEXT=$(echo "$COMMIT_MSG" | tail -n +2 | sed '/^$/d')
if [[ -n "$BODY_TEXT" ]]; then
HAS_BODY=true
fi
fi

# Reject body when body.required is false and commit is not a breaking change
if [[ "$BODY_REQUIRED" == "false" ]] && [[ "$HAS_BODY" == "true" ]] && [[ "$IS_BREAKING" == "false" ]]; then
output_block "[git-pilot] Commit body is not allowed (commit.body.required is false). Use subject-line only unless it's a breaking change."
fi

if [[ "$IS_BREAKING" == true ]]; then
REQUIRE_BODY=$(echo "$CONFIG" | jq -r '.commit.breakingChange.requireBody // true')
BODY_PREFIX=$(echo "$CONFIG" | jq -r '.commit.breakingChange.bodyPrefix // "BREAKING CHANGE: "')
Expand Down
6 changes: 3 additions & 3 deletions plugins/git-pilot/scripts/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ if is_git_repo; then
branch_types=$(echo "$CONFIG" | jq -r '.branch.types // ["feat","fix","refactor","docs","test","chore","style","perf","build","ci"] | join(", ")')

if has_uncommitted_changes; then
messages+=("[git-pilot] You are on '${default_branch}' with uncommitted changes. Ask the user if they want to: (1) stash changes and create a new branch, (2) commit changes first, or (3) continue on the current branch.")
messages+=("[git-pilot] On '${default_branch}' with uncommitted changes. Prompt the user: stash and create branch, commit first, or continue on '${default_branch}'.")
else
messages+=("[git-pilot] You are on the default branch '${default_branch}'. Before making changes, create a new branch. Use the naming pattern: ${branch_pattern}. Available types: ${branch_types}. Ask the user what they're working on to determine the branch name, or infer it from their request.")
messages+=("[git-pilot] On default branch '${default_branch}'. Prompt the user to create a branch before making changes. Pattern: ${branch_pattern}, types: ${branch_types}.")
fi
fi
fi
Expand All @@ -79,7 +79,7 @@ if is_git_repo; then
skip_remote=$(get_config "$CONFIG" '.remote.skipRemotePrompt' 'false')

if [[ "$prompt_remote" == "true" ]] && [[ "$skip_remote" == "false" ]]; then
messages+=("[git-pilot] No git remote is configured. Ask the user if they'd like to add one (e.g., git@github.com:user/repo.git or https://github.com/user/repo.git). If they want to skip, set remoteSkipped in the session so we don't ask again.")
messages+=("[git-pilot] No git remote configured. Prompt the user to add one or skip.")
fi
fi
fi
Expand Down
Loading