fix: harden git workflow skills with better state handling (#406)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,251 @@
|
|||||||
|
---
|
||||||
|
title: "Git workflow skills need explicit state machines for branch, push, and PR state"
|
||||||
|
category: skill-design
|
||||||
|
date: 2026-03-27
|
||||||
|
module: plugins/compound-engineering/skills/git-commit and git-commit-push-pr
|
||||||
|
problem_type: best_practice
|
||||||
|
component: tooling
|
||||||
|
symptoms:
|
||||||
|
- Detached HEAD could fall through to invalid push or PR paths
|
||||||
|
- Untracked-only work could be misclassified as a clean working tree
|
||||||
|
- PR detection could select the wrong PR or mis-handle the no-PR case
|
||||||
|
- Default-branch flows could attempt invalid "open a PR from the default branch" behavior
|
||||||
|
root_cause: missing_workflow_step
|
||||||
|
resolution_type: workflow_improvement
|
||||||
|
severity: high
|
||||||
|
tags:
|
||||||
|
- git-workflows
|
||||||
|
- skill-design
|
||||||
|
- state-machine
|
||||||
|
- detached-head
|
||||||
|
- gh-cli
|
||||||
|
- pr-detection
|
||||||
|
- default-branch
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git workflow skills need explicit state machines for branch, push, and PR state
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The `git-commit` and `git-commit-push-pr` skills had accumulated branch-state and PR-state bugs because they described Git flow in broad prose instead of modeling the workflow as a sequence of explicit state checks. Small wording changes kept introducing regressions around detached HEAD, untracked files, upstream detection, default-branch pushes, and PR lookup.
|
||||||
|
|
||||||
|
## Symptoms
|
||||||
|
|
||||||
|
- `git push -u origin HEAD` could be reached from detached HEAD, where Git rejects the push because `HEAD` is not a branch ref
|
||||||
|
- A repo with only untracked files could be treated as "nothing changed" because `git diff HEAD` is empty for untracked files
|
||||||
|
- A no-PR branch could trigger an error path that looked like a fatal failure instead of an expected "no PR for this branch" state
|
||||||
|
- `gh pr list --head "<branch>"` could match an unrelated PR from another fork with the same branch name
|
||||||
|
- Clean-working-tree flows on the default branch could push default-branch commits and then try to open a PR from the default branch to itself
|
||||||
|
|
||||||
|
## What Didn't Work
|
||||||
|
|
||||||
|
- Using a single early `git branch --show-current` result and referring back to it later. Once the workflow creates a branch, the earlier value is stale.
|
||||||
|
- Using `git diff HEAD` as the definition of "has changes." It does not account for untracked files.
|
||||||
|
- Treating every non-zero exit from `gh pr view` as a fatal failure. "No PR for this branch" is often a normal branch state.
|
||||||
|
- Switching from `gh pr view` to `gh pr list --head "<branch>"` to avoid the no-PR error path. This improved ergonomics but weakened correctness because `gh pr list` cannot disambiguate `<owner>:<branch>`.
|
||||||
|
- Adding a "clean working tree" fast path before re-checking whether the current branch was still the default branch. That let the workflow skip the feature-branch safety gate and head straight toward invalid push/PR transitions.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Treat the skill as a small state machine. For each transition, run the command that answers the next question directly, then branch on that result instead of carrying state forward in prose.
|
||||||
|
|
||||||
|
### 1. Use `git status` as the source of truth for working-tree cleanliness
|
||||||
|
|
||||||
|
Use the `git status` result from Step 1 to decide whether the tree is clean. This covers staged, modified, and untracked files.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Clean working tree:
|
||||||
|
- no staged files
|
||||||
|
- no modified files
|
||||||
|
- no untracked files
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use `git diff HEAD` as the cleanliness check.
|
||||||
|
|
||||||
|
### 2. Re-read branch state after every branch-changing transition
|
||||||
|
|
||||||
|
When the workflow starts in detached HEAD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch --show-current
|
||||||
|
git checkout -b <branch-name>
|
||||||
|
git branch --show-current
|
||||||
|
```
|
||||||
|
|
||||||
|
The second `git branch --show-current` is not redundant. It converts "the skill thinks it created branch X" into "Git says the current branch is X."
|
||||||
|
|
||||||
|
Apply the same pattern before default-branch safety checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch --show-current
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it again at the moment the decision is needed. Do not rely on a branch value captured earlier in the workflow.
|
||||||
|
|
||||||
|
### 3. Split "upstream exists" from "there are unpushed commits"
|
||||||
|
|
||||||
|
Check upstream existence first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rev-parse --abbrev-ref --symbolic-full-name @{u}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only if that succeeds, check for unpushed commits:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log <upstream>..HEAD --oneline
|
||||||
|
```
|
||||||
|
|
||||||
|
This avoids conflating "no upstream configured yet" with "nothing to push."
|
||||||
|
|
||||||
|
### 4. Prefer current-branch `gh pr view` semantics over bare branch-name search
|
||||||
|
|
||||||
|
For "does this branch already have a PR?" use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr view --json url,title,state
|
||||||
|
```
|
||||||
|
|
||||||
|
Interpret it as a state check:
|
||||||
|
|
||||||
|
- PR data returned -> PR exists for the current branch
|
||||||
|
- Non-zero exit with output indicating no PR for the current branch -> expected "no PR yet" state
|
||||||
|
- Any other failure -> real error
|
||||||
|
|
||||||
|
This keeps PR detection tied to the current branch context instead of a bare branch name that may be reused across forks.
|
||||||
|
|
||||||
|
### 5. Keep the default-branch safety gate ahead of push/PR transitions
|
||||||
|
|
||||||
|
If the current branch is `main`, `master`, or the resolved default branch, and the workflow is about to push or create a PR:
|
||||||
|
|
||||||
|
- ask whether to create a feature branch first
|
||||||
|
- if the user agrees, create the branch and re-read the branch name
|
||||||
|
- if the user declines in `git-commit-push-pr`, stop rather than trying to open a PR from the default branch
|
||||||
|
|
||||||
|
This prevents "push default branch, then attempt impossible PR flow" behavior.
|
||||||
|
|
||||||
|
## Why This Works
|
||||||
|
|
||||||
|
Git workflows look linear in prose but are actually stateful. Detached HEAD, missing upstreams, untracked files, and existing-vs-missing PRs are all separate dimensions of state. The bug pattern was always the same: the skill would observe one dimension once, then assume it remained true after a later transition.
|
||||||
|
|
||||||
|
The fix is not more prose. The fix is explicit re-checks at each transition boundary:
|
||||||
|
|
||||||
|
- branch state after branch creation
|
||||||
|
- cleanliness from `git status`, not a partial diff
|
||||||
|
- upstream existence before unpushed-commit checks
|
||||||
|
- PR existence tied to the current branch, not only its name
|
||||||
|
- default-branch safety before any push/PR transition
|
||||||
|
|
||||||
|
This turns a brittle narrative into a deterministic control flow with a small number of clear state transitions.
|
||||||
|
|
||||||
|
## Edge Cases We Hit While Fixing This
|
||||||
|
|
||||||
|
These were not hypothetical concerns. Each one showed up while revising `git-commit` and `git-commit-push-pr`, and several "fixes" introduced a new bug one step later in the flow.
|
||||||
|
|
||||||
|
### 1. Detached HEAD can reappear as a later bug even after it seems "handled"
|
||||||
|
|
||||||
|
An early version only guarded detached HEAD in the PR-detection step. That looked fine until the workflow added a "clean working tree" shortcut before PR detection. In detached HEAD with committed local work, that shortcut could jump directly to push logic and hit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push -u origin HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
which fails because detached HEAD is not a branch ref.
|
||||||
|
|
||||||
|
Learning: detached HEAD must be handled before any later shortcut can skip around it.
|
||||||
|
|
||||||
|
### 2. Creating a branch is not enough; the skill must re-read which branch Git says is current
|
||||||
|
|
||||||
|
Another revision created a branch from detached HEAD but still described later steps as using "the branch name from Step 1." If Step 1 originally ran in detached HEAD, that earlier branch value was empty. Later PR detection could still use the stale empty value.
|
||||||
|
|
||||||
|
Learning: after `git checkout -b <branch-name>`, run `git branch --show-current` again and treat that output as the only trusted branch name.
|
||||||
|
|
||||||
|
### 3. Bare branch-name PR lookup fixed one problem and created another
|
||||||
|
|
||||||
|
We switched from `gh pr view` to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr list --head "<branch>" --json url,title,state --jq '.[0] // empty'
|
||||||
|
```
|
||||||
|
|
||||||
|
because `gh pr view` was surfacing a non-zero exit when no PR existed. That improved the no-PR path, but it introduced a correctness problem: `gh pr list --head` matches on branch name only, and GitHub CLI does not support `<owner>:<branch>` syntax for that flag. In a multi-fork repo, another person's PR can reuse the same branch name.
|
||||||
|
|
||||||
|
Learning: for "PR for the current branch," `gh pr view` is safer even if the no-PR state must be interpreted explicitly.
|
||||||
|
|
||||||
|
### 4. "No PR" is not an error in the workflow, even if the CLI exits non-zero
|
||||||
|
|
||||||
|
The original reason for changing away from `gh pr view` was that a branch with no PR looked like a command failure. But for this workflow, "no PR yet" is often the expected state and should lead to creation logic, not stop the skill.
|
||||||
|
|
||||||
|
Learning: document expected non-zero exits as state transitions, not generic failures.
|
||||||
|
|
||||||
|
### 5. `git diff HEAD` misses one of the most common commit cases: untracked files
|
||||||
|
|
||||||
|
At one point the skill used `git diff HEAD` to decide whether work existed. In a repo with only a newly created file, `git diff HEAD` is empty even though `git status` shows `?? file`.
|
||||||
|
|
||||||
|
Learning: untracked-only work is a first-class case. Use `git status` as the cleanliness check.
|
||||||
|
|
||||||
|
### 6. "No upstream" and "nothing to push" are different states
|
||||||
|
|
||||||
|
An early shortcut treated an error from `git log @{u}..HEAD` as "nothing to push." That is wrong on a new feature branch with local commits but no upstream yet. The branch still needs its first push.
|
||||||
|
|
||||||
|
Learning: first check whether an upstream exists, then check whether there are unpushed commits.
|
||||||
|
|
||||||
|
### 7. Default-branch safety can be bypassed by a convenience shortcut
|
||||||
|
|
||||||
|
Another revision added a clean-working-tree shortcut that said "if there are unpushed commits, skip commit and continue to push." That worked on feature branches but accidentally skipped the normal "don't work directly on main/default branch" safety gate. The result was: push default-branch commits, then head toward PR creation.
|
||||||
|
|
||||||
|
Learning: every path that can lead to push or PR creation must pass through a default-branch safety check.
|
||||||
|
|
||||||
|
### 8. Declining feature-branch creation on the default branch must stop the PR workflow
|
||||||
|
|
||||||
|
One fix asked the user whether to create a feature branch first when clean-tree logic found unpushed default-branch commits. But if the user declined, the workflow still continued to push and then attempt PR creation. That leads to an impossible "open a PR from the default branch to itself" situation.
|
||||||
|
|
||||||
|
Learning: in `git-commit-push-pr`, declining feature-branch creation on the default branch is a stop condition, not a continue condition.
|
||||||
|
|
||||||
|
### 9. Clean-working-tree shortcuts interact with branch safety, PR state, and upstream state all at once
|
||||||
|
|
||||||
|
The hardest bugs came from the "no local edits, but there may still be work to do" path. That single branch of logic had to answer all of these:
|
||||||
|
|
||||||
|
- Is the current branch detached?
|
||||||
|
- Is the current branch the default branch?
|
||||||
|
- Does the branch have an upstream?
|
||||||
|
- Are there unpushed commits?
|
||||||
|
- Does a PR already exist?
|
||||||
|
|
||||||
|
Missing any one of those checks produced a new bug.
|
||||||
|
|
||||||
|
Learning: clean-working-tree shortcuts are the highest-risk part of Git workflow skills because they combine the most state dimensions at once.
|
||||||
|
|
||||||
|
### 10. Git workflow skills are unusually prone to whack-a-mole regressions
|
||||||
|
|
||||||
|
The meta-pattern across all these fixes was:
|
||||||
|
|
||||||
|
1. Improve one failure mode
|
||||||
|
2. Reveal that another state transition was only implicitly modeled
|
||||||
|
3. Add a new branch in the prose
|
||||||
|
4. Discover that the new branch skipped a previously safe checkpoint
|
||||||
|
|
||||||
|
Learning: these skills should be designed and reviewed like tiny state machines, not as narrative instructions. Any change to one state transition should trigger a walkthrough of all adjacent states before considering the skill fixed.
|
||||||
|
|
||||||
|
## Prevention
|
||||||
|
|
||||||
|
- For Git/GitHub skills, treat workflow design as a state machine, not as a linear checklist.
|
||||||
|
- Re-run the command that answers the current question at the point of decision. Do not rely on values gathered earlier if a mutating command may have changed them.
|
||||||
|
- Use `git status` for "is there local work?" and reserve `git diff` for describing content, not determining whether work exists.
|
||||||
|
- Model expected non-zero CLI exits explicitly when they represent state, such as `gh pr view` on a branch with no PR.
|
||||||
|
- Avoid branch-name-only PR detection for multi-fork repos. If the command cannot disambiguate branch ownership, prefer a current-branch-aware command even if the failure path is slightly messier.
|
||||||
|
- Keep default-branch safety checks in every path that can lead to push or PR creation, including "clean working tree but unpushed commits" shortcuts.
|
||||||
|
- When editing skill logic, manually walk these cases before considering the change complete:
|
||||||
|
- detached HEAD with uncommitted changes
|
||||||
|
- detached HEAD with committed but unpushed work
|
||||||
|
- untracked-only files
|
||||||
|
- feature branch with no upstream
|
||||||
|
- feature branch with upstream and no PR
|
||||||
|
- feature branch with upstream and an existing PR
|
||||||
|
- default branch with unpushed commits
|
||||||
|
- non-`main` default branch names such as `develop` or `trunk`
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
- [docs/solutions/skill-design/script-first-skill-architecture.md](/Users/tmchow/conductor/workspaces/compound-engineering-plugin/miami-v2/docs/solutions/skill-design/script-first-skill-architecture.md)
|
||||||
|
- [docs/solutions/skill-design/pass-paths-not-content-to-subagents-2026-03-26.md](/Users/tmchow/conductor/workspaces/compound-engineering-plugin/miami-v2/docs/solutions/skill-design/pass-paths-not-content-to-subagents-2026-03-26.md)
|
||||||
@@ -32,7 +32,7 @@ The primary entry points for engineering work, invoked as slash commands:
|
|||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `git-clean-gone-branches` | Clean up local branches whose remote tracking branch is gone |
|
| `git-clean-gone-branches` | Clean up local branches whose remote tracking branch is gone |
|
||||||
| `git-commit` | Create a git commit with a value-communicating message |
|
| `git-commit` | Create a git commit with a value-communicating message |
|
||||||
| `git-commit-push-pr` | Commit, push, and open a PR with an adaptive, value-first description |
|
| `git-commit-push-pr` | Commit, push, and open a PR with an adaptive description; also update an existing PR description |
|
||||||
| `git-worktree` | Manage Git worktrees for parallel development |
|
| `git-worktree` | Manage Git worktrees for parallel development |
|
||||||
|
|
||||||
### Workflow Utilities
|
### Workflow Utilities
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ bash scripts/clean-gone
|
|||||||
|
|
||||||
[scripts/clean-gone](./scripts/clean-gone)
|
[scripts/clean-gone](./scripts/clean-gone)
|
||||||
|
|
||||||
The script runs `git fetch --prune` first, then parses `git branch -vv` for branches marked `: gone]`. It uses `command git` to bypass shell aliases and RTK proxies.
|
The script runs `git fetch --prune` first, then parses `git branch -vv` for branches marked `: gone]`.
|
||||||
|
|
||||||
If the script outputs `__NONE__`, report that no stale branches were found and stop.
|
If the script outputs `__NONE__`, report that no stale branches were found and stop.
|
||||||
|
|
||||||
@@ -45,9 +45,9 @@ This is a yes-or-no decision on the entire list -- do not offer multi-selection
|
|||||||
|
|
||||||
If the user confirms, delete each branch. For each branch:
|
If the user confirms, delete each branch. For each branch:
|
||||||
|
|
||||||
1. Check if it has an associated worktree (`command git worktree list | grep "\\[$branch\\]"`)
|
1. Check if it has an associated worktree (`git worktree list | grep "\\[$branch\\]"`)
|
||||||
2. If a worktree exists and is not the main repo root, remove it first: `command git worktree remove --force "$worktree_path"`
|
2. If a worktree exists and is not the main repo root, remove it first: `git worktree remove --force "$worktree_path"`
|
||||||
3. Delete the branch: `command git branch -D "$branch"`
|
3. Delete the branch: `git branch -D "$branch"`
|
||||||
|
|
||||||
Report results as you go:
|
Report results as you go:
|
||||||
|
|
||||||
@@ -61,7 +61,3 @@ Cleaned up 3 branches.
|
|||||||
```
|
```
|
||||||
|
|
||||||
If the user declines, acknowledge and stop without deleting anything.
|
If the user declines, acknowledge and stop without deleting anything.
|
||||||
|
|
||||||
## Important: Use `command git`
|
|
||||||
|
|
||||||
Always invoke git as `command git` in shell commands. This bypasses shell aliases and tools like RTK (Rust Token Killer) that proxy git commands, ensuring consistent behavior and output parsing.
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# clean-gone: List local branches whose remote tracking branch is gone.
|
# clean-gone: List local branches whose remote tracking branch is gone.
|
||||||
# Outputs one branch name per line, or nothing if none found.
|
# Outputs one branch name per line, or nothing if none found.
|
||||||
# Uses `command git` to bypass aliases and RTK proxies.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Ensure we have current remote state
|
# Ensure we have current remote state
|
||||||
command git fetch --prune 2>/dev/null
|
git fetch --prune 2>/dev/null
|
||||||
|
|
||||||
# Find branches marked [gone] in tracking info.
|
# Find branches marked [gone] in tracking info.
|
||||||
# `git branch -vv` output format:
|
# `git branch -vv` output format:
|
||||||
@@ -37,7 +36,7 @@ while IFS= read -r line; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
gone_branches+=("$branch_name")
|
gone_branches+=("$branch_name")
|
||||||
done < <(command git branch -vv 2>/dev/null | grep ': gone]')
|
done < <(git branch -vv 2>/dev/null | grep ': gone]')
|
||||||
|
|
||||||
if [[ ${#gone_branches[@]} -eq 0 ]]; then
|
if [[ ${#gone_branches[@]} -eq 0 ]]; then
|
||||||
echo "__NONE__"
|
echo "__NONE__"
|
||||||
|
|||||||
@@ -1,35 +1,118 @@
|
|||||||
---
|
---
|
||||||
name: git-commit-push-pr
|
name: git-commit-push-pr
|
||||||
description: Commit, push, and open a PR with an adaptive, value-first description. Use when the user says "commit and PR", "push and open a PR", "ship this", "create a PR", "open a pull request", "commit push PR", or wants to go from working changes to an open pull request in one step. Produces PR descriptions that scale in depth with the complexity of the change, avoiding cookie-cutter templates.
|
description: Commit, push, and open a PR with an adaptive, value-first description. Use when the user says "commit and PR", "push and open a PR", "ship this", "create a PR", "open a pull request", "commit push PR", or wants to go from working changes to an open pull request in one step. Also use when the user says "update the PR description", "refresh the PR description", "freshen the PR", or wants to rewrite an existing PR description. Produces PR descriptions that scale in depth with the complexity of the change, avoiding cookie-cutter templates.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Git Commit, Push, and PR
|
# Git Commit, Push, and PR
|
||||||
|
|
||||||
Go from working tree changes to an open pull request in a single workflow. The key differentiator of this skill is PR descriptions that communicate *value and intent* proportional to the complexity of the change.
|
Go from working tree changes to an open pull request in a single workflow, or update an existing PR description. The key differentiator of this skill is PR descriptions that communicate *value and intent* proportional to the complexity of the change.
|
||||||
|
|
||||||
## Workflow
|
## Mode detection
|
||||||
|
|
||||||
|
If the user is asking to update, refresh, or rewrite an existing PR description (with no mention of committing or pushing), this is a **description-only update**. The user may also provide a focus for the update (e.g., "update the PR description and add the benchmarking results"). Note any focus instructions for use in DU-3.
|
||||||
|
|
||||||
|
For description-only updates, follow the Description Update workflow below. Otherwise, follow the full workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description Update workflow
|
||||||
|
|
||||||
|
### DU-1: Confirm intent
|
||||||
|
|
||||||
|
Ask the user to confirm: "Update the PR description for this branch?" Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the question and wait for the user's reply.
|
||||||
|
|
||||||
|
If the user declines, stop.
|
||||||
|
|
||||||
|
### DU-2: Find the PR
|
||||||
|
|
||||||
|
Run these commands to identify the branch and locate the PR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch --show-current
|
||||||
|
```
|
||||||
|
|
||||||
|
If empty (detached HEAD), report that there is no branch to update and stop.
|
||||||
|
|
||||||
|
Otherwise, check for an existing open PR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr view --json url,title,state
|
||||||
|
```
|
||||||
|
|
||||||
|
Interpret the result. Do not treat every non-zero exit as a fatal error here:
|
||||||
|
|
||||||
|
- If it returns PR data with `state: OPEN`, an open PR exists for the current branch.
|
||||||
|
- If it returns PR data with a non-OPEN state (CLOSED, MERGED), treat this as "no open PR." Report that no open PR exists for this branch and stop.
|
||||||
|
- If it exits non-zero and the output indicates that no pull request exists for the current branch, treat that as the normal "no PR for this branch" state. Report that no open PR exists for this branch and stop.
|
||||||
|
- If it errors for another reason (auth, network, repo config), report the error and stop.
|
||||||
|
|
||||||
|
### DU-3: Write and apply the updated description
|
||||||
|
|
||||||
|
Read the current PR description:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr view --json body --jq '.body'
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the "Detect the base branch and remote" and "Gather the branch scope" sections of Step 6 to get the full branch diff. Use the PR found in DU-2 as the existing PR for base branch detection. Then write a new description following the writing principles in Step 6. If the user provided a focus, incorporate it into the description alongside the branch diff context.
|
||||||
|
|
||||||
|
Compare the new description against the current one and summarize the substantial changes for the user (e.g., "Added coverage of the new caching layer, updated test plan, removed outdated migration notes"). If the user provided a focus, confirm it was addressed. Ask the user to confirm before applying. Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the summary and wait for the user's reply.
|
||||||
|
|
||||||
|
If confirmed, apply:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr edit --body "$(cat <<'EOF'
|
||||||
|
Updated description here
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Report the PR URL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full workflow
|
||||||
|
|
||||||
### Step 1: Gather context
|
### Step 1: Gather context
|
||||||
|
|
||||||
Run these commands. Use `command git` to bypass aliases and RTK proxies.
|
Run these commands.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command git status
|
git status
|
||||||
command git diff HEAD
|
git diff HEAD
|
||||||
command git branch --show-current
|
git branch --show-current
|
||||||
command git log --oneline -10
|
git log --oneline -10
|
||||||
command git rev-parse --abbrev-ref origin/HEAD
|
git rev-parse --abbrev-ref origin/HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
The last command returns the remote default branch (e.g., `origin/main`). Strip the `origin/` prefix to get the branch name. If the command fails or returns a bare `HEAD`, try:
|
The last command returns the remote default branch (e.g., `origin/main`). Strip the `origin/` prefix to get the branch name. If the command fails or returns a bare `HEAD`, try:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
||||||
```
|
```
|
||||||
|
|
||||||
If both fail, fall back to `main`.
|
If both fail, fall back to `main`.
|
||||||
|
|
||||||
If there are no changes, report that and stop.
|
Run `git branch --show-current`. If it returns an empty result, the repository is in detached HEAD state. Explain that a branch is required before committing and pushing. Ask whether to create a feature branch now. Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the options and wait for the user's reply.
|
||||||
|
|
||||||
|
- If the user agrees, derive a descriptive branch name from the change content, create it with `git checkout -b <branch-name>`, then run `git branch --show-current` again and use that result as the current branch name for the rest of the workflow.
|
||||||
|
- If the user declines, stop.
|
||||||
|
|
||||||
|
If the `git status` result from this step shows a clean working tree (no staged, modified, or untracked files), check whether there are unpushed commits or a missing PR before stopping:
|
||||||
|
|
||||||
|
1. Run `git branch --show-current` to get the current branch name.
|
||||||
|
2. Run `git rev-parse --abbrev-ref --symbolic-full-name @{u}` to check whether an upstream is configured.
|
||||||
|
3. If the command succeeds, run `git log <upstream>..HEAD --oneline` using the upstream name from the previous command.
|
||||||
|
4. If an upstream is configured, check for an existing PR using the method in Step 3.
|
||||||
|
|
||||||
|
- If the current branch is `main`, `master`, or the resolved default branch from Step 1 and there is **no upstream** or there are **unpushed commits**, explain that pushing now would use the default branch directly. Ask whether to create a feature branch first. Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the options and wait for the user's reply.
|
||||||
|
- If the user agrees, derive a descriptive branch name from the change content, create it with `git checkout -b <branch-name>`, then continue from Step 5 (push).
|
||||||
|
- If the user declines, report that this workflow cannot open a PR from the default branch directly and stop.
|
||||||
|
- If there is **no upstream**, treat the branch as needing its first push. Skip Step 4 (commit) and continue from Step 5 (push).
|
||||||
|
- If there are **unpushed commits**, skip Step 4 (commit) and continue from Step 5 (push).
|
||||||
|
- If all commits are pushed but **no open PR exists** and the current branch is `main`, `master`, or the resolved default branch from Step 1, report that there is no feature branch work to open as a PR and stop.
|
||||||
|
- If all commits are pushed but **no open PR exists**, skip Steps 4-5 and continue from Step 6 (write the PR description) and Step 7 (create the PR).
|
||||||
|
- If all commits are pushed **and an open PR exists**, report that and stop -- there is nothing to do.
|
||||||
|
|
||||||
### Step 2: Determine conventions
|
### Step 2: Determine conventions
|
||||||
|
|
||||||
@@ -41,22 +124,24 @@ Follow this priority order for commit messages *and* PR titles:
|
|||||||
|
|
||||||
### Step 3: Check for existing PR
|
### Step 3: Check for existing PR
|
||||||
|
|
||||||
Before committing, check whether a PR already exists for the current branch:
|
Run `git branch --show-current` to get the current branch name. If it returns an empty result here, report that the workflow is still in detached HEAD state and stop.
|
||||||
|
|
||||||
|
Then check for an existing open PR:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command gh pr view --json url,title,state
|
gh pr view --json url,title,state
|
||||||
```
|
```
|
||||||
|
|
||||||
Interpret the result:
|
Interpret the result. Do not treat every non-zero exit as a fatal error here:
|
||||||
|
|
||||||
- If it **returns PR data with `state: OPEN`**, note the URL and continue to Step 4 (commit) and Step 5 (push). Then skip to Step 7 (existing PR flow) instead of creating a new PR.
|
- If it **returns PR data with `state: OPEN`**, an open PR exists for the current branch. Note the URL and continue to Step 4 (commit) and Step 5 (push). Then skip to Step 7 (existing PR flow) instead of creating a new PR.
|
||||||
- If it **returns PR data with a non-OPEN state** (CLOSED, MERGED), treat this the same as "no PR exists" -- the previous PR is done and a new one is needed.
|
- If it **returns PR data with a non-OPEN state** (CLOSED, MERGED), treat this the same as "no PR exists" -- the previous PR is done and a new one is needed. Continue to Step 4 through Step 8 as normal.
|
||||||
- If it **errors with "no pull requests found"**, no PR exists. Continue to Step 4 through Step 8 as normal.
|
- If it **exits non-zero and the output indicates that no pull request exists for the current branch**, no PR exists. Continue to Step 4 through Step 8 as normal.
|
||||||
- If it **errors for another reason** (auth, network, repo config), report the error to the user and stop.
|
- If it **errors** (auth, network, repo config), report the error to the user and stop.
|
||||||
|
|
||||||
### Step 4: Branch, stage, and commit
|
### Step 4: Branch, stage, and commit
|
||||||
|
|
||||||
1. If on `main`, `master`, or the resolved default branch from Step 1, create a descriptive feature branch first (`command git checkout -b <branch-name>`). Derive the branch name from the change content.
|
1. Run `git branch --show-current`. If it returns `main`, `master`, or the resolved default branch from Step 1, create a descriptive feature branch first with `git checkout -b <branch-name>`. Derive the branch name from the change content.
|
||||||
2. Before staging everything together, scan the changed files for naturally distinct concerns. If modified files clearly group into separate logical changes (e.g., a refactor in one set of files and a new feature in another), create separate commits for each group. Keep this lightweight -- group at the **file level only** (no `git add -p`), split only when obvious, and aim for two or three logical commits at most. If it's ambiguous, one commit is fine.
|
2. Before staging everything together, scan the changed files for naturally distinct concerns. If modified files clearly group into separate logical changes (e.g., a refactor in one set of files and a new feature in another), create separate commits for each group. Keep this lightweight -- group at the **file level only** (no `git add -p`), split only when obvious, and aim for two or three logical commits at most. If it's ambiguous, one commit is fine.
|
||||||
3. Stage relevant files by name. Avoid `git add -A` or `git add .` to prevent accidentally including sensitive files.
|
3. Stage relevant files by name. Avoid `git add -A` or `git add .` to prevent accidentally including sensitive files.
|
||||||
4. Commit following the conventions from Step 2. Use a heredoc for the message.
|
4. Commit following the conventions from Step 2. Use a heredoc for the message.
|
||||||
@@ -64,7 +149,7 @@ Interpret the result:
|
|||||||
### Step 5: Push
|
### Step 5: Push
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command git push -u origin HEAD
|
git push -u origin HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Write the PR description
|
### Step 6: Write the PR description
|
||||||
@@ -79,26 +164,26 @@ Use this fallback chain. Stop at the first that succeeds:
|
|||||||
|
|
||||||
1. **PR metadata** (if an existing PR was found in Step 3):
|
1. **PR metadata** (if an existing PR was found in Step 3):
|
||||||
```bash
|
```bash
|
||||||
command gh pr view --json baseRefName,url
|
gh pr view --json baseRefName,url
|
||||||
```
|
```
|
||||||
Extract `baseRefName` as the base branch name. The PR URL contains the base repository (`https://github.com/<owner>/<repo>/pull/...`). Determine which local remote corresponds to that repository:
|
Extract `baseRefName` as the base branch name. The PR URL contains the base repository (`https://github.com/<owner>/<repo>/pull/...`). Determine which local remote corresponds to that repository:
|
||||||
```bash
|
```bash
|
||||||
command git remote -v
|
git remote -v
|
||||||
```
|
```
|
||||||
Match the `owner/repo` from the PR URL against the fetch URLs. Use the matching remote as the base remote. If no remote matches, fall back to `origin`.
|
Match the `owner/repo` from the PR URL against the fetch URLs. Use the matching remote as the base remote. If no remote matches, fall back to `origin`.
|
||||||
2. **`origin/HEAD` symbolic ref:**
|
2. **`origin/HEAD` symbolic ref:**
|
||||||
```bash
|
```bash
|
||||||
command git symbolic-ref --quiet --short refs/remotes/origin/HEAD
|
git symbolic-ref --quiet --short refs/remotes/origin/HEAD
|
||||||
```
|
```
|
||||||
Strip the `origin/` prefix from the result. Use `origin` as the base remote.
|
Strip the `origin/` prefix from the result. Use `origin` as the base remote.
|
||||||
3. **GitHub default branch metadata:**
|
3. **GitHub default branch metadata:**
|
||||||
```bash
|
```bash
|
||||||
command gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
||||||
```
|
```
|
||||||
Use `origin` as the base remote.
|
Use `origin` as the base remote.
|
||||||
4. **Common branch names** -- check `main`, `master`, `develop`, `trunk` in order. Use the first that exists on the remote:
|
4. **Common branch names** -- check `main`, `master`, `develop`, `trunk` in order. Use the first that exists on the remote:
|
||||||
```bash
|
```bash
|
||||||
command git rev-parse --verify origin/<candidate>
|
git rev-parse --verify origin/<candidate>
|
||||||
```
|
```
|
||||||
Use `origin` as the base remote.
|
Use `origin` as the base remote.
|
||||||
|
|
||||||
@@ -110,23 +195,23 @@ Once the base branch and remote are known:
|
|||||||
|
|
||||||
1. Verify the remote-tracking ref exists locally and fetch if needed:
|
1. Verify the remote-tracking ref exists locally and fetch if needed:
|
||||||
```bash
|
```bash
|
||||||
command git rev-parse --verify <base-remote>/<base-branch>
|
git rev-parse --verify <base-remote>/<base-branch>
|
||||||
```
|
```
|
||||||
If this fails (ref missing or stale), fetch it:
|
If this fails (ref missing or stale), fetch it:
|
||||||
```bash
|
```bash
|
||||||
command git fetch --no-tags <base-remote> <base-branch>
|
git fetch --no-tags <base-remote> <base-branch>
|
||||||
```
|
```
|
||||||
2. Find the merge base:
|
2. Find the merge base:
|
||||||
```bash
|
```bash
|
||||||
command git merge-base <base-remote>/<base-branch> HEAD
|
git merge-base <base-remote>/<base-branch> HEAD
|
||||||
```
|
```
|
||||||
2. List all commits unique to this branch:
|
2. List all commits unique to this branch:
|
||||||
```bash
|
```bash
|
||||||
command git log --oneline <merge-base>..HEAD
|
git log --oneline <merge-base>..HEAD
|
||||||
```
|
```
|
||||||
3. Get the full diff a reviewer will see:
|
3. Get the full diff a reviewer will see:
|
||||||
```bash
|
```bash
|
||||||
command git diff <merge-base>...HEAD
|
git diff <merge-base>...HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the full branch diff and commit list as the basis for the PR description -- not the working-tree diff from Step 1.
|
Use the full branch diff and commit list as the basis for the PR description -- not the working-tree diff from Step 1.
|
||||||
@@ -157,6 +242,7 @@ Use this to select the right description depth:
|
|||||||
- **Lead with value**: The first sentence should tell the reviewer *why this PR exists*, not *what files changed*. "Fixes timeout errors during batch exports" beats "Updated export_handler.py and config.yaml".
|
- **Lead with value**: The first sentence should tell the reviewer *why this PR exists*, not *what files changed*. "Fixes timeout errors during batch exports" beats "Updated export_handler.py and config.yaml".
|
||||||
- **No orphaned opening paragraphs**: If the description uses `##` section headings anywhere, the opening summary must also be under a heading (e.g., `## Summary`). An untitled paragraph followed by titled sections looks like a missing heading. For short descriptions with no sections, a bare paragraph is fine.
|
- **No orphaned opening paragraphs**: If the description uses `##` section headings anywhere, the opening summary must also be under a heading (e.g., `## Summary`). An untitled paragraph followed by titled sections looks like a missing heading. For short descriptions with no sections, a bare paragraph is fine.
|
||||||
- **Describe the net result, not the journey**: The PR description is about the end state -- what changed and why. Do not include work-product details like bugs found and fixed during development, intermediate failures, debugging steps, iteration history, or refactoring done along the way. Those are part of getting the work done, not part of the result. If a bug fix happened during development, the fix is already in the diff -- mentioning it in the description implies it's a separate concern the reviewer should evaluate, when really it's just part of the final implementation. Exception: include process details only when they are critical for a reviewer to understand a design choice (e.g., "tried approach X first but it caused Y, so went with Z instead").
|
- **Describe the net result, not the journey**: The PR description is about the end state -- what changed and why. Do not include work-product details like bugs found and fixed during development, intermediate failures, debugging steps, iteration history, or refactoring done along the way. Those are part of getting the work done, not part of the result. If a bug fix happened during development, the fix is already in the diff -- mentioning it in the description implies it's a separate concern the reviewer should evaluate, when really it's just part of the final implementation. Exception: include process details only when they are critical for a reviewer to understand a design choice (e.g., "tried approach X first but it caused Y, so went with Z instead").
|
||||||
|
- **When commits conflict, trust the final diff**: The commit list is supporting context, not the source of truth for the final PR description. If commit messages describe intermediate steps that were later revised or reverted (for example, "switch to gh pr list" followed by a later change back to `gh pr view`), describe the end state shown by the full branch diff. Do not narrate contradictory commit history as if all of it shipped.
|
||||||
- **Explain the non-obvious**: If the diff is self-explanatory, don't narrate it. Spend description space on things the diff *doesn't* show: why this approach, what was considered and rejected, what the reviewer should pay attention to.
|
- **Explain the non-obvious**: If the diff is self-explanatory, don't narrate it. Spend description space on things the diff *doesn't* show: why this approach, what was considered and rejected, what the reviewer should pay attention to.
|
||||||
- **Use structure when it earns its keep**: Headers, bullet lists, and tables are tools -- use them when they aid comprehension, not as mandatory template sections. An empty "## Breaking Changes" section adds noise.
|
- **Use structure when it earns its keep**: Headers, bullet lists, and tables are tools -- use them when they aid comprehension, not as mandatory template sections. An empty "## Breaking Changes" section adds noise.
|
||||||
- **Markdown tables for data**: When there are before/after comparisons, performance numbers, or option trade-offs, a table communicates density well. Example:
|
- **Markdown tables for data**: When there are before/after comparisons, performance numbers, or option trade-offs, a table communicates density well. Example:
|
||||||
@@ -218,7 +304,7 @@ Fill in at PR creation time:
|
|||||||
#### New PR (no existing PR from Step 3)
|
#### New PR (no existing PR from Step 3)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
||||||
PR description here
|
PR description here
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -238,7 +324,7 @@ The new commits are already on the PR from the push in Step 5. Report the PR URL
|
|||||||
- If **yes** -- write a new description following the same principles in Step 6 (size the full PR, not just the new commits), including the Compound Engineering badge unless one is already present in the existing description. Apply it:
|
- If **yes** -- write a new description following the same principles in Step 6 (size the full PR, not just the new commits), including the Compound Engineering badge unless one is already present in the existing description. Apply it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command gh pr edit --body "$(cat <<'EOF'
|
gh pr edit --body "$(cat <<'EOF'
|
||||||
Updated description here
|
Updated description here
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
@@ -249,7 +335,3 @@ The new commits are already on the PR from the push in Step 5. Report the PR URL
|
|||||||
### Step 8: Report
|
### Step 8: Report
|
||||||
|
|
||||||
Output the PR URL so the user can navigate to it directly.
|
Output the PR URL so the user can navigate to it directly.
|
||||||
|
|
||||||
## Important: Use `command git` and `command gh`
|
|
||||||
|
|
||||||
Always invoke git as `command git` and gh as `command gh` in shell commands. This bypasses shell aliases and tools like RTK (Rust Token Killer) that proxy commands.
|
|
||||||
|
|||||||
@@ -11,27 +11,30 @@ Create a single, well-crafted git commit from the current working tree changes.
|
|||||||
|
|
||||||
### Step 1: Gather context
|
### Step 1: Gather context
|
||||||
|
|
||||||
Run these commands to understand the current state. Use `command git` to bypass aliases and RTK proxies.
|
Run these commands to understand the current state.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command git status
|
git status
|
||||||
command git diff HEAD
|
git diff HEAD
|
||||||
command git branch --show-current
|
git branch --show-current
|
||||||
command git log --oneline -10
|
git log --oneline -10
|
||||||
command git rev-parse --abbrev-ref origin/HEAD
|
git rev-parse --abbrev-ref origin/HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
The last command returns the remote default branch (e.g., `origin/main`). Strip the `origin/` prefix to get the branch name. If the command fails or returns a bare `HEAD`, try:
|
The last command returns the remote default branch (e.g., `origin/main`). Strip the `origin/` prefix to get the branch name. If the command fails or returns a bare `HEAD`, try:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
||||||
```
|
```
|
||||||
|
|
||||||
If both fail, fall back to `main`.
|
If both fail, fall back to `main`.
|
||||||
|
|
||||||
If there are no changes (nothing staged, nothing modified), report that and stop.
|
If the `git status` result from this step shows a clean working tree (no staged, modified, or untracked files), report that there is nothing to commit and stop.
|
||||||
|
|
||||||
If the current branch matches `main`, `master`, or the resolved default branch name, warn the user and ask whether to continue committing here or create a feature branch first. Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the options and wait for the user's reply before proceeding. If the user chooses to create a branch, derive the name from the change content and switch to it before continuing.
|
Run `git branch --show-current`. If it returns an empty result, the repository is in detached HEAD state. Explain that a branch is required before committing if the user wants this work attached to a branch. Ask whether to create a feature branch now. Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the options and wait for the user's reply before proceeding.
|
||||||
|
|
||||||
|
- If the user chooses to create a branch, derive the name from the change content, create it with `git checkout -b <branch-name>`, then run `git branch --show-current` again and use that result as the current branch name for the rest of the workflow.
|
||||||
|
- If the user declines, continue with the detached HEAD commit.
|
||||||
|
|
||||||
### Step 2: Determine commit message convention
|
### Step 2: Determine commit message convention
|
||||||
|
|
||||||
@@ -52,6 +55,8 @@ Keep this lightweight:
|
|||||||
|
|
||||||
### Step 4: Stage and commit
|
### Step 4: Stage and commit
|
||||||
|
|
||||||
|
Run `git branch --show-current`. If it returns `main`, `master`, or the resolved default branch from Step 1, warn the user and ask whether to continue committing here or create a feature branch first. Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). If no question tool is available, present the options and wait for the user's reply before proceeding. If the user chooses to create a branch, derive the name from the change content, create it with `git checkout -b <branch-name>`, then run `git branch --show-current` again and use that result as the current branch name for the rest of the workflow.
|
||||||
|
|
||||||
Stage the relevant files. Prefer staging specific files by name over `git add -A` or `git add .` to avoid accidentally including sensitive files (.env, credentials) or unrelated changes.
|
Stage the relevant files. Prefer staging specific files by name over `git add -A` or `git add .` to avoid accidentally including sensitive files (.env, credentials) or unrelated changes.
|
||||||
|
|
||||||
Write the commit message:
|
Write the commit message:
|
||||||
@@ -61,7 +66,7 @@ Write the commit message:
|
|||||||
Use a heredoc to preserve formatting:
|
Use a heredoc to preserve formatting:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command git commit -m "$(cat <<'EOF'
|
git commit -m "$(cat <<'EOF'
|
||||||
type(scope): subject line here
|
type(scope): subject line here
|
||||||
|
|
||||||
Optional body explaining why this change was made,
|
Optional body explaining why this change was made,
|
||||||
@@ -72,8 +77,4 @@ EOF
|
|||||||
|
|
||||||
### Step 5: Confirm
|
### Step 5: Confirm
|
||||||
|
|
||||||
Run `command git status` after the commit to verify success. Report the commit hash(es) and subject line(s).
|
Run `git status` after the commit to verify success. Report the commit hash(es) and subject line(s).
|
||||||
|
|
||||||
## Important: Use `command git`
|
|
||||||
|
|
||||||
Always invoke git as `command git` in shell commands. This bypasses shell aliases and tools like RTK (Rust Token Killer) that proxy git commands.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user