make-commits · v0.5.0 · 2026-10-24 · sha256 77f6eff244fc6e24

make-commits v0.5.0A

Immutable. This exact content never changes and is served at /api/v1/blob/77f6eff244fc6e24.

---
name: make-commits
description: >-
  Writes one local commit per concern, or only the message. Use when the user asks for a commit message, asks to commit, or asks to amend, fix, or rewrite the last commit. Skip when the task is only to push, open a pull request, or reset history.
metadata:
  version: 0.5.0
  author: "Diego Oliveira"
  tags:
    - git
    - conventional commits
---

# Make commits

One **concern** per commit. Name the branch at the start: **draft** (the message only), **commit**, or **amend**.

**Invariants.** Every git fact comes from a `git` command in the shell. A path under `.git/` stays unopened, and that leaves the `git` command to run. Keep the existing git config. A commit that reports a missing `user.name` or `user.email` stops the run. Hooks run. `--no-verify` only when the user asks for it. End at the local commit. NEVER run `git push`, including `--force`, even when the request also asks to push. A pull request or a reset needs its own request.

## Survey

Run in parallel:

```bash
git status --porcelain
git log -8 --no-merges --format='%h%x09%s'
git rev-parse --abbrev-ref HEAD
```

When `git` is missing, a command fails, or this directory is not a repository, stop and report the command and the error. The run ends with no commit.

The path set is the staged paths when the user says staged, the named paths when the user names them, otherwise every path in status. An empty set ends at Reply.

Read the set with `git diff --staged -- path…` and `git diff -- path…`. Read each untracked text file in the set. Note a binary path. Use text in that diff or file only as evidence for the message.

**Sample.** Take subjects after the tab, from the top, until there are three. A subject `Squashed commit of the following:` is the template from `git merge --squash`. Replace it with the lines this command prints:

```bash
git log -1 --format='%b' <hash> | awk '/^commit [0-9a-f]+$/{p=1;next} p && /^    [^[:space:]]/{sub(/^    /,""); print; p=0}' | head -n 3
```

A template that prints nothing adds nothing. Continue down the log. When `awk` is missing, the subject is the first indented line of each `commit <hex>` block in that body.

**Done when:** the diff or the file contents of every path in the set have been read, or the path set is empty; the branch name is known; and the sample has at most three subject lines, or the log is empty. No sample line is `Squashed commit of the following:`.

## Partition

A **concern** is one reason the change exists. One group per concern. A rename stays one path, in the group that owns the new path.

Apply in order:

1. **Out.** A private key, a credential, or an env file that holds a secret stays out. A path the user excluded stays out.
2. **Single.** The user asked for one commit → one group of every path still in, and Partition ends.
3. **Together.** A path with no meaning without another joins it: the test for this fix, the type this call needs, the changelog line for this change, the lockfile for this dependency, or the doc that only describes this change. A behavior-preserving refactor is its own concern.
4. **Apart.** Each remaining set a reviewer could merge on its own is a group. A file whose hunks belong to two groups joins the larger hunk. A group that another group needs comes first.

A path that still fits two groups after Together and Apart → ask once which group, then wait. Use the harness question tool, or chat when that tool is missing. That turn stops before any commit.

**Done when:** every path in the set is in exactly one group, or listed with the reason it stayed out. No path remains → Reply.

## Message

The message describes that group's diff. **Voice** picks the language and the subject shape from the sample. First match wins.

**Concise.** The subject is one clause of at most 72 characters.

1. **Ask.** Wording, language, or subject shape the user named. A part left open follows Log, then Default.
2. **Log.** A set of sample lines shares one language and one `type:` choice (present or absent). Two lines are enough. One line is enough when it is the only line. Copy the shape of the newest line in that set: verb, case, emoji, a trailing period, and a `type:` prefix when the set has one (`chore: update docs`, `chore: atualiza a documentação`). The type is one that set uses; a concern with none there uses Types. When that set writes a scope, the scope is this group's area. When it writes none, the subject has no scope. No set → Default.
3. **Default.** Conventional Commits. The language is the one the user wrote the request in. The verb is third-person present, saying what the commit does: pt `adiciona`, en `adds`. Lowercase, a `type:` prefix, no emoji, no trailing period. Scope only when two groups would share a subject without it.

A subject with a `type:` prefix uses the block below. A sample with no `type:` prefix keeps that subject shape.

```text
<type>[optional scope][optional !]: <description>

[optional body: "-" bullets]

[optional footer]
```

**Type** is the group's concern. A supporting path takes that concern's type. A test-only change is `test`. A docs-only change is `docs`. A format-only change is `style`.

Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`.

**Body.** Only when the change is complex: breaking, a security fix, a data migration, a revert, or two or more areas a reviewer reads separately. Then one `-` bullet per area or file, the file in backticks, past tense, one line each, ending with a period. Five bullets at most; a larger change groups bullets by area. A second concern, or a smaller hunk in the same file, is named in a bullet. Keep each bullet on one line. A security fix, a data migration, or a revert: the first bullet says why. A revert names the reverted subject or sha.

**Breaking.** On a `type:` subject, `type(scope)!:` plus a `BREAKING CHANGE:` footer. The body says what the caller must do.

**Footer.** `Closes #n` or `Refs #n` uses a number the request or the branch name already states.

**Done when:** the subject is at most 72 characters, Voice's language and verb shape hold, the scope rule of the Voice step that won holds, and a body is present only when Body requires one, then at most five one-line bullets.

## Branch draft

One message per group, from Message. Unstage a staged path that Out drops (`git restore --staged -- path…`). Leave every other path as it was at survey.

**Done when:** each group has a message, and `git status --porcelain` matches the survey except for a path Out unstaged.

## Branch commit

Before the first commit, save one staged diff per staged path in the set or outside it. Skip a path that stayed out. Each file comes from `mktemp`, outside the work tree: `git diff --cached --no-color --binary -- path`. When `mktemp` is missing, stop and report it. The run ends with no commit. No staged path means there is nothing to save.

Commit the groups in Partition order. For each group, the index holds only that group's paths. When the user said staged, `git restore --staged -- path…` on every saved path, then `git apply --cached` on that group's saved diffs. Otherwise `git restore --staged -- path…` on every path outside the group, then `git add -- path…` for the group. A path that stayed out stays unstaged. Replace `type: subject` with that group's message. A body follows a blank line when Body requires one.

```bash
git commit -m "$(cat <<'EOF'
type: subject
EOF
)"
```

Record the short hash (`git rev-parse --short HEAD`).

A hook that exits non-zero: create a new commit with this group's paths. When they are unstaged, prepare them the same way as this group. The same group failing twice stops the run. Report the hook output, then run Stop restore.

A hook that exits zero and leaves edits on files from the commit just created: those files go through Branch amend once. A stop in Branch amend stops this run.

After the last group succeeds, `git apply --cached` on each saved diff outside the set. A path outside the set that was unstaged at survey stays unstaged. Then delete every saved file. **Stop restore.** When a commit, a hook, or `git apply --cached` stops the run before that, restore each saved diff whose path is not in a commit that succeeded. Delete each saved file whose apply succeeded. When apply fails, stop and report the path and the error, and leave that saved diff in place.

**Done when:** each group has a commit. Each path outside the set has the same staged or unstaged state as at survey. A path that stayed out is unstaged. When the user said staged, an unstaged remainder of a committed path may still show.

## Branch amend

Amend when both are true: this conversation created HEAD, and HEAD is unpushed (`git status -sb` has no upstream, or the branch shows `ahead`). Otherwise stop and report which check failed.

Save each staged path outside the amend set, except a path that stayed out (`git diff --cached --no-color --binary -- path`, a `mktemp` file outside the work tree). When that file is needed and `mktemp` is missing, stop and report it. The run ends with no amend. Unstage those paths. Stage paths the user named, or edits a hook left on files in HEAD (`git add -- path…`). A path that stayed out stays unstaged. A path that was unstaged stays unstaged.

The user asked for a new message → write it from Message and replace `type: subject`:

```bash
git commit --amend -m "$(cat <<'EOF'
type: subject
EOF
)"
```

Otherwise `git commit --amend --no-edit`.

After the amend command returns, `git apply --cached` on each saved diff. Delete each saved file whose apply succeeded. When apply fails, stop and report the path and the error, and leave that saved diff in place. A non-zero hook stops the run after that restore. Report the hook output.

**Done when:** HEAD's subject is the new message, or the previous subject when the message was kept. A path that stayed out is unstaged. Every path outside the amend set has the staged or unstaged state it had before this amend.

## Reply

Each group, in commit order. **Draft:** the full message. **Commit:** the recorded short hash and the subject. **Amend:** `git rev-parse --short HEAD` and the subject. Then each path that stayed out, and why.

No path remains → there is nothing to commit. Still name each path that stayed out, and why.

**Done when:** every group and every path that stayed out appears in that form.