Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
6ae60e2
chore: bump develop snapshot target to 4.0.2 (#2770)
bpamiri May 20, 2026
c8efebb
ci(release): fix bump-develop-version.yml duplicate Authorization hea…
bpamiri May 20, 2026
d22f97c
docs(web/blog): Wheels 4.0.1: Adobe CF hardening, Windows Scoop fixes…
bpamiri May 20, 2026
1f4d729
fix(cli): guard application.wo in onError so init failures don't casc…
wheels-bot[bot] May 21, 2026
148446b
fix(ci): stop double-nesting framework inside Linux .deb/.rpm package…
bpamiri May 21, 2026
205ac08
docs(web/guides): correct Linux bleeding-edge install URLs to wheels-…
bpamiri May 21, 2026
93ba712
fix(test): BrowserTest reports unwired this.browser with browserDescr…
wheels-bot[bot] May 22, 2026
a3fb879
chore: rename Phase 2 bucket repos to apt-wheels / yum-wheels for nam…
bpamiri May 22, 2026
1f9cf4c
fix(test): resolve BrowserTest base URL through layered lookup at ins…
wheels-bot[bot] May 22, 2026
6789c13
fix(cli): make `wheels packages install` a real alias for `add` in di…
wheels-bot[bot] May 22, 2026
acd86f9
fix(model): quote column identifiers in SELECT clause builder (#2787)
wheels-bot[bot] May 22, 2026
343ae6f
fix(test): auto-bind include-injected globals into WheelsTest spec sc…
wheels-bot[bot] May 22, 2026
950a7f8
fix(mapper): reject redundant namespace prefix in to= and controller=…
wheels-bot[bot] May 22, 2026
fc7398d
fix(events): re-include app/global/*.cfm on bare ?reload=true when fi…
wheels-bot[bot] May 22, 2026
c3c685d
fix(test): keep test-local.sh from silently dying on missing ~/.lucli…
bpamiri May 22, 2026
9d417b2
ci: allow-list APPROVED + CHANGES_REQUESTED in Reviewer A guard (#2797)
bpamiri May 22, 2026
035bab7
fix(migrator): handle orphan versions in shared dev databases (#2780)…
bpamiri May 22, 2026
eee6b30
feat(migrator): doctor/forget/pretend reconciliation commands (#2780)…
bpamiri May 22, 2026
a96b02f
feat(migrator): enrich wheels_migrator_versions with name + applied_a…
bpamiri May 22, 2026
c7a3516
docs: propagate schema_migrations → wheels_migrator_versions rename t…
bpamiri May 23, 2026
64a70a4
feat(migrator): t.references() columnNames alias + underscore suffix …
bpamiri May 23, 2026
a519cb8
feat(migrator): Migration.cfc command consistency sweep (follow-up to…
bpamiri May 24, 2026
330a195
refactor(cli): add advisory tier to wheels upgrade check scanner (#2805)
bpamiri May 24, 2026
cce4bf7
feat(cli): underscore-references opt-in advisories in wheels upgrade …
bpamiri May 24, 2026
c21efb8
fix(cli): wheels upgrade underscore-flag pre-check scans all of confi…
wheels-bot[bot] May 26, 2026
7044120
fix(model): bypass nested cftransaction when migrator owns the outer …
bpamiri May 26, 2026
39a2bbf
feat(migrator): t.primaryKey() columnName/columnNames aliases (#2803)…
bpamiri May 26, 2026
0a6aa5e
fix(migrator): skip transaction commit after rollback in migrateIndiv…
bpamiri May 26, 2026
39b678e
feat(distribution): land Phase 2 native apt/yum repos via Cloudflare …
bpamiri May 26, 2026
c9e8f34
fix(cli): escape # in underscore-references advisory message (#2815)
bpamiri May 27, 2026
4a6bf7e
docs(changelog): correct drifted PR refs and fill 4.0.2 gaps in [Unre…
bpamiri May 27, 2026
e07358f
fix: green the compat matrix for BoxLang and Adobe CF 2023/2025 (#2817)
bpamiri May 27, 2026
59ff240
ci(release): harden CHANGELOG separator check and correct release pla…
bpamiri May 27, 2026
95a59c9
ci(release): sync develop release.yml + fix dispatch-token perms (#2822)
bpamiri May 27, 2026
86fc183
chore: bump develop snapshot target to 4.0.3 (#2821)
github-actions[bot] May 27, 2026
eda2e90
ci(release): pin softprops target_commitish to the release commit (#2…
bpamiri May 27, 2026
73fd548
docs(changelog): promote develop [Unreleased] to [4.0.2] (#2824)
bpamiri May 27, 2026
fc90371
docs(web/blog): add Wheels 4.0.2 release post (#2825)
bpamiri May 27, 2026
d8e920a
docs(web): consolidate 4.0 docs to one v4.0 line, drop stale snapshot…
bpamiri May 28, 2026
9f71ca6
ci(web-deploy): only commit blog baseline on push, not on PRs (#2828)
bpamiri May 28, 2026
a9978a1
feat(engine): recognize RustCFML + degrade caching when cfcache is ab…
bpamiri Jun 2, 2026
7ccff95
fix(distribution): dearmor apt key in docs + stop stable apt index cl…
bpamiri Jun 3, 2026
70291fa
feat(bot): unblock release pipeline (title-lint, freshen, tiered conf…
bpamiri Jun 3, 2026
2ed5568
fix(model): preserve database column case for auto-derived properties…
bpamiri Jun 4, 2026
9386fa7
fix(test): repair CLI suite failures masked by the BDDRunner error se…
bpamiri Jun 4, 2026
84541de
fix(wheelstest): capture spec-load errors against the bundle and emit…
wheels-bot[bot] Jun 4, 2026
359648c
feat(testing): add browserLoginAsHandler override for /_browser/login…
wheels-bot[bot] Jun 4, 2026
d4ce24e
fix(cli): source reloadPassword from .env in scaffold; reconcile on W…
bpamiri Jun 4, 2026
3d7d669
docs: standardize remaining guides on the bare env() accessor (#2858)
bpamiri Jun 4, 2026
d2e24c4
docs(web/guides): document Scoop install for Windows in release-chann…
bpamiri Jun 4, 2026
085c80b
feat(cli): decouple Lucee admin password into its own WHEELS_LUCEE_AD…
bpamiri Jun 4, 2026
f4ed637
docs(web/guides): document reserved CFML scope names in the controlle…
wheels-bot[bot] Jun 4, 2026
6318bee
fix(cli): handle bare `wheels` invocation by delegating to showHelp()…
wheels-bot[bot] Jun 5, 2026
ab6d80c
fix(cli): normalize Windows drive-letter paths in `wheels new` (#2835)
bpamiri Jun 5, 2026
5661859
fix(cli): re-emit --no-* flags so LuCLI-converted negations reach com…
wheels-bot[bot] Jun 5, 2026
7e988fc
refactor(cli): unify path normalization, add tests + Windows install …
bpamiri Jun 5, 2026
87b5f8d
docs: reconcile bot-pipeline-unblock plan doc with #2847 implementati…
wheels-bot[bot] Jun 5, 2026
d1cdbe3
feat(cli): add ArgSpec typed argument-spec builder for CLI subcommand…
wheels-bot[bot] Jun 5, 2026
772d05b
fix(controller): populate protectedControllerMethods so global helper…
wheels-bot[bot] Jun 5, 2026
d29b809
fix(bot): keep conflict-resolver escalation loop-safe when git commit…
bpamiri Jun 5, 2026
040c6da
fix(bot): thread captured head SHA into wheels-bot review markers (#2…
bpamiri Jun 5, 2026
fbde625
build(deps-dev): bump vitest from 2.1.9 to 4.1.0 in /web (#2836)
dependabot[bot] Jun 6, 2026
eaaef21
fix(test): oracle populate using oracle 19c and lower (#2864)
mikegrogan Jun 6, 2026
db3048f
docs: reconcile #2844 changelog entry and note framework-helper auto-…
bpamiri Jun 6, 2026
a189ae6
ci: skip secret-dependent jobs on Dependabot PRs (#2868)
bpamiri Jun 6, 2026
45e5fb1
refactor(cli): migrate 8 leaf commands to ArgSpec, dropping the argv …
bpamiri Jun 6, 2026
84d88be
fix(migrator): make Oracle DROP TABLE/VIEW work on Oracle <23c (#2869)
bpamiri Jun 6, 2026
28f2de2
fix(cli): resolve CLI services via module-relative path so packaged m…
bpamiri Jun 6, 2026
eda5c90
fix(bot): thread captured head SHA into address-review and advisor ma…
bpamiri Jun 6, 2026
6c7fc6a
ci(bot): enable wheels-bot review on fork PRs via hardened pull_reque…
bpamiri Jun 6, 2026
3c58c62
refactor(cli): migrate console + test to ArgSpec, dropping the argv r…
bpamiri Jun 6, 2026
ffd4ac0
refactor(cli): finish #2861 ArgSpec migration and remove the getArgs …
bpamiri Jun 7, 2026
ed9cf93
fix(migrator): add addForeignKeyOptions to PostgreSQL adapter so FK m…
wheels-bot[bot] Jun 8, 2026
dd92369
fix(cli): write-side commands refuse to attach to sibling app's serve…
wheels-bot[bot] Jun 9, 2026
69d403c
fix(cli): repair broken commands and correct command docs (CLI audit)…
bpamiri Jun 9, 2026
332288f
fix(cli): CLI audit follow-ups (test exit codes, deploy flags, releas…
bpamiri Jun 9, 2026
2d8e507
fix(cli): emit generate enum(), warn on view-gen failures, fix route-…
bpamiri Jun 9, 2026
4e0d5f3
fix(cli): gate reload + generate-admin on project config so they refu…
bpamiri Jun 9, 2026
9e432d8
fix(cli): warn when wheels start's pinned port is already taken (#2885)
bpamiri Jun 9, 2026
da257bc
feat(cli): render per-command help for 'wheels <cmd> --help' (#2886)
bpamiri Jun 9, 2026
6f6a981
fix(cli): thread --hasOne through scaffold and api-resource generatio…
bpamiri Jun 9, 2026
8f3283f
fix(cli): exit non-zero on user-error paths instead of silent success…
bpamiri Jun 9, 2026
5690757
fix(cli): audit-tail polish — info version line, MCP helper leak, hel…
bpamiri Jun 10, 2026
08dd480
docs(changelog): reconcile and promote [Unreleased] to [4.0.3] (#2891)
bpamiri Jun 10, 2026
95f970f
Merge main into release/4.0.3-to-main (strategy: ours)
bpamiri Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
36 changes: 36 additions & 0 deletions .ai/wheels/cross-engine-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Wheels runs on multiple CFML engines (Lucee 5/6/7, Adobe CF 2018-2025, BoxLang) and databases (H2, MySQL, PostgreSQL, SQL Server, CockroachDB). Each engine has runtime differences that can cause code to pass on one engine but fail on another. This guide documents the known gotchas.

**RustCFML (best-effort, experimental):** [RustCFML](https://github.com/RustCFML/RustCFML) — a young, JVM-free CFML interpreter written in Rust — is recognized as a first-class engine in the adapter layer (`server.coldfusion.productName == "RustCFML"` → `RustCFMLAdapter`), but it is NOT yet part of the CI matrix and cannot fully boot the framework today. The confirmed divergence handled in-framework is the **missing `cfcache` built-in** (the cfcache-backed cache degrades to a no-op via the adapter's `supportsCfcache()=false`). Remaining blockers are tracked upstream — chiefly an argument-scope-fidelity gap (undeclared/`argumentCollection`-forwarded named args lose their names) and no Query-of-Queries — so treat RustCFML support as in-progress.

## Engine-Specific Gotchas

### struct.map() Collision (Lucee + Adobe)
Expand Down Expand Up @@ -329,6 +331,40 @@ H2 is the embedded database used by default in tests. Key differences:
- Some MySQL-specific functions (e.g., `GROUP_CONCAT`) not available
- Simpler locking model than production databases

### Auto-Derived Property Casing — `$lowerCaseColumnNames()` Adapter Capability

When a model declares no `property()` mappings, Wheels infers its properties from `cfdbinfo` column metadata. The reported column casing varies by database, so the adapter layer carries a capability flag — `$lowerCaseColumnNames()` on `Base.cfc` — that controls whether the derived property name keeps the reported case or is forced to lowercase. Adapters override this when their database folds unquoted identifiers to a non-meaningful default that would otherwise leak into Wheels-side property names.

| Database | Folding behavior | `$lowerCaseColumnNames()` | Resulting property for column `isHidden` |
|----------|------------------|---------------------------|-------------------------------------------|
| SQL Server, MySQL, SQLite | Preserves declared case | `false` (Base default) | `isHidden` |
| PostgreSQL, CockroachDB | Folds unquoted identifiers to lowercase | `false` (Base default) | `ishidden` (database-reported) |
| Oracle | Folds unquoted identifiers to UPPERCASE | `true` (override) | `ishidden` (lowercased from `ISHIDDEN`) |
| H2 | Folds unquoted identifiers to UPPERCASE | `true` (override) | `ishidden` (lowercased from `ISHIDDEN`) |

```cfm
// vendor/wheels/databaseAdapters/Base.cfc
public boolean function $lowerCaseColumnNames() {
return false; // preserve reported case by default
}

// vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc — override
public boolean function $lowerCaseColumnNames() {
return true; // ISHIDDEN → ishidden (Oracle folds to UPPERCASE)
}

// vendor/wheels/databaseAdapters/H2/H2Model.cfc — override
public boolean function $lowerCaseColumnNames() {
return true; // ISHIDDEN → ishidden (H2 folds to UPPERCASE)
}
```

**When adding a new database adapter**: check whether the database's unquoted-identifier folding rule produces case the Wheels developer actually declared. If it folds to UPPERCASE (Oracle/H2 family), override `$lowerCaseColumnNames()` to return `true`. If it preserves case (SQL Server/MySQL/SQLite) or folds to lowercase (PostgreSQL/CockroachDB), keep the Base default — the reported name is already the right property name.

**Explicit `property(name=..., column=...)` declarations bypass this entirely** — they always win, regardless of the adapter flag. The capability only affects the auto-derived path.

**Reference**: `vendor/wheels/Model.cfc` (auto-derivation site), `vendor/wheels/databaseAdapters/Base.cfc::$lowerCaseColumnNames`, regression spec `vendor/wheels/tests/specs/model/propertyCasePreservationSpec.cfc`, [#2852](https://github.com/wheels-dev/wheels/pull/2852).

### Migration Date Functions

Use `NOW()` for cross-database compatibility in migrations:
Expand Down
2 changes: 1 addition & 1 deletion .ai/wheels/testing/browser-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,5 @@ bash tools/test-local.sh # skips browser specs if JARs missin
- **Data URLs work for most tests** — no server needed for ~95% of DSL coverage. Full HTTP integration (cookies, form submits, redirects) needs a running fixture app; that wiring is the same as Wheels Web app bootstrap (separate server + baseUrl).
- **`this.browserTestSkipped`** — when Playwright JARs aren't installed (fresh CI, clean machine), `beforeAll` sets this flag and `browserDescribe`'s hooks short-circuit. All `it`s should check `if (this.browserTestSkipped) return;` to stay green on CI.
- **CI runs browser tests** — `pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. The base URL is resolved at instance time through a layered lookup (`this.baseUrl` → Wheels setting → JVM property `wheels.browserTest.baseUrl` → env var → CGI auto-detect → `http://localhost:8080`); per-spec `this.baseUrl` takes priority over the env var. Set `this.baseUrl` in the component pseudo-constructor (outside any function), not inside `beforeAll()` — `super.beforeAll()` calls `$resolveBaseUrl()` and caches the result, so a `this.baseUrl =` assignment that runs after `super.beforeAll()` is silently ignored.
- **Fixture routes** — `/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application.
- **Fixture routes** — `/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application. The `/_browser/login-as` handler is configurable: `set(browserLoginAsHandler = "AuthFixture##loginAs")` in `config/settings.cfm` substitutes that `Controller##action` at route-registration time (default is `BrowserTestLogin##create`). Env-gating is handled by `wheels.middleware.BrowserTestFixtureGuard` on the whole `/_browser` scope — custom handlers do not need to re-implement the guard. Empty string or absent setting falls back to the default. (#2830)
- **Dialogs are Lucee-only** — `acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines.
20 changes: 20 additions & 0 deletions .ai/wheels/wheels-bot.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,23 @@ Flip the repo variable `WHEELS_BOT_ENABLED` to `false` to halt every bot workflo
## Auto-fire safety net

The bot is permitted to chain stages (triage → research → propose-fix), and handoff fires on `*-confidence:high` OR `*-confidence:medium`. Low stays manual. Sensitive areas (security, middleware, migrations, deploy, DI, cross-engine) are caught by the propose-fix prompt's own step-4 safety net, which posts a `fix-held` marker instead of opening a PR. Reviewer A and B then critique whatever propose-fix produces, escalating to the Senior Advisor on deadlock. All bot PRs land as `--draft` and require a human approving review on `develop`.

## PR-prep automation (release unblocking)

- **Commit-message gate.** `pr.yml`'s `Validate Commit Messages` lints the
**PR title** (the squash subject), not every commit — because PRs are
squash-merged, intermediate commit headers don't land in `develop`; only the
PR title does. Edit the title to fix a failure; the `edited` trigger re-runs
the check (and `fast-test` is skipped on title-only edits). Local guard:
`tools/test-commit-title.sh`.
- **Freshen (`bot-freshen.yml`).** On push to develop + a 30-min backstop:
behind-but-clean bot PRs are updated via non-destructive `update-branch`;
DIRTY ones are dispatched to the resolver. Decision logic:
`.github/scripts/freshen-decide.sh`.
- **Conflict resolution (`bot-resolve-conflicts.yml` + `/resolve-conflicts`).**
A deterministic classifier (`.github/scripts/classify-conflicts.sh`)
auto-resolves content/docs conflicts (markdown/MDX anywhere, CHANGELOG,
`.ai/`, `docs/`) and pushes; any code conflict is escalated with
the `conflict:needs-human` label and a comment — never auto-resolved.
- **Not automated:** merging. PRs are brought to a green, conflict-free,
ready state; the maintainer performs the final squash-merge.
8 changes: 7 additions & 1 deletion .claude/commands/_shared-rails.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ they are honored. Violating them is a bug — fix the prompt, not the rails.
`ci`, `chore`, `revert`. **Scope is optional and unrestricted** — pick a
short noun that helps a reader skim history (e.g. `model`, `web/blog`),
or omit it entirely. Don't agonize over which scope is "right."
- **Subject ≤ 100 chars, not ALL-CAPS.** Sentence-case is fine.
- **Header ≤ 100 chars, not ALL-CAPS.** commitlint measures the WHOLE header —
`type(scope): subject` including the `type(scope): ` prefix — not just the
subject. A 90-char subject under a `docs(web/guides): ` prefix is a 108-char
header and FAILS. Count the prefix. Sentence-case is fine.
- **The PR title is the linted gate.** Because the repo squash-merges, the PR
title becomes the landing commit subject and is what CI validates — make the
PR title itself a valid conventional-commit header ≤ 100 chars.
- **DCO sign-off required.** Every commit you author MUST end with the
trailer `Signed-off-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com>`
matching the configured git author identity. Use `git commit -s` (the
Expand Down
28 changes: 18 additions & 10 deletions .claude/commands/address-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,22 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
## Args

- `<pr-number>` — the PR with converged-changes markers to address
- `<head-sha>` — the PR head SHA at the start of this run, captured once by
the workflow and passed here. Use it verbatim as the marker SHA — it is the
`<sha>` / `<sha-before>` every marker below writes (the head *before* your
own commit). Don't compute the SHA yourself — re-deriving it mid-session is
the #2848 race. This governs only the marker SHA; you still use `gh pr view`
normally to read comments, the consensus, and the head ref name.

## Steps

1. **Idempotency + outer-loop cap.** Read PR comments via
`gh pr view <pr-number> --json comments,headRefOid,headRefName`.
1. **Idempotency + outer-loop cap.** Throughout this command, the marker SHA
— written `<sha>` and `<sha-before>` below — is the `<head-sha>` argument
you were passed; don't compute it yourself (issue #2848). Read PR comments
via `gh pr view <pr-number> --json comments`.
- If any comment contains
`wheels-bot:address-review:<pr>:<sha>:` for the current head
SHA, exit silently — already addressed at this SHA.
`wheels-bot:address-review:<pr>:<head-sha>:` for the `<head-sha>`
you were passed, exit silently — already addressed at this SHA.
- Count comments matching `wheels-bot:address-review:<pr>:` for
ANY SHA on this PR. If count ≥ 5, post:

Expand All @@ -44,7 +52,7 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
either the PR's scope is larger than the bot can resolve, or
the reviewers are deadlocked on a design call.

<!-- wheels-bot:address-review:<pr>:<sha>:terminal -->
<!-- wheels-bot:address-review:<pr>:<head-sha>:terminal -->
```

and exit.
Expand Down Expand Up @@ -90,7 +98,7 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
change. The PR's reviewer-feedback exchange is preserved above
for context.

<!-- wheels-bot:address-held:<pr>:<sha> -->
<!-- wheels-bot:address-held:<pr>:<head-sha> -->
```

and exit.
Expand Down Expand Up @@ -145,17 +153,17 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
SHA. Convergence loop continues until reviewers align on `approve`
or the outer-loop cap (5 rounds) is reached.

<!-- wheels-bot:address-review:<pr>:<sha-before>:<N> -->
<!-- wheels-bot:address-review:<pr>:<head-sha>:<N> -->
```

8. **Self-check before posting.**
- [ ] Branch-aware scope check passed — no files modified outside
allowed paths
- [ ] For `fix/bot-*`: tests re-run, output cited in the comment
- [ ] Commit message is conventional, subject ≤ 100 chars
- [ ] PR comment includes the marker with the correct
`<sha-before>` (the head SHA at the start of this run, not after
your commit)
- [ ] PR comment includes the marker built from the `<head-sha>`
argument (the head SHA at the start of this run, before your
commit — never a value you re-derived; issue #2848)
- [ ] Outer-loop count is correctly reflected in the round number

If any check fails, do not post; investigate and exit non-zero.
40 changes: 26 additions & 14 deletions .claude/commands/advise-on-deadlock.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,37 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
## Args

- `<pr-number>` — the PR with deadlocked A↔B exchange
- `<head-sha>` — the commit SHA this advice runs against; the workflow
resolves it once and checks it out, then passes it here. Use it verbatim as
the marker SHA wherever this prompt writes `<sha>`. Don't compute the SHA
yourself — re-deriving it mid-session is the #2848 race. This governs only
the marker SHA: you still use `gh pr view` / `gh pr diff` normally to read
the PR's comments, reviews, and diff.

## Steps

1. **Idempotency check.** Read PR comments via
`gh pr view <pr-number> --json comments,headRefOid`. If any
comment contains `wheels-bot:advisor:<pr>:<sha>` for the current
head SHA, exit silently — already advised at this SHA.
1. **Idempotency check.** Throughout this command, `<sha>` means the
`<head-sha>` argument you were passed; don't compute it yourself
(issue #2848). Read PR comments via
`gh pr view <pr-number> --json comments`. If any comment contains
`wheels-bot:advisor:<pr>:<head-sha>` for the `<head-sha>` you were
passed, exit silently — already advised at this SHA.

2. **Confirm the deadlock.** Look for a comment containing
`wheels-bot:review-b:<pr>:<sha>:terminal` for the current head
SHA. That's the trigger marker. If no terminal marker is present
for the current SHA, exit silently (this command shouldn't have
`wheels-bot:review-b:<pr>:<head-sha>:terminal` for the `<head-sha>`
you were passed. That's the trigger marker. If no terminal marker is
present for `<head-sha>`, exit silently (this command shouldn't have
fired).

3. **Read the full exchange.**
- The PR diff via `gh pr diff <pr-number>`.
- The PR title/body via `gh pr view <pr-number>` for original
context (and the `Fixes #<issue>` link, if any — the original
issue's framing matters).
- All `wheels-bot[bot]` PR reviews on the current SHA: A's initial
- All `wheels-bot[bot]` PR reviews on `<head-sha>`: A's initial
review and any response reviews
(`wheels-bot:review-a-response:`).
- All `wheels-bot[bot]` PR comments on the current SHA matching
- All `wheels-bot[bot]` PR comments on `<head-sha>` matching
`wheels-bot:review-b:<pr>:<sha>:` — the full B critique chain in
chronological order.

Expand Down Expand Up @@ -137,14 +145,16 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
<if verdict is `approve`, note that the disputed findings should be
dropped and the PR is fine to merge as-is.>

<!-- wheels-bot:advisor:<pr>:<sha> -->
<!-- wheels-bot:advisor:<pr>:<head-sha> -->
<CONVERGENCE_MARKER>
```

Where `<CONVERGENCE_MARKER>` is:
- `<!-- wheels-bot:converged-approve:<pr>:<sha> -->` if verdict is
Build every marker SHA from the `<head-sha>` argument — never a value
re-derived during the session (issue #2848). Where `<CONVERGENCE_MARKER>`
is:
- `<!-- wheels-bot:converged-approve:<pr>:<head-sha> -->` if verdict is
`approve`
- `<!-- wheels-bot:converged-changes:<pr>:<sha> -->` if verdict is
- `<!-- wheels-bot:converged-changes:<pr>:<head-sha> -->` if verdict is
`changes` (triggers `bot-address-review.yml`)

10. **Self-check before posting.**
Expand All @@ -155,7 +165,9 @@ Read `.claude/commands/_shared-rails.md` first. Highlights:
- [ ] Verdict is one of `approve` or `changes` — not "kinda
mostly", not equivocal.
- [ ] Convergence marker is consistent with the verdict.
- [ ] Advisor marker present.
- [ ] Advisor and convergence markers present and built from the
`<head-sha>` argument — not a SHA re-derived during the session
(issue #2848).

If any check fails, fix before posting. The advisor's verdict is
authoritative within the convergence loop — get it right.
61 changes: 61 additions & 0 deletions .claude/commands/resolve-conflicts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# /resolve-conflicts

Reconcile content/docs merge-conflict markers on a bot PR branch (low-risk paths only). Invoked by bot-resolve-conflicts.yml after a deterministic risk gate.

## Rails

Read `.claude/commands/_shared-rails.md` first — they apply to every step
below. Highlights for this command:

- Use `gh` for GitHub state, `git` for the PR branch only.
- **Filesystem writes are limited to the conflicted content/docs files only.**
Never touch code.
- Output is **a completed merge commit** — the workflow pushes after this
prompt completes.

## Args

- `<pr-number>` — the PR branch with content/docs conflict markers to resolve

# Resolve content conflicts — PR #<pr-number>

You are running inside `bot-resolve-conflicts.yml`. The workflow has already
merged `origin/develop` into the PR branch and a **deterministic classifier
has confirmed every conflicted file is pure documentation/content**
(markdown/MDX at any path, CHANGELOG, or under `.ai/` or `docs/`).

## Hard safety rule

Run this first:

```bash
git diff --name-only --diff-filter=U
```

Confirm EVERY listed file is in the low-risk set the upstream classifier
admits — i.e. each file is a `*.md` or `*.mdx` (any path), a `CHANGELOG`
file, or under `.ai/` or `docs/`. If ANY listed file falls OUTSIDE that set
(any code file — `.cfc`, `.cfm`, `.js`, `.ts`, `.py`, `.sh`, `.json`, `.yml`,
`.yaml` — or any other non-doc file), DO NOT resolve it. Run
`git merge --abort`, post a comment saying the gate and the command disagreed
(a bug), and stop. This should never happen, but never resolve a code conflict.

## Resolve

For each conflicted content file:
1. Open it and read the full conflict region(s).
2. Reconcile the `<<<<<<<` / `=======` / `>>>>>>>` markers by **integrating
both sides' intent** — these are docs, so prose from both branches almost
always belongs in the result; merge them coherently rather than picking one
side and discarding the other. Remove all conflict markers.
3. `git add <file>`.

After all files are resolved:

```bash
git diff --name-only --diff-filter=U # must print nothing
git commit --no-edit # completes the merge commit
```

Do NOT `git push` — the workflow pushes after verifying no markers remain.
Do NOT edit any file that was not in the conflicted set. Do NOT touch code.
Loading