diff --git a/.claude/commands/architect.md b/.claude/commands/architect.md index 58ad1007..1935378f 100644 --- a/.claude/commands/architect.md +++ b/.claude/commands/architect.md @@ -86,7 +86,7 @@ A cross-repo feature (e.g. exposing a new proxy flag) touches **both** layer cak | Shell out directly from `Cli` | Route through `Commands::*` builders | | Release gem before proxy image | Proxy image first — `MINIMUM_VERSION` must be pullable from ghcr.io | | Hardcode proxy version in tests | Interpolate `Kamal::Configuration::Proxy::Run::MINIMUM_VERSION` | -| Commit to `main` | Feature branches root off `main`, merge into `dash` | +| Commit to `main` | Feature branches root off `dash`, merge back into `dash` | | Skip tests | TDD — minitest + mocha first, at every layer touched | | Chase Apple-Silicon builder test failures as regressions | Known host-arch-dependent; pass in CI | diff --git a/.claude/commands/finish-prs.md b/.claude/commands/finish-prs.md index 8c6b80ec..c73b570d 100644 --- a/.claude/commands/finish-prs.md +++ b/.claude/commands/finish-prs.md @@ -19,7 +19,7 @@ This is a fork, and its branch model changes what "sync the PR" means. Read `.cl | **Never rebase a published branch** | Every branch here has a PR, so it is published. Sync with **`git merge origin/dash`**, never `git rebase`. There is therefore **no force-push anywhere in this command** — merge commits push cleanly. | | Merging a PR lands on `dash`, not `main` | `main` doesn't move when a PR merges, so the *upstream* base is stable. What each merge invalidates is the others' relationship to `dash` — that's what the re-sync in Phase 2a absorbs. | | `git rerere` is enabled | Previously-seen conflicts auto-replay their recorded resolutions. Always `git diff --staged` before trusting a replay — a resolution recorded in a different context can be wrong. | -| `feat/*` branches root off `main` to stay upstream-PR-able | Merging `dash` into such a branch pollutes its upstream diff. This is accepted (the squash-merge upstreaming path in `upstream-sync.md` hand-cleans the diff), but **note it in the final report** for any PR whose branch is on the upstreamable list. | +| `feat/*` branches root off `dash` | Merging `dash` forward into them is the normal, expected operation — it costs nothing and needs no note in the report. Upstreaming later extracts the feature's own diff (`git diff dash...feat/`, see `upstream-sync.md`), which already excludes everything `dash` contributed. | **This command does NOT merge PRs itself** unless the user passed `automerge`. Default behavior: make each PR merge-ready, then pause and let the user merge; when a merge lands, re-sync the remaining PRs and continue. @@ -164,7 +164,7 @@ When the queue is drained (all merged, or all merge-ready-and-handed-off, or blo Then: 1. What the user must do next (merge the ready ones, decide on any `needs-user` items). -2. Any PR whose branch is meant to be upstream-PR-able and now carries a `dash` merge commit — the upstreaming path (`git checkout -b pr/ main && git merge --squash feat/`) hand-cleans that, but the user should know. +2. Anything that complicates a later upstream extraction for a branch on the upstreamable list — e.g. unrelated fork-only changes committed onto the feature branch, which `git diff dash...feat/` would carry into the upstream PR. 3. Whether upstream drift or a `MINIMUM_VERSION` release-ordering issue (Phase 4) is worth acting on. --- diff --git a/.claude/commands/github-review-comments.md b/.claude/commands/github-review-comments.md index c10039a6..d81f059c 100644 --- a/.claude/commands/github-review-comments.md +++ b/.claude/commands/github-review-comments.md @@ -118,7 +118,7 @@ For each unresolved comment, read the full body and categorise it: | "Tag this `v2.12.0.1`" | Bare `v*` tags belong to upstream; fork gem tags are `dash-v` | | "Hardcode the proxy version string in the test" | Must interpolate `Kamal::Configuration::Proxy::Run::MINIMUM_VERSION` -- see `.claude/rules/testing.md` | | "Use a `-dash.1` style suffix for the proxy tag" | `Gem::Version` parses `-` as a prerelease marker, sorts BELOW the base, breaks `kamal proxy boot`'s version check | -| "Rebase your branch onto `dash`" | Feature branches root off `main` and merge forward; never rebase a published branch | +| "Rebase your branch onto `dash`" | Feature branches root off `dash` and merge it forward; never rebase a published branch | | "This builder test failure needs fixing" | If it's one of the two known Apple-Silicon arch-dependent failures in `test/commands/builder_test.rb`, it's not a regression -- confirm via `.claude/rules/testing.md` before touching assertions | | "This multi-host fixture doesn't need `loadbalancer: false`" | The fork auto-activates the loadbalancer for any primary role with >1 web host; Docker-in-Docker integration VMs can't resolve each other's hostnames without it disabled | diff --git a/.claude/commands/github-review-pr.md b/.claude/commands/github-review-pr.md index a67caa5e..0108eb04 100644 --- a/.claude/commands/github-review-pr.md +++ b/.claude/commands/github-review-pr.md @@ -77,7 +77,7 @@ gh pr view --json mergeable,mergeStateStatus,baseRefName ### Resolution procedure 1. Check out the PR's branch (`gh pr checkout `) with a clean tree (`git status`). Stash nothing — if the tree is dirty, stop and ask the user. -2. `git fetch origin ` then **`git merge origin/`** — MERGE, never rebase. The branch is shared (it has a PR); a rebase would require a force-push, and `.claude/rules/git-workflow.md` forbids rebasing published branches. The base is normally `dash`; if the PR is a `feat/*` branch that's meant to be upstream-PR-able later, note in the report that merging `dash` in pollutes the upstream diff — the squash-merge upstreaming path in `.claude/rules/upstream-sync.md` hand-cleans that diff anyway. +2. `git fetch origin ` then **`git merge origin/`** — MERGE, never rebase. The branch is shared (it has a PR); a rebase would require a force-push, and `.claude/rules/git-workflow.md` forbids rebasing published branches. The base is normally `dash`, and `feat/*` branches root off `dash`, so merging it forward is routine — it does not compromise a later upstream PR, which extracts the feature's own diff (`git diff dash...feat/`, see `.claude/rules/upstream-sync.md`). 3. `git rerere` is enabled on this repo — previously-seen conflicts auto-replay their recorded resolutions. Review what rerere staged before trusting it (`git diff --staged`); a replayed resolution from a different context can be wrong. 4. Resolve every conflicted file **semantically** — read both sides and produce the version that preserves BOTH changes' intent. Never blanket `--ours`/`--theirs` a source file. The authoritative per-file table is the **Conflict playbook in `.claude/rules/upstream-sync.md`** — apply it with these PR-context readings: - **`lib/kamal/version.rb`**: take the BASE's side (`dash`'s). The fork version is only ever written by `bin/release-dash` at release time on `dash` — a feature branch never bumps it on purpose; a bump on the branch is accidental. diff --git a/.claude/commands/lfg.md b/.claude/commands/lfg.md index c7b79e43..d2fd4b1f 100644 --- a/.claude/commands/lfg.md +++ b/.claude/commands/lfg.md @@ -7,17 +7,19 @@ allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), # LFG - Full Autonomous Workflow -Execute a complete engineering workflow with verification at each phase. This is a fork: `main` mirrors basecamp/kamal and is never committed to directly. All work happens on a feature branch rooted off `main`, merged forward into `dash`. See `CLAUDE.md` and `.claude/rules/upstream-sync.md` for the branch model and release ordering — do not duplicate them here. +Execute a complete engineering workflow with verification at each phase. This is a fork: `main` mirrors basecamp/kamal and is never committed to directly. All work happens on a feature branch rooted off `dash`, merged back into `dash`. See `CLAUDE.md` and `.claude/rules/upstream-sync.md` for the branch model and release ordering — do not duplicate them here. ## Phase 0: Branch Setup -**BEFORE any other work, prepare the git branch — always root off `main`, never off `dash`:** +**BEFORE any other work, prepare the git branch — always root off `dash`, never off `main`:** 1. Check the current branch: `git branch --show-current` -2. Switch to `main` and sync it: `git checkout main && git fetch upstream --tags --prune && git merge --ff-only upstream/main && git push origin main` -3. Create feature branch off `main`: `git checkout -b feat/{brief-description}` (or `issue-{number}-{brief-description}` if working a GitHub issue) +2. Switch to `dash` and sync it: `git checkout dash && git pull origin dash` +3. Create feature branch off `dash`: `git checkout -b feat/{brief-description}` (or `issue-{number}-{brief-description}` if working a GitHub issue) -Rooting off `main` keeps the branch upstream-PR-able later (see "Upstreaming a feature" in `upstream-sync.md`). The PR this workflow opens at the end targets `dash`, not `main`. +Rooting off `dash` means the branch builds on the fork's real codebase — merged features, fork identity, and the `.claude/` toolkit are all present, so slash commands keep working and the branch merges back without replaying fork identity. The PR this workflow opens at the end targets `dash`, not `main`. + +Upstreaming later is still possible from a `dash`-rooted branch — the "Upstreaming a feature" recipe in `upstream-sync.md` extracts the feature's own diff with `git diff dash...feat/`. Do not root off `main` to pre-empt that. --- @@ -286,7 +288,7 @@ The PR body MUST end with a `## Deviations & judgment calls` section copied from write "None — the plan held." This section is read FIRST in review — it is the audit trail for every decision the plan didn't make. -If this feature is meant to be upstreamed later (rejected-by-basecamp features are NOT — see `ROADMAP.md`'s "safe moat" list), leave the branch rooted on `main` alone; the separate squash-merge-to-`pr/` flow in `.claude/rules/upstream-sync.md` handles that. +If this feature is meant to be upstreamed later (rejected-by-basecamp features are NOT — see `ROADMAP.md`'s "safe moat" list), do nothing special here: the branch stays on `dash`, and the separate `pr/` extraction flow in `.claude/rules/upstream-sync.md` lifts the feature's own diff onto `main` when the time comes. --- @@ -307,7 +309,7 @@ The tests prove the CODE is right; this phase keeps the USER's mental model righ - [ ] Unit test suite passes (full `bin/test` if proxy/deploy paths touched) - [ ] Backwards compatibility with existing `deploy.yml` maintained - [ ] No edits to upstream-owned files (`kamal.gemspec`, `bin/release`, `lib/kamal/version.rb`) -- [ ] Branch rooted off `main`, PR opened against `dash` +- [ ] Branch rooted off `dash`, PR opened against `dash` - [ ] PR body ends with `## Deviations & judgment calls` (from implementation-notes.md, since deleted) - [ ] Comprehension close-out delivered (decisions + three merge-gate questions) diff --git a/.claude/commands/security.md b/.claude/commands/security.md index 59f4ef95..1eb39cf3 100644 --- a/.claude/commands/security.md +++ b/.claude/commands/security.md @@ -143,7 +143,7 @@ go vet ./... | Unbounded request/response buffering | Size-capped buffer pool (`proxy_buffer_pool.go`) | | ACME solver answering any domain's challenge | Validate the challenge is for the domain being issued | | Editing `MINIMUM_VERSION` or `ghcr.io/mhenrixon` defaults casually | Follow `.claude/rules/upstream-sync.md` — proxy image ships before the gem references it | -| Committing a fix straight to `main` | `main` is fast-forward-only; branch off `main`, PR into `dash` | +| Committing a fix straight to `main` | `main` is fast-forward-only; branch off `dash`, PR into `dash` | ## Handoff diff --git a/.claude/rules/git-workflow.md b/.claude/rules/git-workflow.md index da1cca48..4a9b6958 100644 --- a/.claude/rules/git-workflow.md +++ b/.claude/rules/git-workflow.md @@ -8,7 +8,9 @@ Fork-specific. See `CLAUDE.md` for architecture/tooling and `.claude/rules/upstr |---|---|---| | `main` | Mirror of `basecamp/kamal` | Fast-forward only — **NEVER** commit here | | `dash` | Integration + release branch | Fork identity + merged features; PRs target this | -| `feat/*` | Feature branches | Root off `main` (stays upstream-PR-able), merge **forward** into `dash` | +| `feat/*` | Feature branches | Root off `dash` (always), merge **forward** into `dash` | + +**ALWAYS** root a feature branch off `dash`, never off `main`. `dash` is where the fork's features, fixes and toolkit live, so a `dash`-rooted branch builds on the real codebase, gets the `.claude/` toolkit for free, and merges back without replaying fork identity. Upstreaming stays possible from a `dash`-rooted branch — see "Upstreaming a feature" in `.claude/rules/upstream-sync.md` — it just extracts the feature's own diff instead of relying on the branch point. **NEVER** rebase a published branch (`main`, `dash`, or a shared `feat/*`) — history is shared, merge forward instead. @@ -43,12 +45,12 @@ Refs #123 ## PR Workflow -1. Branch off `main`, not `dash` — keeps the branch upstream-PR-able +1. Branch off `dash`, never off `main` 2. Make focused, atomic commits 3. Run the pre-commit checklist before every push 4. Open the PR against `dash` (never `main`) 5. Request review -6. Merge `main` → your feature branch → `dash`, in that order, whenever `main` moves — never rebase +6. Merge `dash` → your feature branch whenever `dash` moves, then merge the branch back into `dash` — never rebase ## PR and Issue Bodies @@ -102,6 +104,7 @@ Full procedure, conflict playbook, and sync runbook: `.claude/rules/upstream-syn ## Rules - **NEVER** commit directly to `main` +- **NEVER** root a feature branch off `main` — always off `dash` - **NEVER** force push to `main` or `dash` - **NEVER** edit `kamal.gemspec` or `bin/release` — upstream-owned, kept byte-identical so syncs never conflict - **ALWAYS** run rubocop + unit tests before pushing; run `bin/test` before merging into `dash` diff --git a/.claude/rules/upstream-sync.md b/.claude/rules/upstream-sync.md index bc0c8e84..49bb28ba 100644 --- a/.claude/rules/upstream-sync.md +++ b/.claude/rules/upstream-sync.md @@ -1,6 +1,8 @@ # Upstream Sync Rules -Branch roles: `main` mirrors basecamp/kamal (fast-forward only, never commit). `dash` is the integration + release branch — fork identity plus merged features. Feature branches (`feat/*`) root off `main` and merge `main` forward — never rebase published branches. `git rerere` is enabled, so previously-seen conflicts auto-replay their resolutions. +Branch roles: `main` mirrors basecamp/kamal (fast-forward only, never commit). `dash` is the integration + release branch — fork identity plus merged features. Feature branches (`feat/*`) always root off `dash` and merge `dash` forward — never off `main`, never rebase published branches. `git rerere` is enabled, so previously-seen conflicts auto-replay their resolutions. + +Upstream only ever reaches a feature branch through `dash`: `upstream/main` → `main` → `dash` → `feat/*`. A feature branch never merges `main` directly. ## Routine sync @@ -14,8 +16,8 @@ bundle exec ruby -Itest -e 'Dir["test/**/*_test.rb"].grep_v(/integration/).each bundle exec rubocop --parallel git push origin dash -# per feature branch: -git checkout feat/loadbalancing && git merge main && && git push +# per feature branch — merge dash forward, never main: +git checkout feat/loadbalancing && git merge dash && && git push git checkout dash && git merge feat/loadbalancing && && git push ``` @@ -52,12 +54,22 @@ Tag grammar: gem tags `dash-v.`, proxy image tags `v ## Upstreaming a feature +Feature branches root off `dash`, so they carry fork identity in their history. Do **not** `merge --squash` one onto `main` — that would drag all of `dash` into the PR. Extract the feature's own diff instead: + ```bash git checkout -b pr/ main -git merge --squash feat/ +git diff dash...feat/ | git apply -3 # the feature's changes only +git status # review before committing +git commit -a -m "" ``` -Hand-clean the diff before opening the PR against basecamp/kamal (e.g. drop the unrelated `lib/kamal/cli/build.rb` login change bundled in feat/loadbalancing). +The three-dot `dash...feat/` diffs from `merge-base(dash, feat/)` to the branch tip. The merge-base is a commit on `dash`, so everything `dash` already had — fork identity, the `.claude/` toolkit, other merged features — falls out of the diff, and any `git merge dash` the branch did along the way is excluded too. What is left is exactly the feature. + +`git apply -3` may conflict where the feature touches a file that differs between `main` and `dash` (proxy defaults, `MINIMUM_VERSION`, test fixtures). Resolve toward upstream's shape — the PR must read as if written against `main`. + +Hand-clean the diff before opening the PR against basecamp/kamal (e.g. drop the unrelated `lib/kamal/cli/build.rb` login change bundled in feat/loadbalancing), and confirm no fork-owned file (`dash.gemspec`, `bin/release-dash`, `CLAUDE.md`, `.claude/`) is present. + +Rejected-by-basecamp features (see `ROADMAP.md`'s "safe moat" list) are never upstreamed — skip this entirely for them. ## Never diff --git a/CLAUDE.md b/CLAUDE.md index 43c0d1af..9b0dab2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,11 +20,12 @@ mhenrixon's maintained fork of [kamal](https://github.com/basecamp/kamal) — de 4. **NO `git push --tags`** — it would push fetched upstream tags to the fork; push single tags (`git push origin tag dash-v2.12.0.1`) 5. **NO suffix proxy versions** like `v0.9.2-dash.1` — Gem::Version parses `-` as a prerelease, which sorts OLDER than the base and hard-fails `kamal proxy boot` 6. **NO gem release before the proxy image exists** — the tag named by `Kamal::Configuration::Proxy::Run::MINIMUM_VERSION` must be pullable from ghcr.io first -7. **NO rebasing published branches** — merge `main` forward; history is shared +7. **NO rebasing published branches** — merge forward; history is shared +8. **NO feature branches rooted off `main`** — always root off `dash` ### Always Do -1. **Branch features off `main`** — keeps them upstream-PR-able; merge them into `dash` +1. **Branch features off `dash`** — always; merge `dash` forward into them, then merge them back into `dash` 2. **Mirror `kamal.gemspec` dependency changes into `dash.gemspec`** after every sync (`diff kamal.gemspec dash.gemspec`) 3. **Interpolate `MINIMUM_VERSION` in test expectations** — never hardcode proxy versions 4. **Run unit tests + rubocop before pushing `dash`** @@ -84,7 +85,7 @@ Features (loadbalancing, …) live on their own branches and merge into `dash` | Command | Purpose | |---------|---------| -| `/lfg` | Full autonomous workflow: branch off `main` → understand → plan → TDD → verify → PR into `dash` | +| `/lfg` | Full autonomous workflow: branch off `dash` → understand → plan → TDD → verify → PR into `dash` | | `/plan` | Read-only planning → GitHub issue or `docs/plans/` markdown (execute with `/lfg`) | | `/architect` | Coordinate multi-layer work across the Thor CLI → Commander → Commands → Configuration cake | | `/tdd` | Enforce RED → GREEN → REFACTOR with Minitest + Mocha | @@ -101,13 +102,15 @@ Commands pin a model tier via frontmatter aliases (`sonnet` implementation, `opu ### Toolkit availability on feature branches -This toolkit (CLAUDE.md, `.claude/`) is committed on `dash` only. Feature branches root off `main` (the pristine upstream mirror), so switching to one removes the toolkit from the working tree — slash commands stop loading. To work with the toolkit on a feature branch, materialize it without committing it: +This toolkit (CLAUDE.md, `.claude/`) is committed on `dash`. Because feature branches always root off `dash`, they inherit it — slash commands load with no extra step. + +If you ever land on a `main`-rooted branch (an old branch, or a `pr/` branch created for upstreaming), the toolkit is absent. Materialize it without committing it: ```bash git restore --source=dash --worktree -- .claude CLAUDE.md ``` -The files appear as untracked — **never commit them on a `main`-rooted branch** (they would pollute an upstream-PR-able diff and conflict on merge into `dash`). Stage specific paths, not `git add -A`. Before opening an upstream PR, `git clean` them or leave them untracked. +The files appear as untracked — **never commit them on a `main`-rooted branch**; they would pollute the upstream diff. `git clean` them before opening an upstream PR. ## More Documentation