gwm-cli 1.5.0

git worktree manager β€” TUI + CLI, native libgit2, per-repo bootstrap
Documentation
# gwm-cli β€” house rules for AI assistants

This file is the project-level CLAUDE.md. Anything stated here OVERRIDES
defaults and applies to every contribution made via an AI assistant in
this repository.

## πŸ”΄ Primordial rule β€” Test-Driven Development is mandatory

**No production code lands without a failing test that pinned the
behaviour down first.** This is not a guideline, it is a hard merge
requirement. PRs that add or change behaviour without tests are sent
back, full stop.

### The TDD loop (red β†’ green β†’ refactor)

1. **Red** β€” write a failing test that captures the new behaviour or
   the bug you are fixing. Run it. It MUST fail for the right reason
   (assertion mismatch, not a compile error in unrelated code). Commit
   the test alone if it helps reviewers see the contract.
2. **Green** β€” write the minimum production code to make the test pass.
   No extra branches, no speculative abstractions.
3. **Refactor** β€” clean up while the tests are green. Re-run the full
   suite after every refactor step.

### What counts as "behaviour"

Anything observable from outside the function under test:

- A new CLI subcommand, flag, or output format β†’ end-to-end test in
  `tests/cli_binary.rs` via `assert_cmd`.
- A new public function in `src/<module>.rs` β†’ unit test in
  `tests/<module>_tests.rs`.
- A new bootstrap step (file copy, guard, no-symlink, command hook) β†’
  integration test in `tests/bootstrap_tests.rs` exercising it against
  a `tempfile::TempDir`.
- A libgit2 worktree operation β†’ integration test in
  `tests/worktree_integration.rs` using `tests/common::init_repo()`.
- A TUI state transition β†’ state-machine test in
  `tests/tui_app_tests.rs` (ratatui-free).

### Exceptions (narrow, must be argued in the PR description)

The bar to skip a test is "the change is observably untestable from
the public surface". Concretely:

- **Pure formatting / typo fixes** in user-facing strings β†’ no test
  required if the string is incidental (a log line, a help blurb). If
  the string is asserted somewhere, update the assertion.
- **Dependency bumps** without behaviour change β†’ CI green is the test.
- **Comments-only changes** β†’ no test required.

Everything else needs a test. "I tested it manually" is not an
exception; codify the manual test as an integration test.

### Enforcement

- PR template ships with a `cargo test` checkbox under **Tests**. Do
  not tick it unless the suite actually ran green locally.
- Reviewers will run `git log --stat <branch>..HEAD -- tests/` and
  block the PR if the touched module has no companion test diff.
- `tests/cli_binary.rs::help_prints_subcommands` should be updated
  every time a new subcommand is added β€” treat this as the canary.

## Other house rules

- **`main` is protected: there is no direct push, for anyone.** PRs are
  required, seven status checks must be green, and `enforce_admins` is
  on, so `git push origin main` is rejected for the maintainer too and
  there is no override. `dev` reaches `main` through a PR, hotfixes
  included; the tag is pushed after the merge (tags are not covered by
  the protection). Do not plan a release around a local `dev` β†’ `main`
  merge, which is how v1.0.2 and v1.1.1 were cut: it will now be
  rejected. Rules and rationale in
  [CONTRIBUTING.md Β§Branch protection]CONTRIBUTING.md#branch-protection.
- **Reconcile open PRs before any tag.** Before cutting an RC or a
  stable, run `gh pr list --state open` and account for every open
  PR: either it's in the changeset, intentionally deferred, or
  closed as stale. The v0.3.0 stable shipped without three queued
  feature PRs (#51, #52, #53) because this check was skipped β€”
  forced an immediate v0.4.0 promotion 38 minutes later. Two
  minutes upfront beats a rushed follow-up release.
- **Release notes are per-version, never the index.** The release
  workflows (`release.yml` / `pre-release.yml`) source their
  `body_path` from `changelogs/<version>.md` (stable) or
  `changelogs/pre-releases/<version>.md` (rc/alpha/beta), NOT from
  the top-level `CHANGELOG.md` (which is the in-progress index β€”
  entries get moved into the per-version file when the release is
  cut, so the index is empty at tag time). Before tagging, verify
  the per-version file exists and contains the release contents;
  the workflow now hard-fails if the file is missing rather than
  silently publishing the empty index (witnessed on v0.6.0 /
  v0.6.0-rc.1 β€” both releases had to be re-edited post-hoc via
  `gh release edit --notes-file`).
- **Do not stack deep PR chains.** After the v0.7.0 hardening run,
  the cost of rebasing stacked PRs was higher than the cost of waiting
  for review. For decompositions touching related surfaces, keep at
  most 2-3 PRs open, merge each PR as soon as Copilot + CI are green,
  wait for `dev` to settle, then branch the next one.
- **Batch low-risk encapsulation nits after a stack merges.** If a
  Copilot review asks for private fields, accessors, or re-export
  cleanup while several dependent PRs are queued, prefer filing or
  applying a small polish PR after the stack lands. Do not force a
  cascade of mechanical rebases for non-behavioural cleanup.
- **Parallel agents only when file ownership is disjoint.** Sub-agents
  work well for independent surfaces. If multiple tasks all touch
  shared files such as `src/tui/app.rs`, `src/tui/state/*`, or config
  plumbing, dispatch them sequentially instead of creating avoidable
  merge conflicts.
- **Follow-up issues beat scope creep.** If review uncovers a design
  bug whose fix changes the shape of the implementation, file a
  focused follow-up issue rather than hiding it in the current PR.
  Keep the original PR atomic unless the bug invalidates its contract.
- **Verify MSRV against the whole codebase, not the feature you just
  added.** Before declaring or changing MSRV, run `cargo clippy
  --all-targets -- -W clippy::incompatible_msrv` locally; prefer
  `cargo msrv verify` when available. The v0.7.0 cycle caught an
  existing `std::iter::repeat_n` usage after a separate `LazyLock`
  MSRV discussion, so grep for newer APIs before assuming the latest
  edit is the limiting factor.
- **Keep root `CHANGELOG.md` as in-progress only.** PRs may add entries
  under `[Unreleased]`, but must not reintroduce bullets already moved
  into the latest `changelogs/pre-releases/<previous-rc>.md`. The guard
  from #147 now ships as `.github/scripts/check-rc-changelog-dupes.sh`
  and runs in CI on every pre-release tag (`pre-release.yml`). Run it
  locally before cutting an RC β€” `./.github/scripts/check-rc-changelog-dupes.sh <tag>`
  (e.g. `v0.8.0-rc.4`) β€” so a duplicated bullet is caught before the tag,
  not by a red CI job after it.
- **Release workflow edits must prove publishing credentials.** The
  v0.7.0 stable tag built all five release artifacts, then the GitHub
  Release publish step failed with `Bad credentials` and required
  manual recovery. Any PR touching `release.yml`, token permissions, or
  release actions should explain how the publish path was validated and
  should keep #146 in view.
- **Pre-validate environment-dependent tests.** Any test that reads
  `$PATH`, the user's home directory, or other ambient state must be
  pre-validated locally against a stripped environment before the test
  gets pushed β€” CI runners don't have `lazygit`, a pre-created
  `~/cc-worktree/`, or your installed dev tooling. The one-liner that
  reproduces a CI-like minimal PATH:

  ```bash
  PATH="$(dirname "$(command -v cargo)"):/usr/bin:/bin" cargo test
  ```

  Run it before push. The cost is one minute; the cost of skipping
  it is at least two CI round-trips (witnessed on PR #43 β€” three
  fix commits before the suite went green). If a test can't be made
  env-independent, assert intent (sigils, names) instead of exit
  codes; cover the deterministic 0/1/2 contract in a separate hand-
  built unit test.
- **Run `gwm doctor` locally** before opening a PR that touches
  `.gwm.toml`, the bootstrap schema, or the doctor module itself. The
  same check runs in CI as an advisory job β€” green there means you'll
  not surprise a reviewer.
- **Indentation**: 2 spaces. `cargo fmt` is run on every commit; CI
  enforces `cargo fmt --check`.
- **Linter**: `cargo clippy --all-targets -- -D warnings` must pass.
  Do not `#[allow(...)]` warnings without a comment explaining why.
- **No `unwrap()` on user-facing paths**: return a `GwmError` variant
  instead. `unwrap()` is acceptable inside tests and in genuinely
  infallible spots (e.g. `.lock()` on a never-poisoned mutex), but it
  must be a deliberate choice, not a shortcut.
- **No `println!` in TUI render code**: the status bar is the only
  channel for runtime feedback inside the TUI.
- **Branch convention**: `<type>/#<issue>-<description>`. Use
  `gwm create <type> <issue> <description>` β€” it bootstraps the
  worktree and creates the branch in one go.
- **Commit format**: Gitmoji + Conventional Commits. See
  [CONTRIBUTING.md]CONTRIBUTING.md#commits.
- **Merge strategy**: regular merge commit, never squash, never delete
  the source branch. The atomic commit history is the artefact.

## Where to look for the rest

- Branch / commit / PR conventions β†’ [CONTRIBUTING.md]CONTRIBUTING.md
- Community standards β†’ [CODE_OF_CONDUCT.md]CODE_OF_CONDUCT.md
- Public roadmap β†’ [ROADMAP.md]ROADMAP.md
- Release process β†’ [CONTRIBUTING.md Β§Releases]CONTRIBUTING.md#releases