--- name: git-workflow description: Git branching strategy, commit standards, and PR workflow for the {{project.name}} project. Use when performing any git operations (commits, branches, pushes, PRs). user-invocable: false --- # Git Workflow Standards ## Identity {{git.identitySection}} ### Bot identity (config) Reference data resolved from config — the Identity section above governs whether a bot identity is actually in effect. - Name (`git.botName`): `{{git.botName}}` - Email (`git.botEmail`): `{{git.botEmail}}` - Signing key (`git.signingKey`): "{{git.signingKey}}" — empty means no dedicated bot signing key. It **selects** a key; it **enables** nothing. Signing is enabled only by a `git.cmd` that pins `commit.gpgsign=true` and references this key. - Per-agent identities (`git.agentIdentities`) — **overrides only.** When a bot identity is in effect, an agent spawned by the `delegate` skill commits under a *derived* identity by default (its `identity.displayName`, and the bot email plus-addressed as `bot+@…`); an entry here replaces that per field. The derivation rule lives in the `delegate` skill's "Per-agent commit identity" section — that is its canonical definition. `{}` (below) means no overrides: ```yaml {{git.agentIdentities}} ``` These ship placeholder defaults. A project that opts into a bot identity overrides the name in the committed `.waffle/waffle.yaml` and the email / signing key in the gitignored `.waffle/waffle.local.yaml` — nested `{{...}}` substitution lets committed values such as `git.cmd` reference the overlay's keys. Never put private key material in `git.signingKey`: it is a key ID or a public-key path, and it renders into committed files. Setting those values alone changes nothing. The opt-in is pointing `git.cmd` at them, so the identity is injected via `-c` flags into the command examples that actually write it — `commit`, and an annotated `tag` if you add one. `checkout`, `push`, `diff` and `log` record no committer, so they stay a bare `git`. In this project `git.cmd` resolves to: ```bash {{git.cmd}} ``` If that is a bare `git`, no bot identity is in effect and agent commits use the machine's own git config. To opt in, set `git.cmd` to `git -c commit.gpgsign=false -c tag.gpgSign=false -c user.name="…" -c user.email=…` referencing the two identity keys with nested `{{...}}` substitution — quoting `user.name` (it may contain spaces) and setting **both** keys explicitly in project config rather than relying on their stack defaults. See the stack's setup note for the exact recipes and the layering rules. ### Attribution trailer (config) Reference data resolved from config — every agent-made commit ends with the attribution trailer (`git.coAuthorTrailer`), which by default credits this repo's **owner** so agent work that reaches the default branch counts toward their GitHub contribution graph: - Owner name (`git.ownerName`): `{{git.ownerName}}` - Owner email (`git.ownerEmail`): `{{git.ownerEmail}}` The owner email must be a **verified email on that GitHub account** for the credit to register (the `ID+user@users.noreply.github.com` form works and stays private); credit accrues only on default-branch commits. A project that would rather have the trailer name the bot points `git.coAuthorTrailer` at `{{git.botName}} <{{git.botEmail}}>` instead. See the stack's setup note. ### Signing model Signing has three tiers. When `git.cmd` above is **not** a bare `git`, the resolved command **is** this project's signing posture — explicit, committed, reviewable. A bare `git` pins no posture: the machine's own config governs, exactly as it does for humans. 1. **Human** — machine git config. The toolkit configures no signing for humans; your own key and account produce GitHub's "Verified" badge exactly as they always did. 2. **Bot and agents** — whatever `git.cmd` pins. The canonical recipe pins `commit.gpgsign=false`: bot commits are *deliberately unsigned* and carry **no badge** (which is cleaner than the yellow "Unverified" a key GitHub cannot check would earn). A project that wants a signing bot pins `commit.gpgsign=true` with `user.signingkey` (and `gpg.format=ssh` for an SSH key) — and must use a **non-prompting** signer, or every non-interactive agent commit hangs. 3. **Per-agent keys** — `git.agentIdentities[].signingKey` appends `-c user.signingkey=` after the base flags, so it *selects* the key when the base recipe signs and is inert when it does not. A per-agent leaf never flips a project-wide "do not sign". Never deviate from the resolved `git.cmd` per-invocation — in **either** direction. Do not add `-c commit.gpgsign=false` because a signing prompt hung (that is a machine/config problem: stop and surface it), and do not sign when the recipe says not to. Changing the posture means changing `git.cmd` in config, with the human's say-so. The asymmetry matters: where `git.cmd` is a bare `git` it pins nothing, so ambient signing is simply the human's own config doing its job — nothing to deviate from. Where the project **has** opted in, ambient signing is the bug this model exists to eliminate. ## Branch Strategy ### Main is protected - **NEVER push directly to `main`.** - All work happens on feature branches. - Changes reach `main` only through pull requests. ### Branch naming - `feat/` — new features - `fix/` — bug fixes - `refactor/` — restructuring without behavior change - `chore/` — tooling, config, docs, CI ### Creating a branch ```bash git checkout main git pull git checkout -b feat/my-feature ``` ## Commits ### Message format ``` : {{git.coAuthorTrailer}} ``` **Types:** `feat`, `fix`, `refactor`, `chore`, `test`, `docs` ### Rules - End every commit message with the attribution trailer shown above — agent-made commits must be traceable. - Pass commit messages via HEREDOC for proper formatting. - Stage specific files by name; avoid `git add -A` / `git add .` (risks committing secrets, generated files, or large artifacts). - Never skip hooks (`--no-verify`) or bypass signing unless explicitly asked. The resolved `git.cmd` **is** the project's explicit signing posture — never deviate from it per-invocation, in either direction, without the human's per-run say-so. - Prefer new commits over amending — amending overwrites history. - **Comments are not spec.** In deterministic files (JS, YAML, shell), keep comments short, rare, and non-normative — the rule lives in the code and its tests; the why lives in `DECISIONS.md`. When you touch a file, shrink its comment mass rather than growing it, and never grow a comment to answer a review finding. Skill and agent markdown is the exception: that prose is the program. ### Example ```bash {{git.cmd}} commit -m "$(cat <<'EOF' feat: add data-export command Three-phase flow: collect eligible records, confirm with the user, then write the export with cancellation support. {{git.coAuthorTrailer}} EOF )" ``` ## Push & Pull Requests ### Pushing ```bash git push -u origin feat/my-feature ``` ### Creating PRs (no human in the loop) After pushing, create the PR immediately using `gh`: ```bash gh pr create --title "feat: short title" --body "$(cat <<'EOF' ## Summary - Bullet points describing changes ## Test plan - [ ] Lint passes (`{{project.lintCmd}}`) - [ ] Types pass (`{{project.typecheckCmd}}`) - [ ] Tests pass (`{{project.testCmd}}`) - [ ] Build passes (`{{project.buildCmd}}`) {{git.coAuthorTrailer}} EOF )" ``` - PR title should be under 70 characters. - Always target `main` as the base branch. - Push and PR without waiting for human approval — the user reviews in GitHub. - Closing keywords apply **per issue reference**: `Closes #1, #2` only closes #1. Write `Closes #1, closes #2` (one keyword per issue) — and after merge, verify the issues actually closed (see [After a PR merges](#after-a-pr-merges)). ## After a PR merges Merging isn't the end of the task. Three things go stale the moment a PR lands, and the **merging agent** owns all three — a CI job can't reach your machine, so local cleanup and board reconciliation are a **local-agent step by design** (the optional post-merge CI hook stays scoped to remote-only branch hygiene; see the stack setup note): 1. **Verify the close.** Closing keywords fire only on merge, and only per issue reference (see the rule above), so confirm each linked issue actually closed — don't assume it did: ```bash gh issue view --json state -q .state # expect CLOSED ``` Re-close by hand any a missing or mis-written keyword left open: `gh issue close `. 2. **Reconcile the board.** If the project has a Projects v2 board, make sure each linked issue's **Status** reflects reality — a merged-and-closed issue should read **Done**, not linger in *In Progress* or *In Review*. Fix drift in place with the `github-project-management` skill, or hand the sweep to the `project-manager` agent. This stays a **local-agent step, not CI**: it needs no API budget, and the agent that just merged already has the context to fix drift — so board verification is deliberately *not* wired into the post-merge CI hook (which does remote branch hygiene only). 3. **Clean up the debris.** Run the `clean-up` skill in its non-interactive post-merge mode to delete the merged local branch and its worktree and prune stale remote-tracking refs: ``` clean-up git --yes ``` `--yes` exists for exactly this — an agent calling it right after it merges a PR (it skips the confirmation prompt; see the `clean-up` skill). The **remote** head branch is removed by GitHub's auto-delete setting or the optional `waffle-post-merge-hook` workflow, but the **local** branch and worktree are always the merging agent's job — nothing remote can delete them. ## Releases A release is a **lightweight tag `vX.Y.Z` on the bump commit** — nothing more. The flow is PR-first, so it never violates *Main is protected*: 1. **Bump via a PR.** On a `chore/bump-X.Y.Z` branch: `npm version X.Y.Z --no-git-tag-version` (updates `package.json` + lockfile, no tag), sync any other documented version locations, stamp `CHANGELOG.md` (`[Unreleased]` → `[X.Y.Z] - `, leave a fresh empty `[Unreleased]`), re-render if generated output tracks the version, run the pre-flight, then open the PR — its body carries the consumer-impact notes. 2. **Tag on merge.** The tag is pushed **after** the bump PR merges, as a lightweight tag on the merge commit — so `vX.Y.Z` always points at the commit that actually carries that version on `main`. Never tag the feature branch pre-merge, and never `git push origin main` directly for a release. Two ways the tag gets pushed: - **Automated (preferred):** label the bump PR with the release label; the `waffle-release-hook` workflow pushes `vX.Y.Z` on merge. The `release` skill does the whole bump-PR half (level selection, bump, CHANGELOG stamp, pre-flight, PR, label) — use it rather than doing these steps by hand. - **Manual fallback** (hook not installed): after the merge, run `git tag vX.Y.Z `, then `git push origin vX.Y.Z` — two separate commands. Do **not** push a tag before the PR merges, and never as a substitute for the PR. ## Parallel Work (Worktrees + Named Agents) When multiple agents work in parallel on files that might conflict: 1. **Use git worktrees** to isolate work: ```bash git worktree add {{git.worktreesDir}}/feat-x feat/feature-x ``` Keeping worktrees under `{{git.worktreesDir}}/` keeps them out of the repo's sibling directories and out of version control. 2. **Lock files** — if two agents suspect they'll edit the same file, one should finish first. Use `TaskList` (it takes no arguments) to see what every other agent has in flight — subject, status, and owner. 3. **Conflict alerting** — if you detect a potential conflict with another agent's work, notify them immediately: `SendMessage(to: "", message: "Conflict: I'm editing ", summary: "conflict warning")`. 4. **Clean up** worktrees after merging: ```bash git worktree remove {{git.worktreesDir}}/feat-x ``` Coordination and isolation are orthogonal — **named agents** provide coordination (task tracking via `TaskCreate`/`TaskUpdate`, messaging via `SendMessage(to: "")`), **worktrees** provide isolation (separate working directories). Use both together for parallel agent work. There is no team to set up: the session has a single implicit team, and the `Agent` tool's `team_name` parameter is deprecated and ignored — a bare `name:` is all an agent needs to be addressable. ## Pre-flight Checklist Before pushing any branch, run all of these in order. Do not push if any fail: 1. `{{project.lintCmd}}` — lint/format checks pass 2. `{{project.typecheckCmd}}` — types pass 3. `{{project.testCmd}}` — all tests pass 4. `{{project.buildCmd}}` — build succeeds 5. `git diff main...HEAD` — review all changes since branching