Bootstrap prompt: interview first, multi-app solutions, and honour the configured app host name - #106
Merged
Merged
Conversation
The prompt hardcoded "App" as the application name, so every repo
bootstrapped from it ended up with App.mpr regardless of what was being
built — and the name is awkward to change afterwards: it is the .mpr file
name, the Studio Pro app name, and the path baked into the SessionStart
hook.
A Step 0 now asks for the app name, what the app is for, what it keeps
track of, who logs in, the theme and the Mendix version — all in one
message with defaults, so "defaults" is a valid answer and the agent is
told not to block twice. The answers become the brief, written to
README.md and committed, so a session resuming after an idle reap knows
what it is building. The provisioning steps take <AppName> throughout,
and a closing step has the agent propose the model from the brief and
wait rather than inventing one.
Two corrections while rewriting, both verified rather than assumed:
- "Create the app at the repo root: mxcli new App" cannot work. mxcli
new refuses a directory that is not empty and .git alone trips it,
and with no --output-dir it creates ./App/, leaving every later
`-p App.mpr` wrong by one directory. The prompt now creates in a
subfolder and moves the contents up, which is what the SessionStart
hook's relative path needs.
- The blank template's module is MyFirstModule, not the app name, so
the closing step asks for a module name explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The prompt assumed one app at the repo root, which is most of what people
want but silently wrong for a solution — a backend that owns the data and
publishes OData, plus a frontend that consumes it.
The interview now opens by asking the shape, and a new section carries the
deltas that a second app actually needs. They are deltas, not a rewrite:
per-app subfolders instead of moving to the root, explicit ports for the
second app (8180/8190/6643 — 8081/8091/6544 belong to `mxcli test
--local`), and `--hub-solution` so previews group. Databases need no
action, since the name is derived from the .mpr file name.
Two things the agent could not have inferred, both verified:
- `mxcli init` dedupes the SessionStart hook on the command, not on the
project, so a second app never gets its own entry. Claude Code reads
the root `.claude/settings.json`, which has to be written by hand,
one line per app.
- `CREATE ODATA CLIENT` fetches the $metadata at creation time and
caches it (warning, not failing, when unreachable). So the producer
must be published and running before the consumer is wired, and
ServiceUrl belongs in a constant since it will not stay localhost.
Default Mendix version 11.6.3 -> 11.13.0, with a note on what "newest
supported" means: both mxbuild-<v> and mendix-<v> must be on the CDN, and
a solution's apps should share a version. Confirmed 11.13.0 serves both
tarballs and 11.14.0 serves neither.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Separating a solution's apps by port alone does not separate their sessions: cookies are keyed on host name and ignore the port, so localhost:8080 and localhost:8180 share one jar and a login to one can replace the other's XASSESSIONID. Distinct hostnames fix it and need no mxcli change, which is worth stating explicitly because the obvious alternative — one loopback IP per app on a shared port — is not possible today: the runtime binds 127.0.0.1 and there is no listen-address flag. Verified against a booted app on 11.12.1: a foreign Host header is served 200 (via /etc/hosts, nip.io and localtest.me alike), and the client uses relative URLs, so no ApplicationRootUrl is needed for this. /etc/hosts is recommended over public wildcard DNS for locked-down containers, where localtest.me resolves to ::1. Also notes the one thing the hostname alone does not cover: ApplicationRootUrl is only set by --hub, so absolute-URL cases (OIDC/SAML redirect URIs, deep links) still advertise the listen address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The project's own configuration (App Settings -> Configurations in Studio Pro, `alter settings configuration '…' ApplicationRootUrl = …` in MDL) is where a custom host name belongs — versioned with the app, per configuration, no flag to repeat. `run --local` ignored it: the boot payload's ApplicationRootUrl came only from a --hub registration, and since update_configuration replaces rather than merges, nothing else could supply it. It is now read at boot and used when no hub URL was assigned (a hub assignment wins, being the URL actually serving the app). Without it, serving a solution's apps under their own host names still works — the runtime accepts any Host and the client uses relative URLs — but the absolute URLs Mendix generates for OIDC/SAML redirects and deep links kept naming the listen address. The trap: a blank Mendix app already ships ApplicationRootUrl = http://localhost:8080/, so "is set" does not mean "was chosen". Honouring every value would change behaviour for every existing project and advertise the wrong port under --app-port. Only a non-loopback host is passed through, and a port disagreeing with --app-port warns. Verified end-to-end on 11.12.1: with backend.local configured, the boot prints the configuration it came from and the app answers 200 on both the host name and the listen address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…tity A widget bound to an attribute the context entity inherits rather than declares passed `mxcli check --references` and `mxcli lint`, and then the real MxBuild rejected the project: [error] [CE1613] "The selected attribute 'TaskBoard.Person.FullName' no longer exists." Mendix stores the reference against the entity that DECLARES the attribute; mxcli qualified it with the entity in context, leaving a dangling reference. The message reads as a deletion — it never existed there. Two independent resolvers had the bug and both are fixed: the direct binding (resolveAttributePath) and the final attribute of an association path (resolveAssociationAttributePath). The reporter's own table showed both failing, and the association case still failed after the first patch — a probe of the direct case alone would have shipped half a fix. Unknown names keep their previous context qualification rather than being re-pointed, and a cyclic generalization chain terminates. Attribute resolution now consults the domain models, so the lookup bails out when there is no backend and nothing cached — several unit tests build a pageBuilder with neither. A/B on Mendix 11.12.1: pre-fix binary gives CE1613 for the inherited column and 0 errors for the own column; fixed binary gives 0 errors for both shapes, direct and over an association. Repro: mdl-examples/bug-tests/todo-12-inherited-attribute-on-page.mdl Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The generated hook was `test -x ./mxcli && ./mxcli run --local --setup --ensure-db -p App.mpr || true`, and .gitignore excludes that binary (~85 MB) on purpose. In an ephemeral container the two combine badly: the container is reclaimed, the repo re-cloned without the binary, the guard fails, and the hook silently no-ops through `|| true`. The next session has no mxcli, no MxBuild cache and no database — exactly the state the hook exists to prevent — with nothing said about it. `mxcli init` now writes a committed `.claude/bootstrap-mxcli.sh` that resolves OS/arch, fetches the binary when it is missing (MXCLI_TAG pins a version, default nightly) and then runs the setup; the hook is reduced to `sh .claude/bootstrap-mxcli.sh || true`. A hook line cannot reasonably do OS/arch detection, but a committed script can — and the binary stays out of git. That changes the hook command, which is what dedupe matched on, so addSessionStartHook now recognises any known marker and rewrites the entry in place. An existing project migrates on the next `mxcli init` instead of ending up with two hooks that both run. Also adds /theme-cache/ to the generated .gitignore: MxBuild regenerates the compiled theme on every build, so tracking it means a fresh clone goes dirty the first time anyone builds. Verified by reproducing the reap: moved ./mxcli out of a project, ran the hook command verbatim, and watched it re-download the binary (88 MB, new mtime) and finish with "Setup complete … database ready". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
ako
force-pushed
the
claude/mxbuild-diagnostics-spike-emta6h
branch
from
August 7, 2026 11:19
c501fde to
a87de0a
Compare
A heredoc is the natural way to drive MDL from an agent or a shell script, and `-` is how every other Unix tool spells stdin. It was taken literally as a filename: Error reading file: open -: no such file or directory so every ad-hoc script needed writing to a temp file first. One helper now backs both commands, so `check` gained the same spelling rather than only the reported one; it reports the source as <stdin> instead of a bare dash. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mxcli syntax` is the reference an agent reads before writing MDL, and
nothing checked it against the parser. Two entries had drifted, each
costing a build round-trip to discover:
- TEXTBOX/TEXTAREA/COMBOBOX/DATEPICKER/CHECKBOX were documented with
`Binds:`, which the parser rejects outright ("'Binds:' is no longer
supported, use 'Attribute:' instead").
- `DataSource: MICROFLOW Module.MF()` — a zero-argument microflow
DATASOURCE takes no parentheses, unlike RETRIEVE/CALL where they are
normal. The parser errors at the `)`.
Both verified against the parser before and after.
A table-driven test now fails if either spelling reappears in any topic's
Syntax or Example field. It is a spelling guard rather than a parse: the
snippets are fragments (a DATAVIEW body, a property line) that do not
stand alone as statements. Proven by reintroducing `Binds:` and watching
the test name the topic and field.
A third claim in the same report did not reproduce — a CONTAINER with
`OnClick: SHOW_PAGE M.Page(Param: $currentObject)` parses fine on current
main — so it was left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #105 (merged): the four commits that landed after its merge point. Mostly the bootstrap prompt, plus one runtime fix that came out of reviewing it.
The bootstrap prompt asks what the app is (docs-site/src/tools/bootstrap-prompt.md)
It hardcoded
App, so every repo bootstrapped from it gotApp.mprregardless of what was being built — and that name is the.mprfile name, the Studio Pro app name, and the path baked into the SessionStart hook, so it is awkward to change afterwards.A Step 0 now asks for the shape (one app or a solution), the name, what the app is for, what it keeps track of, who logs in, the theme and the Mendix version — one message, defaults offered, and the agent is told not to block twice. The answers become a brief written to
README.mdand committed, so a session resuming after an idle reap knows what it is building. A closing step has the agent propose the model as readable MDL and wait, instead of inventing one.Two corrections found while rewriting, both verified rather than assumed:
mxcli new App" cannot work.mxcli newrefuses a non-empty directory and.gitalone trips it; with no--output-dirit creates./App/, leaving every later-p App.mprwrong by one directory. Confirmed by running it in an empty git repo. The prompt now creates in a subfolder and moves the contents up, which is what the SessionStart hook's relative path needs.MyFirstModule, not the app name, so the closing step asks for a module name explicitly.Multi-app solutions
The prompt assumed one app at the repo root — fine for the common case, wrong for a backend that owns the data plus a frontend that consumes it. Added as deltas rather than a second prompt: per-app subfolders, explicit ports for the second app (8180/8190/6643 — 8081/8091/6544 belong to
mxcli test --local), and--hub-solutionso previews group. Databases need no action, since the name derives from the.mprfile name.Two things the agent could not have inferred:
mxcli initdedupes the SessionStart hook on the command, not on the project, so a second app never gets its own entry — and Claude Code reads the root.claude/settings.json, not the per-app ones. The root file has to be written by hand, one line per app.CREATE ODATA CLIENTfetches$metadataat creation time and caches it, warning rather than failing when unreachable. So the producer must be published and running before the consumer is wired, andServiceUrlbelongs in a constant since it will not staylocalhost.Default Mendix version 11.6.3 → 11.13.0, with a note on what "newest supported" means. Confirmed
mxbuild-11.13.0.tar.gzandmendix-11.13.0.tar.gzboth serve 200 and 11.14.0 serves neither.Each app gets its own host name
Separating a solution's apps by port alone does not separate their sessions: cookies key on host name and ignore the port, so
localhost:8080andlocalhost:8180share one jar and a login to one can replace the other'sXASSESSIONID.Distinct hostnames fix it and need no mxcli change — worth stating explicitly, because the obvious alternative (one loopback IP per app on a shared port) is not possible: the runtime binds
127.0.0.1and there is no listen-address flag. Verified against a booted app: a foreignHostheader is served 200 via/etc/hosts, nip.io and localtest.me alike, and the client uses relative URLs./etc/hostsis recommended over public wildcard DNS for locked-down containers, wherelocaltest.meresolves to::1.run --localhonours the configured Application root URLThe project's own configuration is where a custom host name belongs — versioned with the app, per configuration, no flag to repeat:
run --localignored it. The boot payload'sApplicationRootUrlcame only from a--hubregistration, and sinceupdate_configurationreplaces rather than merges, nothing else could supply it. It is now read at boot and used when no hub URL was assigned (a hub assignment wins, being the URL actually serving the app).Serving under a host name works without this; what it fixes is the absolute URLs Mendix generates — OIDC/SAML redirect URIs, deep links — which kept naming the listen address.
The trap: a blank Mendix app already ships
ApplicationRootUrl = http://localhost:8080/, so "is set" does not mean "was chosen". Honouring every value would change behaviour for every existing project and, under--app-port, advertise a port the app is not serving on. Only a non-loopback host is passed through; a port disagreeing with--app-portwarns.Validation
go build ./...and the affectedcmd/mxcli/dockertests pass on the rebased baseTestApplicationRootURLFrom(Default wins wherever it sits, case-insensitive, first-with-a-URL otherwise) andTestCustomHostRootURL(including the stocklocalhost:8080value)backend.localconfigured, the boot prints the configuration it came from and the app answers 200 on both the host name and the listen addressmake check-skill-mdlpassed on the docs changes before the rebase; a fullmake buildsweep has not been re-run since and is left to CI🤖 Generated with Claude Code
https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Generated by Claude Code