Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bb82348
docs(fix-issue): restore the microflow loop symptom rows
claude Jul 31, 2026
9321006
fix(docker): stop `docker build` converting MPRv2 projects to MPRv1 (…
claude Jul 31, 2026
13e2363
Merge pull request #63 from ako/claude/microflow-symptom-rows
ako Jul 31, 2026
e246032
Merge pull request #64 from ako/claude/docker-build-mprv2-808
ako Jul 31, 2026
9b97912
Merge pull request #65 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Jul 31, 2026
c00ab83
feat(grammar)!: require semicolons on microflow and nanoflow statements
claude Jul 31, 2026
e0e4883
Merge pull request #66 from ako/claude/microflow-mandatory-semicolons
ako Aug 1, 2026
24e5bc1
Merge branch 'mendixlabs:main' into main
ako Aug 1, 2026
361c53e
fix(pages): write a null TitleOverride, unblanking every popup captio…
claude Aug 1, 2026
38766a3
fix(new): resolve the exact Mendix version, and verify what was created
claude Aug 1, 2026
77de33c
docs(skills): verify a fix at the layer its symptom lives in
claude Aug 1, 2026
6432bc4
Merge origin/main into claude/showpage-titleoverride-812
claude Aug 1, 2026
761192d
Merge origin/main into claude/new-version-exact-mxbuild
claude Aug 1, 2026
73301a8
Merge pull request #68 from ako/claude/showpage-titleoverride-812
ako Aug 1, 2026
2767afb
docs(fix-issue): append symptom rows at the end of the table
claude Aug 1, 2026
863fc5c
Merge origin/main into claude/new-version-exact-mxbuild
claude Aug 1, 2026
b546544
Merge pull request #69 from ako/claude/new-version-exact-mxbuild
ako Aug 1, 2026
94dc279
Merge pull request #70 from ako/claude/verify-in-runtime-skill
ako Aug 1, 2026
68f290a
fix(microflows): declare the Commit values Mendix actually defines
claude Jul 31, 2026
d55832d
feat(microflows): express the Commit flag on create/change activities…
claude Jul 31, 2026
50515be
feat(microflows): emit the Commit flag in DESCRIBE MICROFLOW (#779)
claude Jul 31, 2026
efa3de4
Merge pull request #71 from ako/claude/commit-flag-779-recover
ako Aug 1, 2026
6067f41
docs(web): document the full Claude Code web workflow (repo → hub key…
claude Aug 1, 2026
216c11a
Merge pull request #72 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 1, 2026
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
31 changes: 29 additions & 2 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ end;
- Parameters start with `$` prefix
- Return variable must be declared or used
- Every microflow must end with `return` statement
- Statements end with semicolon `;`
- Every body statement ends with a semicolon `;` — **required**, not optional. This
includes block terminators: `end if;`, `end loop;`, `end while;`, `end case;`.
A missing one is a parse error (`missing ';' at 'return'`), not a warning.
- Microflow ends with `/` separator

### Parameter Types
Expand Down Expand Up @@ -540,15 +542,34 @@ $NewProduct = create Test.Product (
- Closing `)` followed by semicolon
- Syntax aligned with CALL MICROFLOW/CALL JAVA ACTION

**The Commit flag** (Studio Pro's "Commit" dropdown) is the optional `commit`
modifier after the member list:

```mdl
$Order = create Sales.Order (Number = $Nr); -- Commit: No (default)
$Order = create Sales.Order (Number = $Nr) commit; -- Commit: Yes
$Order = create Sales.Order (Number = $Nr) commit without events;-- Commit: YesWithoutEvents
```

Omit it for the default. This is a **modifier on the create**, not the separate
`commit $Var;` activity — see COMMIT Object below for that one.

### CHANGE Object

```mdl
change $Product (
Name = $NewName,
ModifiedDate = [%CurrentDateTime%]);

-- Commit the changed object as part of the change activity
change $Product (Name = $NewName) commit;
change $Product (Name = $NewName) commit without events;

-- Refresh the changed object in the client
change $Product (Name = $NewName) refresh;

-- Both: commit comes first
change $Product (Name = $NewName) commit refresh;
```

**Note**: Only specify attributes you want to change. Syntax aligned with CREATE.
Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/mendix/write-nanoflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ BEGIN
END;
```

**Every body statement ends with a semicolon `;`** — required, not optional, exactly as
in microflows. That includes block terminators: `end if;`, `end loop;`, `end while;`,
`end case;`. A missing one is a parse error (`missing ';' at 'return'`), not a warning.

## Naming Convention

Nanoflow names use the `NAV_` prefix by convention:
Expand Down
183 changes: 183 additions & 0 deletions .claude/skills/verify-in-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Verify a Fix in the Running App

A contributor workflow for proving a fix in a **real Mendix app in a real browser**,
rather than at the BSON or parser layer.

This is the outermost verification tier. It is slow (~5 min the first time, ~2 min
after) and it is not needed for most fixes — read *When This Is Required* before
reaching for it.

## When This Is Required

Use the layer where the symptom actually lives:

| Symptom lives in | Sufficient proof |
|------------------|------------------|
| the parser / grammar | unit test |
| the BSON we write | unit test on the encoded document |
| files on disk after `mx` runs | integration test (`-tags integration`) |
| **the rendered app's behaviour or appearance** | **this skill** |

**The rule: if the symptom is a property of the *running app* rather than of the file
we write, no unit or BSON test can prove the fix.** A page can serialize to perfectly
correct-looking BSON and still render wrong.

Worked example — mendixlabs/mxcli#812. Every popup opened by an mxcli-authored button
showed a blank caption. The BSON was structurally valid, `mx check` reported 0 errors,
and MxBuild completed. Nothing below the browser could see the defect, because the
defect *was* the rendering: an empty `Microflows$TextTemplate` is not "no title
override", it is an override to the empty string.

Counter-examples from the same week, where this skill would have been waste:
`#808` (MPRv2 → v1 conversion — visible on disk, integration test), `#779` (Commit
flag — visible in BSON round-trip, unit test), mandatory semicolons (parser).

## The Procedure

### 1. Cache MxBuild for the version you want

```bash
mxcli setup mxbuild --version 11.12.2
```

Do this **first**, explicitly. Test against the version users report against, not
whatever happens to be cached.

### 2. Create a scratch project — at a SHORT path

```bash
mxcli new PopupDemo --version 11.12.2 --output-dir /root/pd
```

> **Trap: `PathTooLongException`.** Mendix's package extractor fails on deep paths.
> A scratchpad path like
> `/tmp/claude-.../<uuid>/scratchpad/proj` is already too long and
> `mx create-project` dies. Use a short root (`/root/pd`). The same applies to Go
> tests: set `TMPDIR=/root/t` so `t.TempDir()` stays short, or the scaffolding fails
> and the test **skips** — which is indistinguishable from passing.

Confirm you got the version you asked for; the connect banner prints it:

```bash
echo "" > /root/pd/noop.mdl
mxcli exec /root/pd/noop.mdl -p /root/pd/PopupDemo.mpr | grep -i connected
# Connected to: /root/pd/PopupDemo.mpr (Mendix 11.12.2)
```

### 3. Author the smallest model that shows the symptom

Keep it to the widgets/flows involved. A popup-caption repro is one popup page and one
button:

```mdl
create or replace page MyFirstModule.OrderPopup (
title: 'Order Details Popup',
Layout: Atlas_Core.PopupLayout
) { container c1 { dynamictext dtBody (content: 'Body of the popup') } }
/
create or replace page MyFirstModule.Home_Web (
title: 'Home', Layout: Atlas_Core.Atlas_Default
) {
container c1 {
actionbutton btnOpen (caption: 'Open popup', action: show_page MyFirstModule.OrderPopup)
}
}
/
```

### 4. Provision and boot

```bash
mxcli run --local -p /root/pd/PopupDemo.mpr --setup --ensure-db # once
mxcli run --local -p /root/pd/PopupDemo.mpr # boot (blocks)
```

`--setup` caches the runtime and provisions Postgres without booting. Run the boot in
the background and wait for the ready line rather than sleeping:

```bash
until grep -qE 'App is running at|Error:|Exception' run.log; do sleep 3; done
```

First boot downloads the runtime and bundles the web client (~2–4 min). Later boots
are fast.

### 5. Assert in the browser

Chromium is pre-installed at `/opt/pw-browsers/chromium`; install the Playwright
package only (never `playwright install`):

```bash
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install playwright
```

Drive it with `NO_PROXY='*'` so the agent proxy does not intercept localhost. Assert
on **text content**, and print a JSON result so the outcome is unambiguous:

```js
const browser = await chromium.launch({ executablePath: '/opt/pw-browsers/chromium' });
// ... click, wait for the dialog, then:
const title = await dialog.locator('.modal-header .modal-title').innerText();
console.log(JSON.stringify({ captionText: title, captionIsBlank: title.trim() === '' }));
```

### 6. A/B against the broken build — this is the actual proof

**A run that only shows the fixed state proves nothing.** It shows the app works, not
that your change is why. Build a binary from before the fix, rewrite the same model
with it, restart, and re-observe:

```bash
git checkout HEAD~1 -- <the writer files>
go build -o /tmp/mxcli-buggy ./cmd/mxcli
git checkout HEAD -- <the writer files> # restore immediately

/tmp/mxcli-buggy exec demo.mdl -p app.mpr # rewrite the model, broken
# restart the app, re-run the browser assertion
```

The #812 result, which is what a finished verification looks like:

| build | `TitleOverride` | `headerText` | caption |
|---|---|---|---|
| fixed | `null` | `"×\nOrder Details Popup"` | `"Order Details Popup"` |
| pre-fix | empty `TextTemplate` | `"×"` | `""` |

Only the second row proves the fix is the cause.

> **Trap: restoring after the mutation.** Use **absolute paths** when copying files
> back. A relative `cp` after a `cd` silently fails and leaves the reverted code in
> your tree — it will be committed. Always finish with `git status`.

## Why This Tier Exists

Two bugs in one week had a green test suite while broken:

- **#812** — a duplicate `codec.RegisterTypeDefaults` for the same `$Type` silently
clobbered the fix (registrations overwrite rather than merge, resolved by init
order). The unit test passed; the BSON was still wrong. Caught only by dumping the
actual document.
- **#808** — the integration test had *only ever skipped*, for want of an `mx` binary.
A skip reads exactly like a pass in CI output.

So: **a green suite is evidence about the layer it tests, and nothing more.** When a
test can silently become a no-op, prefer `t.Fatal` over `t.Skipf` once the
prerequisite is present — a missing `mx` is a legitimate skip; a present `mx` plus
failed scaffolding is a failure.

## Cleanup

```bash
pkill -f 'mxbuild --serve'; pkill -f runtimelauncher
rm -rf /root/pd
```

Cached MxBuild is ~1.4 GB per version. Keep the versions you test against; drop the
rest if disk gets tight (`~/.mxcli/mxbuild/<version>`).

## Related

- `.claude/skills/fix-issue.md` — symptom table; start there
- `.claude/skills/debug-bson.md` — when the symptom *is* in the BSON
- `.claude/skills/mendix/run-local.md` — full `run --local` reference (flags,
`--screenshot`, hot reload)
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,10 @@ When reviewing pull requests or validating work before commit, verify these item

### Bug fixes
- [ ] **Fix-issue skill consulted** — read `.claude/skills/fix-issue.md` before diagnosing; match symptom to table before opening files
- [ ] **Symptom table updated** — new symptom/layer/file mapping added to `.claude/skills/fix-issue.md` if not already covered
- [ ] **Symptom table updated** — new symptom/layer/file mapping added to `.claude/skills/fix-issue.md` if not already covered. **Append at the END of the table**: every fix touches this file, so inserting at the top means two concurrent fixes conflict at the same line by construction (five resolution rounds in one week). The table is looked up by matching a symptom, not read in order, so position carries no meaning
- [ ] **Test written first** — failing test exists before implementation (parser test in `sdk/mpr/`, backend mutation test in `mdl/backend/mpr/`, executor handler test in `mdl/executor/` using `MockBackend`)
- [ ] **Verified at the layer the symptom lives in** — a test proves something about the layer it exercises and nothing more. Parser/grammar → unit test. BSON we write → unit test on the encoded document. Files on disk after `mx` runs → integration test (`-tags integration`). **The rendered app's behaviour or appearance → `.claude/skills/verify-in-runtime.md`** (boot with `run --local`, assert in Playwright). A page can serialize to valid-looking BSON, pass `mx check`, build cleanly, and still render wrong — that was #812.
- [ ] **Fix proven to be the cause** — revert the fix (or stub the guard) and confirm the test fails with the reported symptom. A test that only passes against fixed code has not been shown to detect anything; two bugs this week had a green suite while live (#812 a clobbered `RegisterTypeDefaults`, #808 an integration test that had only ever skipped)

### Overlap & duplication
- [ ] Check `docs/11-proposals/` for existing proposals covering the same functionality
Expand Down Expand Up @@ -575,6 +577,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
- `docs/03-development/PAGE_BSON_SERIALIZATION.md` - Page/widget BSON format, type mappings, required defaults
- `docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md` - What's version-resilient vs version-fragile in widget BSON output, and how to onboard a new Mendix minor (e.g. 11.10)
- `.claude/skills/debug-bson.md` - Workflow for debugging BSON serialization issues with `mx` tool (includes the "Studio Pro Update Widget" diff methodology that closed CE0463)
- `.claude/skills/verify-in-runtime.md` - Proving a fix in a real app in a real browser (`run --local` + Playwright). For symptoms that only exist at render time, where valid-looking BSON and a clean `mx check` prove nothing — see #812
- `cmd/mxcli/lsp.go` - LSP server implementation (hover, definition, diagnostics, completion, symbols)
- `cmd/mxcli/init.go` - `mxcli init` command (project initialization + VS Code extension install)
- `cmd/mxcli/docker/oql.go` - OQL query execution against running Mendix runtime via M2EE admin API
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,30 @@ mxcli's primary value is that AI agents (Claude Code, Cursor, etc.) can generate
| Claude uses outdated patterns | Update CLI help text (`Short`/`long`/`Example` in Cobra) |
| Error messages are unhelpful | Improve error text with hints |

### End-to-end testing on the web (build a real app + keep a findings doc)

The highest-signal way to exercise mxcli is to **build a real Mendix app with it in
[Claude Code on the web](https://claude.ai/code)** and record what breaks. This is
the same loop the maintainers dogfood with (e.g. the `mxcli-traceops` /
`mxcli-timeregistration` test apps), and it works from a laptop or an iPad — no
local install.

1. Create an **empty GitHub repo** and a Claude Code environment on it (set
`MXCLI_HUB_KEY` for browser preview; allow outbound HTTPS to GitHub Releases,
Mendix's CDN, and `hub.mxcli.org`).
2. Paste the **[bootstrap prompt](https://mendixlabs.github.io/mxcli/tools/bootstrap-prompt.html)**
— it installs the toolchain, creates the app, starts a `FINDINGS.md`, commits,
and boots the app.
3. Ask the agent to build real features (a domain model, CRUD pages, microflows),
and verify with `mxcli run --local` + `mxcli oql` and a real `mx check`.

**Keep a `FINDINGS.md`** at the repo root as you go — one entry per surprise or
defect: the exact MDL, the command, the error, the Mendix + mxcli versions, and how
you verified it. A finding that reproduces against a real `mx check` is often enough
to drive a fix straight into a symptom row in `.claude/skills/fix-issue.md`. The full
workflow is documented in
**[Developing on the Web (Claude Code)](https://mendixlabs.github.io/mxcli/tutorial/claude-code-web.html)**.

### PR Section

Include in your PR:
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,19 @@ This downloads MxBuild, creates a blank Mendix project, sets up AI tooling and a

### From the web or an iPad (empty repo, no local install)

No machine with a CLI? Open an **empty repo** in [Claude Code on the web](https://claude.ai/code) (works on an iPad) and paste the **bootstrap prompt** — the agent provisions the whole project (creates the app, wires the Dev Container + AI tooling, provisions the database) and commits it so future sessions self-bootstrap. This is the recommended web/iPad path; you don't need to pick a GitHub template.

> See **[Bootstrap Prompt](https://mendixlabs.github.io/mxcli/tools/bootstrap-prompt.html)** for the exact copy-paste text. In short: it runs `mxcli new` → `mxcli init` → commits the config → `mxcli run --local --setup --ensure-db` so the app comes up testable.
No machine with a CLI? Build a Mendix app **entirely in the browser** with
[Claude Code on the web](https://claude.ai/code) (works on an iPad) — mxcli
provisions the whole toolchain in the cloud container and commits it so future
sessions self-bootstrap. You don't need to pick a GitHub template. The full
workflow:

1. **Create an empty GitHub repo** — this is where the app lives and where sessions run.
2. **(Optional) Get a hub key** for a browser preview: open <https://hub.mxcli.org/cli>, sign in with GitHub, and **Create a hub key**.
3. **Create a Claude Code environment** on the repo. Set `MXCLI_HUB_KEY=<key>` (from step 2) and allow outbound HTTPS to GitHub Releases, Mendix's CDN, and `hub.mxcli.org`.
4. **Paste the bootstrap prompt** into a session. The agent installs mxcli, runs `mxcli new` → `mxcli init`, starts a `FINDINGS.md`, commits the config, and boots the app with `mxcli run --local --setup --ensure-db` so it comes up testable.
5. **Iterate** with the warm loop below; add `--hub https://hub.mxcli.org` for a shareable browser preview.

> **[Developing on the Web (Claude Code)](https://mendixlabs.github.io/mxcli/tutorial/claude-code-web.html)** walks the whole workflow, and **[Bootstrap Prompt](https://mendixlabs.github.io/mxcli/tools/bootstrap-prompt.html)** has the exact copy-paste text.

Then iterate with the **warm local dev loop** — a Docker-free ~1-second edit→test cycle:

Expand Down
23 changes: 23 additions & 0 deletions cmd/mxcli/cmd_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"runtime"

"github.com/mendixlabs/mxcli/cmd/mxcli/docker"
"github.com/mendixlabs/mxcli/sdk/mpr"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -115,6 +116,28 @@ Examples:
}
fmt.Printf(" Created %s\n", mprPath)

// The project is stamped with the version of the binary that created it, not
// with --version. Resolving the wrong binary therefore yields a project at a
// version the user never asked for, and every later step (init, mxbuild,
// runtime) silently follows it. Check the postcondition rather than trusting
// the resolution: a mismatch here means the model is wrong, so fail loudly
// instead of handing back something that merely looks finished.
if reader, err := mpr.Open(mprPath); err == nil {
created := reader.ProjectVersion().ProductVersion
reader.Close()
if created != "" && created != mendixVersion {
fmt.Fprintf(os.Stderr,
"Error: requested Mendix %s but the created project is %s.\n",
mendixVersion, created)
fmt.Fprintf(os.Stderr,
" mx create-project stamps the project with the version of the binary that ran it (%s).\n", mxPath)
fmt.Fprintf(os.Stderr,
" Run 'mxcli setup mxbuild --version %s' and try again.\n", mendixVersion)
os.Exit(1)
}
fmt.Printf(" Mendix version: %s\n", created)
}

// Step 3: Initialize tooling
if !skipInit {
fmt.Printf("\nStep 3/4: Initializing AI tooling...\n")
Expand Down
17 changes: 8 additions & 9 deletions cmd/mxcli/docker/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,15 @@ func Build(opts BuildOptions) error {
if err != nil {
fmt.Fprintf(w, " Skipping check: %v\n", err)
} else {
// Run update-widgets before check to prevent false CE0463 errors
// Run update-widgets before check to prevent false CE0463 errors.
// runUpdateWidgets preserves the project's on-disk storage format: the bare
// invocation this replaced converted MPRv2 projects to MPRv1 and deleted
// mprcontents/ (mendixlabs/mxcli#808). restore is deferred to Build's exit
// rather than run here, so both `mx check` and MxBuild below see the
// widget-normalized model; only the on-disk format is put back.
if !opts.SkipUpdateWidgets {
fmt.Fprintln(w, " Updating widget definitions...")
uwCmd := exec.Command(mxPath, "update-widgets", updateWidgetsPathArg(opts.ProjectPath))
uwCmd.Stdout = w
uwCmd.Stderr = os.Stderr
PrepareMxCommand(uwCmd)
if err := uwCmd.Run(); err != nil {
fmt.Fprintf(w, " Warning: update-widgets failed (continuing): %v\n", err)
}
restore := runUpdateWidgets(mxPath, opts.ProjectPath, w, os.Stderr)
defer restore()
}

cmd := exec.Command(mxPath, "check", opts.ProjectPath)
Expand Down
Loading
Loading