Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/commands/architect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
4 changes: 2 additions & 2 deletions .claude/commands/finish-prs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<feature>`, 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.

Expand Down Expand Up @@ -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/<feature> main && git merge --squash feat/<feature>`) 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/<feature>` would carry into the upstream PR.
3. Whether upstream drift or a `MINIMUM_VERSION` release-ordering issue (Phase 4) is worth acting on.

---
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/github-review-comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>` |
| "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 |

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/github-review-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ gh pr view <PR_NUMBER> --json mergeable,mergeStateStatus,baseRefName
### Resolution procedure

1. Check out the PR's branch (`gh pr checkout <PR_NUMBER>`) with a clean tree (`git status`). Stash nothing — if the tree is dirty, stop and ask the user.
2. `git fetch origin <base>` then **`git merge origin/<base>`** — 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 <base>` then **`git merge origin/<base>`** — 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/<feature>`, 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.
Expand Down
16 changes: 9 additions & 7 deletions .claude/commands/lfg.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<feature>`. Do not root off `main` to pre-empt that.

---

Expand Down Expand Up @@ -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/<feature>` 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/<feature>` extraction flow in `.claude/rules/upstream-sync.md` lifts the feature's own diff onto `main` when the time comes.

---

Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions .claude/rules/git-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand Down
22 changes: 17 additions & 5 deletions .claude/rules/upstream-sync.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 && <tests> && git push
# per feature branch — merge dash forward, never main:
git checkout feat/loadbalancing && git merge dash && <tests> && git push
git checkout dash && git merge feat/loadbalancing && <tests> && git push
```

Expand Down Expand Up @@ -52,12 +54,22 @@ Tag grammar: gem tags `dash-v<upstream>.<n>`, proxy image tags `v<upstream-base>

## 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/<feature> main
git merge --squash feat/<feature>
git diff dash...feat/<feature> | git apply -3 # the feature's changes only
git status # review before committing
git commit -a -m "<upstream-facing message>"
```

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/<feature>` diffs from `merge-base(dash, feat/<feature>)` 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

Expand Down
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`**
Expand Down Expand Up @@ -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 |
Expand All @@ -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/<feature>` 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

Expand Down