5.4 KiB
title, category, date, last_refreshed, module, problem_type, component, symptoms, root_cause, resolution_type, severity, tags
| title | category | date | last_refreshed | module | problem_type | component | symptoms | root_cause | resolution_type | severity | tags | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Git workflow skills need explicit state machines for branch, push, and PR state | skill-design | 2026-03-27 | 2026-07-12 | skills/ce-commit and ce-commit-push-pr | architecture_pattern | tooling |
|
missing_workflow_step | workflow_improvement | high |
|
Git workflow skills need explicit state machines for branch, push, and PR state
Problem
ce-commit and ce-commit-push-pr accumulated branch-state and PR-state regressions because they described Git flow in prose instead of as explicit state checks. Git workflows look linear but are stateful along independent dimensions — detached HEAD, untracked files, upstream existence, default branch, existing PR — and every bug had the same shape: the skill observed one dimension once, then assumed it still held after a later transition. Each fix that added a branch to the prose tended to skip a checkpoint an earlier path had protected; the clean-working-tree shortcut was the worst, because it combines all five dimensions at once.
Solution
Treat the skill as a small state machine: at each transition, run the command that answers the next question, then branch on that result instead of carrying state forward in prose. The current commands and their exit-status readings live in skills/ce-commit-push-pr/references/context.md; the transitions they must protect are:
- Cleanliness from
git status, nevergit diff HEAD.git diff HEADis empty for untracked-only work, which is one of the most common commit cases. - Re-read branch state after every branch-changing transition. A
git branch --show-currentcaptured beforegit checkout -bis stale (and empty on detached HEAD); run it again at the moment of decision. The second read converts "the skill thinks it created branch X" into "Git says the current branch is X." - Upstream existence before unpushed-commit checks. An error from
git log @{u}..HEADon a branch with no upstream is not "nothing to push"; that branch needs its first push. - PR detection reads exit status as state (tradeoff below).
- Default-branch safety ahead of every push/PR transition, including the clean-tree-but-unpushed-commits shortcut. In
ce-commit-push-pr, declining feature-branch creation on the default branch is a stop condition, not a continue — otherwise the flow pushes the default branch and then tries to open a PR from it to itself.
The PR-detection tradeoff (§4)
Two commands answer "does this branch already have a PR?", and the choice regressed repeatedly until the tradeoff was written down:
gh pr viewis current-branch-aware, so it cannot pick up another fork's PR that shares a branch name — but it exits 1 on the normal no-PR state, conflating "no PR" with a real failure. That exit is also fatal if the check ever runs at skill load time (the always-loaded project instructions explain why load-time pre-resolution is banned).gh pr list --head <branch> --state openhas clean semantics — exit 0 with[]= no PR; non-zero =ghmissing, unauthenticated, or offline, so PR state is unknown, never "none" — but filters by branch name only, so in a multi-fork repo two owners' PRs can collide.
The skills keep gh pr list for the exit semantics and bound the collision: pass the branch name only (--head silently returns [] for <owner>:<branch>, which reads as "no PR" and opens a duplicate), target the base repo on a fork, skip the check on detached HEAD (an empty --head drops the filter and lists unrelated PRs), match the returned entry by headRepositoryOwner/headRefName rather than index 0, and re-verify immediately before gh pr create. Whatever a future edit picks, the point is that it is a genuine tradeoff, not a settled winner, and it stops regressing only when the caveat travels with the choice.
Prevention
- Only skills that own Git/GitHub mechanics get a state machine. A delegating skill states the condition and lets the owner run the machine — see skill-gates-state-conditions-not-prescribed-git-commands.md.
- Model expected non-zero CLI exits as state transitions, not failures; when a tool highlights non-zero exits, capture the exit code yourself so correct logic does not look broken to the user.
- Any change to one transition triggers a walkthrough of the adjacent states before the change is considered done: 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 an existing PR; default branch with unpushed commits; non-
maindefault branch names such asdeveloportrunk.
Related
- skill-gates-state-conditions-not-prescribed-git-commands.md — the caller-side rule.
- pass-paths-not-content-to-subagents.md