The "Cutting a release" ritual moved the changelog and bumped pubspec but never mentioned tagging — which is how tags silently lapsed from v2.2.0 through v2.8.0. Add step 6: run `make release` (verify + gate + annotated tag), then push with --follow-tags, and state the rule that every released version must have a matching git tag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
name, description
| name | description |
|---|---|
| git-commit | Git commit conventions for this repo — message style, changelog and version-bump rules, attribution trailer, and the safety rules that always apply. Use whenever the user says "commit", "commit this", "create a commit", "make a commit", "amend", or asks Claude to check work into git in any form. |
Git commit rule — this repo
Follow these conventions whenever you create a commit in this repository. These are in addition to the standard Claude Code git-safety protocol (no --no-verify, no force-push to main, prefer new commits over --amend, etc.).
Message style
This repo uses Conventional Commits 1.0 (per D-37).
- First line:
type(scope): imperative subject, ≤ 72 characters including the prefix. Examples:feat(settings): add Appearance font picker (T-460),fix(ipc): reconnect after app reload,docs(readme): drop brittle version line. - Type: one of
feat,fix,docs,style,refactor,perf,test,build,chore. Usefeat/fixfor user-visible behavior;chorefor bookkeeping (chore(plan)is the convention for pql ticket housekeeping). Append!after the scope for a breaking change (feat(ipc)!: …). - Scope (optional but preferred): the subsystem the change lives in —
settings,vim,pty,git,plan, etc. Lower-case, no spaces. - Ticket ref: keep a trailing
(T-NNN)on the subject when the work has a ticket —feat(settings): category rail + navigation (T-447). - Body (optional): wrap at ~72 chars. Explain the why — the reason this change exists. The diff already shows the what; don't restate it in prose.
- No emojis. Anywhere.
- Don't reference the current task or flow (
for the v2.0 milestone,used by the canvas panel) — that context belongs in the PR description and rots as the repo evolves. - Naming: the project is
clide. The Flutter desktop app lives at the repo root; theclideCLI is a thin C client (native/clide-cli/). The supporter project ispql(referenced, not part of this repo). The archived Python implementation lives underlegacy/.
Logically-separated commits
Default to one logical change per commit, even when a lot of work lands in the tree at once. When there's a pile of uncommitted or untracked files:
- Read
git status+git difffirst — never stage the whole tree blind. - Group by concern, not by file location. Typical concerns to separate:
- Bookkeeping —
.gitignore, editor/IDE config, lockfiles,go.sum/pubspec.lockupdates. - Documentation —
README.md,CLAUDE.md, ADRs underdocs/ADRs/, design notes underdocs/. - General-purpose tooling / skills — things that aren't project-specific (reusable skills, shared scripts).
- Project-specific conventions — this repo's own rules.
- Feature or subsystem — one cohesive change per commit; a sidecar change and an app change for the same feature can land together, but two unrelated features should split.
- Layer changes — Flutter app, sidecar CLI, sidecar daemon, IPC server, pql wrapper, canvas driver, git panel — separate concerns; prefer separate commits when the changes are independent.
- Bookkeeping —
- Sequence the commits so each one is cleanly scoped, but don't obsess about whether each intermediate commit "works" — for scaffolding PRs it's fine if the full picture only snaps together at the end.
- Prefer many small focused commits over one large mixed one — a reviewer can read, revert, or cherry-pick a focused commit; they can't do any of those to a blob.
- Use
git add <specific paths>— nevergit add -Aorgit add .when splitting, or you'll sweep in the next commit's work by accident. - Verify between commits with
git statusandgit log -1to confirm the split landed as intended.
Corollary: if a commit's subject line needs the word "and" to be accurate, it probably should have been two commits.
Changelog discipline
This repo follows Keep a Changelog 1.1.0. Every user-visible commit must touch CHANGELOG.md under the ## [Unreleased] section, in the appropriate subsection:
- Added — new features or capabilities.
- Changed — changes to existing functionality.
- Deprecated — soon-to-be-removed features.
- Removed — now-removed features.
- Fixed — bug fixes.
- Security — vulnerability fixes.
Entries should be short imperative phrases that describe user-facing impact — not implementation detail. "Added sidecar PTY support for terminal pane" beats "Added internal/pty/session.go."
Be concise — this is the rule, not a suggestion
CHANGELOG entries must be one or two short sentences. Hard cap: 60 words per bullet, enforced by the pre-push gate — verify before committing with make changelog-gate (run the make target, not the script it wraps). Aim for 30 or under; if you can't say it in one line wrapped at ~75 columns, you're writing the wrong document.
The CHANGELOG is read by humans scanning for what changed between two versions. It is not the place for the rationale, the probe results, the implementation detail, the behavior-change deep dive, or the "see also" cross-references. Those belong in:
- the commit message body — explain why, list evidence, name the trade-offs;
- a D-record / decision document — durable architectural rationale;
- the ticket / PR description — work-context and review notes.
Hard rules:
- No multi-paragraph bullets. One paragraph max. If you reach for a blank line inside a bullet, stop and split or trim.
- No "Behavior change:" / "Side benefit:" / "Note:" sub-headers inside a bullet. Those are essay structure; put them in the commit message.
- No probe numbers, latency stats, or %-coverage deltas in entries. ("hit 95% target" is fine; "0 hangs in 300 spawns vs ~5% before" is commit-body material.)
- No nested function/file lists inside parentheses. If you find yourself writing
(foo, bar, baz, …)for more than 3 items, just say "several X" and trust the diff. - Don't restate the title in the body. A bullet is its own title.
Calibration — match the existing entries in CHANGELOG.md. Open it, look at five recent bullets, write to that length. If your draft is visibly bigger than its neighbors, trim until it isn't.
Good:
- Mouse wheel scrolling in Claude pane — converts scroll events to PgUp/PgDown so TUI apps scroll naturally.
Bad (verbose; commit-body material leaked in):
- PTY spawning switched from
forkpty()toposix_openpt()+posix_spawn()(T-96).forkptycallsfork()underneath, which is unsafe in the multithreaded Dart VM: about 5% of spawns deadlocked in the child beforeexecvebecause libc locks held by ghost-threads remained "locked forever" in the forked child.posix_spawnusesvfork(glibc/musl/macOS), keeping the parent suspended untilexecvecompletes — no Dart code runs in the child. Probed: zero hangs in 300 sequential spawns vs ~5% before. Behavior change: missing executable / missing workingDirectory now surface as aPtyException…
Better:
- PTY spawning uses
posix_openpt+posix_spawninstead offorkpty— closes a ~5% deadlock window in the multithreaded Dart VM (T-96). Missing exe/cwd now throwPtyExceptionat spawn time.
What skips the changelog: pure bookkeeping commits that have no user-visible effect (typo fix in internal comment, .gitignore tweak, lint config change, reformatting). When in doubt, add an entry — the harm of an extra line is zero.
When a commit spans multiple entries (e.g. a feature that adds one thing and fixes another), add a line under each applicable subsection rather than cramming both into one.
Cutting a release
Cutting a release is its own commit. In a single commit:
- Move all entries from
## [Unreleased]under a new heading## [X.Y.Z] — YYYY-MM-DD. - Leave an empty
## [Unreleased]section at the top with its subsection skeleton ready. - Bump
pubspec.yamlversion:toX.Y.Z(drop the-devsuffix for the tag; re-add it on the next development commit if desired). - Run
make gen-build-infosoassets/licenses.yamlself.version:re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3. - Commit subject:
release vX.Y.Z. - Tag it. After the commit lands, run
make release— it verifies the version/changelog/clean-tree invariants, runs the full gate, and creates the annotatedvX.Y.Ztag. Thengit push origin main --follow-tagsto publish the commit and the tag together. Every released version must have a matchinggit tag— a CHANGELOG heading without a tag is an incomplete release (T-393: tagging had silently lapsed from v2.2.0 through v2.8.0). Don't hand-roll the tag;make releasekeeps the tag, gate, and version in lockstep. If you tag by hand, use an annotated tag (git tag -a vX.Y.Z -m "clide vX.Y.Z"); never push a release commit without its tag.
pubspec.yaml is the single source of truth for the version. Every make build/run/test target regenerates lib/src/build_info.g.dart (gitignored) and rewrites assets/licenses.yaml self.version: from it — so the Flutter app sees the current version everywhere without manual sync. Bumping pubspec.yaml and the changelog out of sync is the mistake this rule prevents.
Attribution trailer
Every commit ends with the Claude co-author line:
Co-Authored-By: Claude <noreply@anthropic.com>
The model-identifier variant produced by the Claude Code harness (e.g. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>) is also accepted — don't rewrite it if the harness emits that form.
HEREDOC discipline
Pass commit messages via HEREDOC so multi-line formatting survives:
git commit -m "$(cat <<'EOF'
short imperative summary
Optional longer body explaining why this change was needed,
wrapped at about 72 characters.
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
Never pass multi-line messages via -m "line1\nline2" or multiple -m flags — Git's behavior differs between shells and quoting regimes and the trailer can end up in the wrong place.
What not to commit
.envand any*.env.local— see.gitignore..claude/settings.local.json— user-specific Claude Code settings, ignored.- Build artefacts:
sidecar/bin/,sidecar/dist/,build/(Flutter output),app/.dart_tool/— gitignored. - SQLite index files (
*.sqlite,*.sqlite-wal,*.sqlite-shm,*.db) — caches generated against local repos; must never land here. Gitignored defensively. - Coverage / test output (
*.out,coverage.*,*.test) — gitignored.
Don't hand-manage .pql/changelog
The pre-commit hook exports the pql ticket DB and auto-stages .pql/changelog/ on every commit. Don't git add .pql/changelog yourself and don't write a dedicated "flush the export" commit — just make your normal commit and the hook sweeps the ticket state in. The only thing to remember: a turn that files/changes a ticket but makes zero commits never fires the hook, so the change won't persist (and a later branch switch can drop it). The fix is simply to make a commit — you don't need to touch .pql/changelog.
Safety reminders (reinforced from the global Claude Code protocol)
- Never
--no-verify. If a pre-commit hook fails, fix the underlying issue and create a new commit. - Never
--amenda commit unless the user explicitly asks. A failed-hook commit didn't land, so amending would overwrite the previous commit and lose work. - Never force-push to
mainormaster. Warn the user if they ask. - When staging, prefer naming specific files over
git add -A/git add .— those can sweep in secrets or unintended files. git statusbefore staging,git diff --stagedbefore committing,git log -1after committing to confirm.