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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"name": "git-pilot",
"source": "./plugins/git-pilot",
"description": "Automated git workflow management for Claude Code — branch creation, commit formatting, push/MR workflows, rebase and conflict resolution, worktree management, stash automation, agent teams support, and natural language configuration.",
"version": "2.2.1",
"version": "3.0.0",
"author": {
"name": "moukrea"
},
Expand Down
2,056 changes: 0 additions & 2,056 deletions TECHNICAL-SPEC.md

This file was deleted.

2 changes: 1 addition & 1 deletion plugins/git-pilot/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "git-pilot",
"version": "2.2.1",
"version": "3.0.0",
"description": "Automated git workflow management for Claude Code — branch creation, commit formatting, push/MR workflows, rebase and conflict resolution, worktree management, stash automation, agent teams support, and natural language configuration",
"author": {
"name": "git-pilot contributors"
Expand Down
52 changes: 35 additions & 17 deletions plugins/git-pilot/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
> **Note:** The `git-pilot-workflow` skill contains the definitive behavioral contract for git-pilot. This CLAUDE.md file serves as reinforcement. If both are loaded, the skill takes precedence.

# 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
## How hooks work

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.
Hook messages appear as `[git-pilot]` prefixed context. Act on them according
to the rules below. Hooks provide facts -- you decide the response.

## Rule 2: Branch discipline
## Rule 1: 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
## Rule 2: 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.
Expand All @@ -25,29 +22,31 @@ When you receive a system message prefixed with `[git-pilot]` from any hook (Ses
- 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
## Rule 3: 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 you see '[git-pilot] Push rejected' in hook context, present resolution options: pull & rebase, force-with-lease, pull & merge, cancel.

## Rule 4: 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).
2. If unpushed commits exist, prompt to push (same as Rule 3).
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
## Rule 5: 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.

## Rule 7: Unrelated work detection
## Rule 6: Unrelated work detection

Before starting work on a new user request, assess whether the request is related
to the current branch's purpose:
Expand All @@ -69,9 +68,11 @@ to the current branch's purpose:
a bug discovered while implementing a feature on the same branch), for work
on the default branch, or for branches with no commits yet.
5. **If the user chooses to create a new branch**: Follow the branch switch
workflow (see Rule 8).
workflow (see Rule 7).

When you see '[git-pilot] Branch <name> (<purpose>, N commits)' in hook context, assess whether the user's request is related. If unrelated, use AskUserQuestion with options.

## Rule 8: Branch switching
## Rule 7: Branch switching

When switching branches (via /branch, user request, or unrelated work detection):

Expand All @@ -81,7 +82,7 @@ When switching branches (via /branch, user request, or unrelated work detection)
restore it automatically.
3. If stash restoration fails (conflicts), inform the user and suggest manual resolution.

## Rule 9: Conflict resolution
## Rule 8: Conflict resolution

When a rebase or merge results in conflicts:

Expand All @@ -94,7 +95,9 @@ When a rebase or merge results in conflicts:
4. After the user resolves conflicts, continue the interrupted operation
(`git rebase --continue` or `git merge --continue`).

## Rule 10: Agent Teams
When you see '[git-pilot] Push rejected' or '[git-pilot] Auto-rebase aborted (conflicts)', present resolution options to the user.

## Rule 9: Agent Teams

When operating as a spawned agent (not the orchestrator):

Expand All @@ -104,6 +107,21 @@ When operating as a spawned agent (not the orchestrator):
3. Do not run auto-commit suggestions. Commit when instructed by the orchestrator.
4. If instructed to work in a specific worktree directory, stay in that directory.

## Rule 10: Hook context responses

When you see hook context messages, respond according to this map:

| Hook Output Pattern | Claude Action |
|---------------------|---------------|
| `[git-pilot] Detached HEAD at {sha}` | AskUserQuestion: return to branch, create new, continue |
| `[git-pilot] On default branch` | Create feature branch before changes |
| `[git-pilot] Branch diverged` | AskUserQuestion: rebase, merge, reset, continue |
| `[git-pilot] N unpushed commit(s)` (after commit) | AskUserQuestion: push or skip |
| `[git-pilot] Push rejected` | AskUserQuestion: pull & rebase, force-with-lease, merge, cancel |
| `[git-pilot] N file changes since last commit` | AskUserQuestion: commit now or continue |
| `[git-pilot] No git remote configured` | AskUserQuestion: add remote or skip |
| `[git-pilot] Branch behind` (when ff failed) | AskUserQuestion: pull merge, reset, continue |

## Skill reference

| Skill | When to use |
Expand Down
17 changes: 4 additions & 13 deletions plugins/git-pilot/scripts/post-bash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ if echo "$command" | grep -qE 'git\s+commit'; then
# Only prompt when mode is "ask" — "always" and "never" are handled elsewhere
if [[ "$push_on_finish" == "ask" ]] && has_remote; then
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
Expand All @@ -68,10 +66,9 @@ if echo "$command" | grep -qE 'git\s+commit'; then
if [[ -n "$unpushed" ]]; then
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}'. You MUST use AskUserQuestion NOW to ask the user whether to push. Push command: git push -u ${remote_name} ${current_branch}. Do not silently skip this prompt."
message="[git-pilot] ${unpushed_count} unpushed commit(s) on '${current_branch}'"

jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}'
jq -n --arg msg "$message" '{"continue": true, "additionalContext": $msg}'
exit 0
fi
fi
Expand All @@ -87,14 +84,8 @@ if echo "$command" | grep -qE 'git\s+push'; then
if [[ "$exit_code" != "0" ]]; then
if echo "$stderr" | grep -qiE 'rejected|failed to push|non-fast-forward'; then
current_branch=$(get_current_branch)
remote_name=$(get_config "$config" '.remote.defaultName' 'origin')
message="[git-pilot] Push rejected — remote '${remote_name}/${current_branch}' has new commits. STOP and use AskUserQuestion NOW to present these options:
1. Pull and rebase, then retry push (git pull --rebase && git push)
2. Force push with lease (git push --force-with-lease)
3. Pull and merge (git pull)
4. Cancel
Do not attempt any resolution without the user's choice."
jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}'
message="[git-pilot] Push rejected — remote has new commits on '${current_branch}'"
jq -n --arg msg "$message" '{"continue": true, "additionalContext": $msg}'
exit 0
fi
fi
Expand Down
10 changes: 4 additions & 6 deletions plugins/git-pilot/scripts/post-write.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ fi
threshold=$(get_config "$config" '.autoCommit.threshold' '3')
mode=$(get_config "$config" '.autoCommit.mode' 'suggest')
wip_prefix=$(get_config "$config" '.autoCommit.wipPrefix' 'wip: ')
commit_pattern=$(get_config "$config" '.commit.pattern' '{{type}}({{scope}}): {{description}}')

# Compute state file path
state_file=$(get_state_file "$session_id")

Expand Down Expand Up @@ -73,10 +71,10 @@ write_state "$state_file" "$reset_state"
# Act based on mode
case "$mode" in
suggest)
message="[git-pilot] You have made ${threshold} file changes since the last commit (${modified_files}). You MUST commit your progress now for easier rollback. Use AskUserQuestion to confirm with the user, then commit with a descriptive message following the configured commit format."
message="[git-pilot] ${threshold} file changes since last commit (${modified_files})"
;;
auto)
message="[git-pilot] Auto-commit threshold reached (${threshold} file changes). You MUST commit your current changes NOW with a descriptive message following the commit format: ${commit_pattern}. Do not continue writing code until this commit is made."
message="[git-pilot] Auto-commit threshold reached (${threshold} changes)commit now"
;;
silent)
commit_msg="${wip_prefix}checkpoint after ${threshold} file changes"
Expand All @@ -90,9 +88,9 @@ case "$mode" in
;;
*)
# Unknown mode, fall back to suggest
message="[git-pilot] You have made ${threshold} file changes since the last commit (${modified_files}). You MUST commit your progress now for easier rollback. Use AskUserQuestion to confirm with the user, then commit with a descriptive message following the configured commit format."
message="[git-pilot] ${threshold} file changes since last commit (${modified_files})"
;;
esac

# Output the result
jq -n --arg msg "$message" '{continue: true, systemMessage: $msg}'
jq -n --arg msg "$message" '{"continue": true, "additionalContext": $msg}'
5 changes: 2 additions & 3 deletions plugins/git-pilot/scripts/prompt-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,5 @@ commit_count=$(git rev-list --count "${default_branch}..${current_branch}" 2>/de
if [[ "$commit_count" == "0" ]]; then echo '{"continue": true}'; exit 0; fi

branch_purpose=$(derive_branch_purpose "$current_branch")
recent_commits=$(git log "${default_branch}..${current_branch}" --oneline --no-decorate -5 2>/dev/null || true)
message="[git-pilot] You are on branch '${current_branch}' (purpose: ${branch_purpose}, ${commit_count} commit(s)). Before acting on the user's next request, assess whether the request is related to this branch's purpose. If unrelated, STOP and use AskUserQuestion to suggest creating a new branch. Recent commits: ${recent_commits}"
jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}'
message="[git-pilot] Branch '${current_branch}' (${branch_purpose}, ${commit_count} commits)"
jq -n --arg msg "$message" '{"continue": true, "additionalContext": $msg}'
55 changes: 21 additions & 34 deletions plugins/git-pilot/scripts/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ messages=()

# Step 5: Check dependencies
if ! command -v git >/dev/null 2>&1; then
messages+=("[git-pilot] Warning: git is not installed. Git workflow features are disabled.")
messages+=("[git-pilot] git not installed -- workflow features disabled")
fi

if ! command -v jq >/dev/null 2>&1; then
messages+=("[git-pilot] Warning: jq is not installed. Configuration loading may not work correctly.")
messages+=("[git-pilot] jq not installed -- config loading may fail")
fi

# Step 6: Git init check
Expand All @@ -43,12 +43,12 @@ if ! is_git_repo; then
if [[ ! -f ".gitignore" ]]; then
touch .gitignore
fi
messages+=("[git-pilot] Initialized git repository with default branch '${default_branch}'.")
messages+=("[git-pilot] Initialized git repo (branch: ${default_branch})")
else
messages+=("[git-pilot] Warning: Failed to initialize git repository.")
messages+=("[git-pilot] git init failed")
fi
else
messages+=("[git-pilot] Warning: Current directory is not a git repository. Run 'git init' to initialize one.")
messages+=("[git-pilot] Not a git repository")
fi
fi

Expand All @@ -57,9 +57,8 @@ if is_git_repo && has_remote; then
auto_fetch=$(get_config "$CONFIG" '.git.autoFetch' 'true')
remote_name=$(get_config "$CONFIG" '.remote.defaultName' 'origin')
if [[ "$auto_fetch" == "true" ]]; then
retries=$(get_config "$CONFIG" '.git.fetchRetries' '2')
if ! fetch_with_retry "$remote_name" "$CONFIG"; then
messages+=("[git-pilot] Warning: Could not fetch from '${remote_name}' after ${retries} attempts. Network may be unavailable. Proceeding without remote sync.")
messages+=("[git-pilot] Remote fetch failed -- proceeding offline")
fi
fi
fi
Expand All @@ -78,9 +77,9 @@ if is_git_repo; then
| grep -m1 'checkout: moving from' \
| sed 's/checkout: moving from \([^ ]*\) to .*/\1/' || true)
if [[ -n "$prev_branch" ]]; then
messages+=("[git-pilot] Detached HEAD at ${head_sha}. You MUST NOT make any code changes in detached HEAD state. Use AskUserQuestion NOW to ask the user to choose: (1) return to '${prev_branch}', (2) create a new branch from HEAD, or (3) continue in detached state. Do not proceed until the user responds.")
messages+=("[git-pilot] Detached HEAD at ${head_sha} (previous branch: ${prev_branch})")
else
messages+=("[git-pilot] Detached HEAD at ${head_sha}. You MUST NOT make any code changes in detached HEAD state. Use AskUserQuestion NOW to ask the user to choose: (1) create a new branch from HEAD, or (2) continue in detached state. Do not proceed until the user responds.")
messages+=("[git-pilot] Detached HEAD at ${head_sha}")
fi
fi

Expand All @@ -92,47 +91,35 @@ if is_git_repo; then
behind:*)
behind_count="${tracking_status#behind:}"
if git merge --ff-only "${remote_name}/${current_branch}" >/dev/null 2>&1; then
messages+=("[git-pilot] Branch '${current_branch}' was ${behind_count} commit(s) behind '${remote_name}/${current_branch}'. Fast-forwarded to latest.")
messages+=("[git-pilot] Branch '${current_branch}' fast-forwarded ${behind_count} commit(s)")
else
messages+=("[git-pilot] Branch '${current_branch}' is ${behind_count} commit(s) behind '${remote_name}/${current_branch}' and fast-forward failed. You MUST use AskUserQuestion NOW to ask the user to choose: (1) pull with merge, (2) reset to remote, or (3) continue as-is. Do not proceed with any code changes until this is resolved.")
messages+=("[git-pilot] Branch '${current_branch}' is ${behind_count} commit(s) behind remote")
fi
;;
diverged:*:*)
IFS=':' read -r _ ahead_count behind_count <<< "$tracking_status"
messages+=("[git-pilot] Branch '${current_branch}' has diverged from '${remote_name}/${current_branch}' (${ahead_count} local, ${behind_count} remote). You MUST use AskUserQuestion NOW to ask the user to choose: (1) rebase onto remote, (2) merge remote changes, (3) reset to remote, or (4) continue as-is. Do not make any code changes until this is resolved.")
messages+=("[git-pilot] Branch '${current_branch}' diverged: ${ahead_count} local, ${behind_count} remote")
;;
ahead:*)
ahead_count="${tracking_status#ahead:}"
messages+=("[git-pilot] Branch '${current_branch}' has ${ahead_count} unpushed commit(s). Before continuing, use AskUserQuestion to ask the user whether to push now or continue working.")
messages+=("[git-pilot] ${ahead_count} unpushed commit(s) on '${current_branch}'")
;;
# up-to-date and no-remote: no message
esac
fi

# Step 7c: Branch creation prompt (existing v1 logic)
# Step 7c: Default branch detection
auto_create=$(get_config "$CONFIG" '.branch.autoCreate' 'true')

if [[ "$auto_create" == "true" ]] && [[ "$current_branch" == "$default_branch" ]]; then
branch_pattern=$(get_config "$CONFIG" '.branch.pattern' '{{type}}/{{description}}')
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 the default branch '${default_branch}' with uncommitted changes. You MUST NOT make any further code changes until this is resolved. Use AskUserQuestion NOW to ask the user to choose: (1) stash changes and create a feature branch, (2) commit current changes first, or (3) continue on '${default_branch}'. Do not proceed until the user responds.")
else
messages+=("[git-pilot] You are on the default branch '${default_branch}'. You MUST NOT make any code changes until a feature branch is created. Use AskUserQuestion to ask the user for the branch type and description NOW, before doing anything else. Pattern: ${branch_pattern}, types: ${branch_types}.")
fi
messages+=("[git-pilot] On default branch '${default_branch}'")
fi
fi

# Step 8: Remote detection (only if we are in a git repo)
if is_git_repo; then
if ! has_remote; then
prompt_remote=$(get_config "$CONFIG" '.remote.promptForRemote' 'true')
skip_remote=$(get_config "$CONFIG" '.remote.skipRemotePrompt' 'false')

if [[ "$prompt_remote" == "true" ]] && [[ "$skip_remote" == "false" ]]; then
messages+=("[git-pilot] No git remote configured. Use AskUserQuestion to ask the user whether to add a remote now (provide the command format: git remote add origin <url>) or skip. This affects push and sync capabilities for this session.")
fi
messages+=("[git-pilot] No git remote configured")
fi
fi

Expand All @@ -152,17 +139,17 @@ if [[ -n "$SESSION_ID" ]]; then
fi

# Step 10: Build and output final JSON
system_message=""
context_lines=""
for msg in "${messages[@]+${messages[@]}}"; do
if [[ -n "$system_message" ]]; then
system_message="${system_message}"$'\n'"${msg}"
if [[ -n "$context_lines" ]]; then
context_lines="${context_lines}"$'\n'"${msg}"
else
system_message="$msg"
context_lines="$msg"
fi
done

if [[ -n "$system_message" ]]; then
jq -n --arg msg "$system_message" '{"continue": true, "systemMessage": $msg}'
if [[ -n "$context_lines" ]]; then
jq -n --arg ctx "$context_lines" '{"continue": true, "additionalContext": $ctx}'
else
jq -n '{"continue": true}'
fi
Expand Down
Loading