Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
32eafaf
chore: bump develop snapshot target to 4.0.1
bpamiri May 12, 2026
43e4493
Merge pull request #2608 from wheels-dev/auto-bump/develop-4.0.1
bpamiri May 12, 2026
dcf73d3
fix(cli): generated-app templates stop advertising broken `wheels pac…
github-actions[bot] May 12, 2026
1b0dcb3
docs(web/guides): replace broken `wheels packages install` verb with …
github-actions[bot] May 12, 2026
4a62207
chore(web/guides): rename v4-0-0-snapshot dir to v4-0-0 for GA
bpamiri May 12, 2026
81316ba
chore(web/guides): rewrite internal /v4-0-0-snapshot/ links in rename…
bpamiri May 12, 2026
c1bfe90
chore(web): update versions.ts + astro redirects for v4.0.0 GA
bpamiri May 12, 2026
dcaa5a6
chore(web/api): generate v4.0.0 API content from docs/api/v4.0.0.json
bpamiri May 12, 2026
74f91d7
chore(web/guides): seed v4-0-1-snapshot/ as clone-and-diverge baselin…
bpamiri May 12, 2026
7ca37c6
chore(web): update outside-doc references to v4-0-0-snapshot for GA
bpamiri May 12, 2026
dec472f
docs(web/guides): address Reviewer A/B consensus findings (round 1)
github-actions[bot] May 12, 2026
cd8ce3f
chore(web): refresh guides + api visual-regression baselines for the …
bpamiri May 12, 2026
387bc45
Merge pull request #2611 from wheels-dev/fix/bot-2610-wheels-packages…
bpamiri May 12, 2026
477c2db
Merge develop into chore/docs-rename-v4-0-snapshot-to-stable
bpamiri May 12, 2026
6bac9e9
Merge pull request #2612 from wheels-dev/chore/docs-rename-v4-0-snaps…
bpamiri May 12, 2026
2b908f6
docs(web/blog): Wheels 4.0: Closing the Maturity Gap
bpamiri May 12, 2026
5a6f1f6
ci: let medium-confidence triages auto-fire propose-fix and write-doc…
bpamiri May 12, 2026
759a31b
fix(security): use Adobe-safe 3-arg mid() in MCP command parser (#2617)
bpamiri May 12, 2026
7cdb036
fix(test): route browser-test artifact dir creation through File.mkdi…
bpamiri May 12, 2026
b60735b
ci(release): fire bump-develop via repository_dispatch (fix #2609) (#…
bpamiri May 12, 2026
4e695b2
fix(web/blog): quote title in wheels-4-closing-the-maturity-gap (#2620)
bpamiri May 12, 2026
12e82e5
fix(cli): assign new wheels.Injector directly to application.wheelsdi…
wheels-bot[bot] May 12, 2026
1e7dfa4
fix(cli): honor --no-sqlite when scaffolding lucee.json (#2624)
wheels-bot[bot] May 13, 2026
c3fef61
fix(cli): point scaffolded routes.cfm comment at canonical guides.whe…
wheels-bot[bot] May 13, 2026
7e68b87
fix(config): interpolate plugin and package names in load-trace log m…
wheels-bot[bot] May 13, 2026
a97a8b4
docs(releases): reconcile breaking-change count in 4.0 upgrade docs (…
wheels-bot[bot] May 13, 2026
02f8911
docs(web/guides): clarify legacy adapter does not shim application.wi…
wheels-bot[bot] May 13, 2026
de32ce3
docs(web/guides): document reloadPassword must be set via env() in co…
wheels-bot[bot] May 13, 2026
91fc6dc
fix(cli): scan seven more 3.x → 4.x breakers in wheels upgrade check …
wheels-bot[bot] May 13, 2026
27aba54
fix(cli): deprecate legacy `box wheels upgrade` and point users at th…
wheels-bot[bot] May 13, 2026
8b540ce
docs(web/guides): document allowCorsRequests global setting still wor…
wheels-bot[bot] May 13, 2026
44db72e
fix(cli): align wheels upgrade help with the scanner-only reality (#2…
wheels-bot[bot] May 13, 2026
bac76bd
fix(cli): provision project rewrite.config on wheels start for 3.x up…
wheels-bot[bot] May 13, 2026
a116f60
fix(dispatch): preserve receiver context in BoxLang invokeMethod (#26…
bpamiri May 13, 2026
888abc2
docs(web/blog): Upgrading from Wheels 3.x
bpamiri May 13, 2026
660281f
docs(web/blog): revise Upgrading from Wheels 3.x with Titan cutover l…
bpamiri May 13, 2026
dab7412
fix(view): resolve guides sidebar dynamically so the in-app view load…
wheels-bot[bot] May 13, 2026
d9e7ce9
ci(web): add manual workflow to refresh visual regression baselines (…
bpamiri May 13, 2026
3194244
feat(ci): draft apt/yum bucket repo templates + wire release dispatch…
bpamiri May 13, 2026
163d48d
fix(view): inline tab init on core test runner so full-suite tabs sta…
wheels-bot[bot] May 13, 2026
a74a07d
ci(compat-matrix): make CF engine readiness diagnostics actionable on…
bpamiri May 14, 2026
5a97715
fix(dispatch): override getStatusCode on BoxLang adapter to reach Htt…
wheels-bot[bot] May 14, 2026
8822462
fix(test): replace toBeInstanceOf("component") with portable toBeWhee…
wheels-bot[bot] May 14, 2026
2910ec7
docs(web/blog): From WireBox to wheelsdi — The Framework Gets Leaner
bpamiri May 14, 2026
601ca06
chore(web): refresh blog visual baseline after wheelsdi post (#2678)
bpamiri May 14, 2026
089c39b
fix(model): exempt binary columns from scalar-column type guard in $s…
wheels-bot[bot] May 14, 2026
283a239
fix(migrator): make addColumnOptions spec adapter-aware and fix PG em…
wheels-bot[bot] May 14, 2026
dc08f8f
test(model): guard lockingSpec advisory-lock tests on adapters withou…
wheels-bot[bot] May 14, 2026
6d42559
Update from-wirebox-to-wheelsdi.md (#2679)
bpamiri May 14, 2026
855e9b7
fix(test): render txt test report inline so format=txt no longer 404s…
wheels-bot[bot] May 14, 2026
447bcb1
fix(model): narrow binary-column carve-out to array-shape only (#2682)
bpamiri May 14, 2026
58b0f9c
ci: unblock Oracle in compat matrix and fix Lucee 6 datasource (#2683)
bpamiri May 14, 2026
d703c09
fix(cli): anchor wheels deploy init template lookup to CFC location (…
bpamiri May 15, 2026
baac9d1
build(deps): bump devalue from 5.7.1 to 5.8.1 in /web (#2680)
dependabot[bot] May 15, 2026
f681426
test(migrator): drop migrator system tables between specs (fixes #266…
bpamiri May 15, 2026
8aa92f6
feat(cli): scaffold Dockerfile and .dockerignore in wheels deploy ini…
bpamiri May 15, 2026
32c6db0
fix(cli): propagate ssh: block from deploy.yml to SshPool (#2688)
bpamiri May 15, 2026
3633cc9
fix(cli): return "unknown" from $gitShortSha() when git exits non-zer…
bpamiri May 15, 2026
b720ad5
fix(cli): add `wheels deploy bootstrap`/`exec` flat aliases (#2677) (…
bpamiri May 15, 2026
41a846f
fix(cli): add --release alias for deploy --version to dodge picocli (…
bpamiri May 15, 2026
3c13e31
fix(ci): unbreak Oracle compat-matrix — Lucee 6 dbdriver + cascade dr…
bpamiri May 15, 2026
46de718
feat(web/blog): rate-limited API post + RateLimiter init validation (…
bpamiri May 15, 2026
28fe721
fix(middleware): validate RateLimiter windowSeconds and maxRequests a…
wheels-bot[bot] May 15, 2026
8c35d8d
fix(cli): add wheels deploy fetch-secrets/extract-secrets/print-secre…
bpamiri May 15, 2026
cf0d169
fix(cli): ship lucli-native module, version stamps, and tar dep in Li…
wheels-bot[bot] May 15, 2026
756e0af
fix(cli): deploy verbs must surface remote nonzero exit codes (#2696)…
bpamiri May 15, 2026
725c6da
docs(web/guides): document env() pattern for config/environment.cfm l…
wheels-bot[bot] May 15, 2026
dd2c8ec
docs(web/guides): document CORS allow-list defaults drift on 3.x→4.x …
wheels-bot[bot] May 15, 2026
f135c5c
docs(web/guides): add Reading the Changelog guide for vendored-framew…
wheels-bot[bot] May 15, 2026
8b1a448
fix(cli): stop wheels --help advertising `packages install` as a real…
wheels-bot[bot] May 15, 2026
a575c52
fix(middleware): emit Vary: Origin from Cors when reflecting allowed …
wheels-bot[bot] May 16, 2026
f9a4312
fix(middleware): Cors no longer emits raw comma list as Access-Contro…
wheels-bot[bot] May 16, 2026
ce47c80
fix(view): paginationNav rejects unknown sub-helper arguments (#2726)
wheels-bot[bot] May 16, 2026
d3dc877
fix(view): surface paginationLinks() deprecation at runtime and in up…
wheels-bot[bot] May 16, 2026
151feb3
feat(config): auto-register per-package CFML mapping in PackageLoader…
wheels-bot[bot] May 16, 2026
81321f7
feat(web/blog): anatomy of a Wheels package post + manifest doc fixes…
bpamiri May 16, 2026
06e490c
fix(cli): `wheels packages` help now documents `add` as canonical ins…
wheels-bot[bot] May 16, 2026
54f4484
fix(view): paginationNav now accepts Bootstrap-style prepend/append/p…
wheels-bot[bot] May 16, 2026
30fb11e
feat(web/blog): wheels + claude stdio MCP post + OpenCode template fi…
bpamiri May 16, 2026
5a4a299
feat(web/blog,model): beyond findAll post + whereIn empty-array fix (…
bpamiri May 16, 2026
1588e32
feat(view): add viewStyle preset to paginationNav and pageNumberLinks…
wheels-bot[bot] May 16, 2026
707f607
fix(view): paginationNav anchor modes default to window-aware auto-su…
wheels-bot[bot] May 16, 2026
9b1bb9f
fix(middleware): short-circuit OPTIONS preflight in dispatch when COR…
wheels-bot[bot] May 16, 2026
bcc658a
ci: keep refresh-visual-baselines commit body under commitlint's 100-…
bpamiri May 16, 2026
9293ec5
feat(config): plural `mappings` in package.json for legacy CFML alias…
bpamiri May 16, 2026
cb04f68
docs(claude): trim CLAUDE.md, extract deploy/bot/browser docs, add an…
bpamiri May 17, 2026
89709af
fix(model): override $supportsAdvisoryLocks=false on CockroachDB so l…
bpamiri May 17, 2026
76f3b8f
fix(migrator): carve out MySQL in addColumnOptionsSpec text-default a…
wheels-bot[bot] May 17, 2026
b6313cc
fix(model): Oracle insertAll uses INSERT ALL form to avoid RETURNING-…
wheels-bot[bot] May 17, 2026
efb4de6
fix(model): skip CockroachDB advisory-lock specs via capability overr…
wheels-bot[bot] May 17, 2026
afcd208
fix(dispatch): copy arguments to a plain struct in $header() for Adob…
bpamiri May 17, 2026
cd39342
fix(migrator): renameSystemTables skips transaction wrapper on Oracle…
bpamiri May 17, 2026
4a8ebfc
fix(test): work around BoxLang catch-scope discard in lockingSpec (#2…
bpamiri May 17, 2026
9b2c3b8
docs: document attributeCollection + arguments scope gotcha on Adobe …
bpamiri May 17, 2026
b97a051
fix(dispatch): swallow cfheader InvalidHeaderException when response …
bpamiri May 18, 2026
651c34a
fix: Phase 2 Linux repo prep — BE pkg naming, RPM signing, empty-pool…
bpamiri May 18, 2026
0ba3cd0
fix(release): emit .zip.sha512 sidecars + close Scoop install gotchas…
bpamiri May 18, 2026
ac75290
docs: drop `scoop bucket add java` step, update autoupdate descriptio…
bpamiri May 18, 2026
6616405
fix(cli): escape literal # in packages help string (#2760)
bpamiri May 18, 2026
666c728
docs: add scoop install git prereq to Windows install steps (#2763)
bpamiri May 18, 2026
c393ccd
fix(release): bake .module-version at build time, not from source (#2…
bpamiri May 18, 2026
9620085
fix(cli): scoop wheels.cmd invokes java.exe directly to dodge cmd.exe…
wheels-bot[bot] May 20, 2026
bc33fce
chore(scoop): remove stale draft manifests; bucket repo is authoritat…
bpamiri May 20, 2026
63777a0
chore(release): prep CHANGELOG for v4.0.1 GA (#2768)
bpamiri May 20, 2026
ca4ee38
Release 4.0.1 — merge develop
bpamiri May 20, 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
  •  
  •  
  •  
221 changes: 221 additions & 0 deletions .ai/wheels/cross-engine-compatibility.md

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions .ai/wheels/deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Deploy Reference

`wheels deploy` ships a Dockerized Wheels app to production Linux servers via SSH. Ported from Basecamp Kamal's developer CLI — same `config/deploy.yml` schema, same on-server conventions (container names, labels, network, lock path), invokes the same `kamal-proxy` Go binary for zero-downtime rollover. No Ruby runtime required.

## Commands

```
wheels deploy init # scaffold config/deploy.yml + .kamal/secrets
wheels deploy setup # one-time server bootstrap + first deploy
wheels deploy # rolling deploy
wheels deploy --dry-run # print commands without executing
wheels deploy rollback v1 # roll back to a previous version
wheels deploy config # print resolved config as YAML
wheels deploy version # show Kamal version this port mirrors
```

## Subcommands

```
wheels deploy app <verb> # boot/start/stop/details/containers/images/logs/live/maintenance/remove
wheels deploy proxy <verb> # boot/reboot/start/stop/restart/details/logs/remove
wheels deploy accessory <verb> # boot/reboot/start/stop/restart/details/logs/remove (sidecars: db/redis/search)
wheels deploy build <verb> # deliver/push/pull/create/remove/details/dev
wheels deploy registry <verb> # setup/login/logout/remove
wheels deploy bootstrap # install Docker on every host (flat alias — preferred)
wheels deploy exec "<cmd>" # run a command on every host (flat alias — preferred)
wheels deploy server <verb> # exec/bootstrap (legacy nested form — see #2677)
wheels deploy prune <verb> # all/images/containers [--keep=N]
wheels deploy lock <verb> # acquire/release/status (manual — normal deploys auto-lock)
wheels deploy fetch-secrets ... # resolve KEY=VALUE lines from an adapter (flat alias — preferred)
wheels deploy extract-secrets # pull one key from a KEY=VALUE block (flat alias — preferred)
wheels deploy print-secrets # print resolved .kamal/secrets (flat alias — preferred)
wheels deploy secrets <verb> # fetch/extract/print (legacy nested form — see #2697)
wheels deploy audit # tail /tmp/kamal-audit.log on each server
wheels deploy details # aggregate app + proxy + accessory status
wheels deploy remove --confirm # teardown all app/proxy/accessory containers
wheels deploy docs [section] # in-terminal config reference
```

## On-server parity contract (byte-compatible with Ruby Kamal)

- Container names: `<service>-<role>-<version>`
- Labels: `service=`, `role=`, `destination=`, `version=`
- Docker network: `kamal`
- Lock file: `/tmp/kamal_deploy_lock_<service>`
- Proxy config: `/home/<user>/.config/kamal-proxy/`
- Hook env prefix: `KAMAL_*` (never `WHEELS_*` — user hooks migrate unchanged)

A server managed by Ruby Kamal can be taken over by `wheels deploy` without cleanup.

## Architecture

```
cli/lucli/services/deploy/
├── cli/*.cfc DeployMainCli + Deploy<App|Proxy|Accessory|Build|Registry|Server|Prune|Lock|Secrets>Cli
├── commands/*.cfc Base + Docker/App/Proxy/Builder/Registry/Auditor/Lock/Hook/Accessory/PruneCommands
├── config/*.cfc Config + Role/Env/Builder/Proxy/Registry/Ssh/Accessory/Validator/ConfigLoader
├── lib/*.cfc JarLoader/Mustache/Yaml/SshClient/SshPool/FakeSshPool/Output/SecretResolver
└── secrets/*.cfc BaseAdapter + OnePassword/Bitwarden/AwsSecrets/LastPass/Doppler adapters

cli/lucli/lib/deploy/*.jar jmustache, snakeyaml, sshj + BouncyCastle transitives (URLClassLoader-isolated)
cli/lucli/templates/deploy/ Mustache templates for `wheels deploy init` output
```

Commands-are-strings invariant: every `*Commands.cfc` method returns a shell-command string; only `*Cli.cfc` and the orchestrator execute them. That's why `--dry-run` is trivial and unit tests run without network.

## Critical gotchas

1. **Kamal-compatible schema, ONE divergence.** ERB in `deploy.yml` is NOT supported (rendering it would require embedding a Ruby runtime). Kamal's native `${VAR}` env-var interpolation is preserved unchanged — uppercase-snake tokens resolve via `envOverride → .kamal/secrets → System.getenv → ""` (see `ConfigLoader.$interpolate`). Mustache (`{{...}}`) is used only by `wheels deploy init` to scaffold a fresh `deploy.yml`/`secrets`; it is NOT applied to `deploy.yml` at runtime. Everything else in `config/deploy.yml` is byte-identical to Kamal 2.4.0.
2. **Hook env prefix is `KAMAL_`, not `WHEELS_`.** Deliberate — Ruby Kamal users' existing `.kamal/hooks/` scripts work unchanged.
3. **`app live` / `app maintenance` use a marker file** (`/tmp/kamal-maintenance-<svc>`) rather than kamal-proxy native maintenance mode. Phase 2 simplification; Phase 3 follow-up will align with Kamal's proxy-native semantics.
4. **`wheels deploy remove` is destructive and requires `--confirm`.** Bare `wheels deploy remove` throws without touching anything.
5. **Lucee reserved scope names in subagent-authored deploy code.** `client`, `session`, `application` — use `ssh`/`sc`, `sess`, `app` instead. Bit us multiple times during the port.
6. **No `--dry-run` flag in Ruby Kamal 2.4.0.** The `tools/deploy-config-diff.sh` harness compares config-layer output only. Byte-identical command-string parity is aspirational; see `tools/deploy-dry-run-diff.sh` for the plan.
7. **`wheels deploy server <verb>` collides with LuCLI's top-level `server` command.** LuCLI (the picocli runtime under the wheels brand) registers `server` for Lucee dev-server lifecycle, so picocli grabs the `server` token before it can reach the deploy dispatcher. The wheels module exposes flat aliases `wheels deploy bootstrap` and `wheels deploy exec` that sidestep the collision — these are the canonical CLI form. The nested `server <verb>` branch is retained in `Module.cfc::deploy()` for MCP/programmatic callers. See [#2677](https://github.com/wheels-dev/wheels/issues/2677).
8. **`wheels deploy secrets <verb>` collides with LuCLI's top-level `secrets` command.** Same shape as #2677 — LuCLI registers `secrets` for its own credential store (init/set/list/rm/get/provider). The wheels module exposes flat aliases `wheels deploy fetch-secrets`, `wheels deploy extract-secrets`, and `wheels deploy print-secrets` that sidestep the collision — these are the canonical CLI form. The nested `secrets <verb>` branch is retained for MCP/programmatic callers. See [#2697](https://github.com/wheels-dev/wheels/issues/2697).

## Testing

`cli/lucli/tests/specs/deploy/` extends `wheels.wheelstest.system.BaseSpec`. Run with:

bash tools/test-cli-local.sh

Fixtures at `cli/lucli/tests/_fixtures/deploy/configs/` (`minimal.yml`, `full.yml`, `with-accessories.yml`, `invalid/*.yml`). `FakeSshPool.cfc` records every command for offline assertions; no sshd needed for unit tests. `SshClientSpec` + `SshPoolSpec` exercise real SSH via the fixture at `cli/lucli/tests/_fixtures/deploy/sshd/` (brought up by `tools/deploy-sshd-up.sh`).

## Reference docs

- User guides: `web/sites/guides/src/content/docs/v4-0-0/deployment/` (first-deploy, production-config, accessories, secrets, hooks, migrating-from-kamal, security-hardening, docker-deployment)
- In-source CLI docs: `cli/lucli/services/deploy/cli/docs/` (per-verb)
- Design spec: [docs/superpowers/specs/2026-04-20-wheels-deploy-kamal-port-design.md](../../docs/superpowers/specs/2026-04-20-wheels-deploy-kamal-port-design.md)
- Implementation plan: [docs/superpowers/plans/2026-04-20-wheels-deploy-kamal-port.md](../../docs/superpowers/plans/2026-04-20-wheels-deploy-kamal-port.md)
- Retrospective: [docs/superpowers/plans/2026-04-21-phase1-retrospective.md](../../docs/superpowers/plans/2026-04-21-phase1-retrospective.md)
68 changes: 68 additions & 0 deletions .ai/wheels/testing/browser-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Browser Testing

Shipped in v4.0 across PRs #2113, #2115, #2116. Specs extend `wheels.wheelstest.BrowserTest` and drive a real Chromium through `this.browser` — a fluent DSL wrapping Playwright Java.

## Example

```cfm
// vendor/wheels/tests/specs/browser/LoginBrowserSpec.cfc
component extends="wheels.wheelstest.BrowserTest" {

this.browserEngine = "chromium"; // chromium only in PR 1

function run() {
// browserDescribe() wraps describe() with beforeEach/afterEach that
// create a fresh Page per `it`. WheelsTest's BDD lifecycle only treats
// beforeAll/afterAll as class-level, so we register per-it hooks
// from inside the suite body via this helper.
browserDescribe("Login flow", () => {
it("can load a page and read its title", () => {
if (this.browserTestSkipped) return;
this.browser.visitUrl("data:text/html,<title>Hi</title><h1>x</h1>")
.assertTitleContains("Hi");
});
});
}
}
```

## Installation

Install Playwright locally before first run (~370MB download: JARs + Chromium):

```bash
wheels browser setup # downloads JARs + Chromium
```

Then run browser specs via the normal test suite:

```bash
bash tools/test-local.sh # skips browser specs if JARs missing
```

## Implemented DSL methods

- **Navigation:** visit, visitUrl, back, forward, refresh, visitRoute
- **Interaction:** click, press, fill, type, clear, select, check, uncheck, attach, dragAndDrop
- **Keyboard:** keys, pressEnter, pressTab, pressEscape
- **Waiting:** waitFor, waitForText, waitForUrl
- **Scoping:** within(selector, callback)
- **Cookies:** setCookie, deleteCookie, cookie, clearCookies
- **Auth:** loginAs, logout
- **Dialogs:** acceptDialog, dismissDialog, dialogMessage (Lucee-only via createDynamicProxy)
- **Viewport:** resize, resizeToMobile, resizeToTablet, resizeToDesktop
- **Script:** script (returns `page.evaluate` result), pause
- **Assertions (text/vis/presence):** assertSee, assertDontSee, assertSeeIn, assertVisible, assertMissing, assertPresent, assertNotPresent
- **Assertions (URL/title/query):** assertUrlIs, assertUrlContains, assertTitleContains, assertQueryStringHas, assertQueryStringMissing, assertRouteIs
- **Assertions (form):** assertInputValue, assertChecked, assertHasClass
- **Terminals:** currentUrl, title, pageSource, text, value, screenshot

## Key gotchas

- **`##` in selectors** — CFML requires `##` to emit literal `#`. `"##email"` → `"#email"` at runtime.
- **`client` is a Lucee reserved scope.** `var client = ...` in a closure throws "client scope is not enabled". Use `var c = ...` or `var bc = ...`. (Generalized rule: see CLAUDE.md anti-pattern #11.)
- **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.
- **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.
- **Dialogs are Lucee-only** — `acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines.
34 changes: 34 additions & 0 deletions .ai/wheels/wheels-bot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Wheels Bot

`wheels-bot[bot]` is a custom GitHub App that runs Claude-powered automation on issues and PRs in `wheels-dev/wheels`. Five stages, all opt-out via the `[skip-claude]` label or repo variable `WHEELS_BOT_ENABLED=false`. Slash-command prompts live in `.claude/commands/`; workflows in `.github/workflows/bot-*.yml`. Full user-facing docs: [`docs/contributing/wheels-bot.md`](../../docs/contributing/wheels-bot.md).

## Stages

| Stage | Trigger | Model | Output |
|---|---|---|---|
| Triage | issue opened/reopened | Opus | Comment classifying as `bug` / `framework-design` / `other` (+ confidence on `bug` path). Reads code with the allowlisted tools to resolve uncertainty before rating. |
| Research | bot triage emits `framework-design` marker | Opus | Comment comparing Rails / Laravel / Django / Phoenix / Spring Boot / +1 and recommending a Wheels-idiomatic path (+ confidence). |
| Propose Fix | bot triage emits `triage-confidence:high\|medium` OR research emits `research-confidence:high\|medium` (or `workflow_dispatch`) | Opus | TDD-mandatory draft PR on branch `fix/bot-<issue>-<slug>`. Spec-then-implementation, both required by `bot-tdd-gate.yml`. |
| Reviewer A | PR opened / synchronized / ready_for_review | Sonnet | Single PR review with line comments, verdict, and `wheels-bot:review-a:<pr>:<sha>` marker. |
| Reviewer B | Reviewer A submits a review | Sonnet | PR comment critiquing A for sycophancy, false positives, and missed issues. Loop cap = 3 rounds. |

## Marker conventions (HTML comments, used for idempotency)

- `<!-- wheels-bot:triage:<issue> -->` + `<!-- wheels-bot:triage-class:<bug|framework-design|other> -->` (+ optional `<!-- wheels-bot:triage-confidence:high|medium -->` — either fires propose-fix; low omitted)
- `<!-- wheels-bot:research:<issue> -->` (+ optional `<!-- wheels-bot:research-confidence:high|medium -->` — either fires propose-fix; low omitted)
- `<!-- wheels-bot:fix:<issue> -->` / `<!-- wheels-bot:fix-held:<issue> -->`
- `<!-- wheels-bot:review-a:<pr>:<sha> -->`
- `<!-- wheels-bot:review-b:<pr>:<sha>:<round> -->`
- `<!-- wheels-bot:auto-close:<issue> -->`

## Allow-listed scopes per stage

Every bot-authored commit must conform to the `commitlint.config.js` allowlist (see CLAUDE.md § Commit Message Conventions). The bot's prompt (`.claude/commands/_shared-rails.md`) re-states the allowlist verbatim.

## Kill switch

Flip the repo variable `WHEELS_BOT_ENABLED` to `false` to halt every bot workflow without code changes. Add the `[skip-claude]` label (or `[skip-claude]` in the title) to halt activity on a single issue/PR.

## 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`.
8 changes: 7 additions & 1 deletion .claude/commands/research-frameworks.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,13 @@ Read `.claude/commands/_shared-rails.md` first. Highlights for this command:

Where `<CONFIDENCE_MARKER>` is:
- `<!-- wheels-bot:research-confidence:high -->` if confidence is high
- omitted otherwise
- `<!-- wheels-bot:research-confidence:medium -->` if confidence is medium
- omitted if confidence is low

Both `high` and `medium` markers trigger auto-fire of
`bot-propose-fix.yml`. Low stays manual — material framework
disagreement or new-infrastructure proposals warrant a human discussion
before any code is written.

7. **Self-check before posting.**
- Have you cited at least one URL per framework?
Expand Down
35 changes: 27 additions & 8 deletions .claude/commands/triage-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,18 @@ below. Highlights for this command:
existing structure under
`web/sites/guides/src/content/docs/v4-0-0-snapshot/`, the work is
mostly translation-of-code (not requiring deep design decisions).
High-confidence docs-requests trigger the auto-fire write-docs
workflow.
- **`medium`**: the gap is real but the right page/path is ambiguous,
OR a new top-level section is needed (a structural design decision),
OR the docs require significant code investigation to write
accurately.
- **`low`**: the gap is vague, the scope is large (e.g. "rewrite the X
chapter"), or it's not clear what concretely needs to exist.

**Auto-fire threshold:** both `high` and `medium` docs-requests trigger
the auto-fire write-docs workflow. Write-docs has its own safety net
for structural docs-architecture decisions (posts a `docs-held` marker
instead of opening a PR). Only `low` stays manual.

Post the triage comment:

```
Expand All @@ -133,8 +136,11 @@ below. Highlights for this command:

Where `<CONFIDENCE_MARKER>` is:
- `<!-- wheels-bot:docs-confidence:high -->` if confidence is high
(triggers auto-fire of `bot-write-docs.yml`)
- omitted otherwise (medium/low confidence does not auto-trigger)
- `<!-- wheels-bot:docs-confidence:medium -->` if confidence is medium
- omitted if confidence is low (low does not auto-trigger)

Both `high` and `medium` markers trigger auto-fire of
`bot-write-docs.yml`.

Then exit.

Expand All @@ -155,16 +161,25 @@ below. Highlights for this command:

- **`high`**: the report has a clear "what happened / what I expected"
description; the suspected layer is unambiguous; the fix sketch is
mechanical (one file, no design decisions). High-confidence bugs
trigger the auto-fire fix-PR workflow.
mechanical (one file, no design decisions).
- **`medium`**: the report is clear but the fix has design trade-offs,
OR cross-engine concerns may exist, OR more than one layer is
plausibly involved.
- **`low`**: the report is ambiguous; reproduction steps are vague;
environment-specific symptoms are suspected; the fix shape isn't
obvious from the issue body alone.

**Auto-downgrade rules** (force at least one level lower):
**Auto-fire threshold:** both `high` and `medium` bugs trigger the
auto-fire propose-fix workflow. Propose-fix has its own step-4 safety
net for sensitive areas (security / middleware / migrations / deploy /
DI / cross-engine) — it posts a `fix-held` marker instead of opening a
PR. Reviewer A and B will critique whatever propose-fix produces. Only
`low` stays manual.

**Auto-downgrade rules** (force at least one level lower — these
downgrades are still informative for humans skimming the comment;
`medium` ratings still auto-fire, but propose-fix's safety net will
catch the sensitive-area cases):
- The fix would touch `vendor/wheels/security/**`, auth flows, or any
`vendor/wheels/middleware/**` → at most `medium`
- Cross-engine concern detected (Lucee vs Adobe vs BoxLang behavior
Expand Down Expand Up @@ -199,7 +214,11 @@ below. Highlights for this command:

Where `<CONFIDENCE_MARKER>` is:
- `<!-- wheels-bot:triage-confidence:high -->` if confidence is high
- omitted otherwise (medium/low confidence does not auto-trigger fix-PR)
- `<!-- wheels-bot:triage-confidence:medium -->` if confidence is medium
- omitted if confidence is low (low does not auto-trigger fix-PR)

Both `high` and `medium` markers trigger auto-fire of
`bot-propose-fix.yml`.

8. **Self-check before posting.**
- Is the classification justified by quoted text from the issue?
Expand Down
Loading