Skip to content

Bootstrap prompt: interview first, multi-app solutions, and honour the configured app host name - #106

Merged
ako merged 8 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h
Aug 7, 2026
Merged

Bootstrap prompt: interview first, multi-app solutions, and honour the configured app host name#106
ako merged 8 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h

Conversation

@ako

@ako ako commented Aug 7, 2026

Copy link
Copy Markdown
Owner

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 got App.mpr regardless of what was being built — and that name is the .mpr file 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.md and 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:

  • "Create the app at the repo root: mxcli new App" cannot work. mxcli new refuses a non-empty directory and .git alone trips it; with no --output-dir it creates ./App/, leaving every later -p App.mpr wrong 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.
  • The blank template's module is 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-solution so previews group. Databases need no action, since the name derives from the .mpr file name.

Two things the agent could not have inferred:

  • mxcli init dedupes 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 CLIENT fetches $metadata at creation time and caches it, warning rather than 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. Confirmed mxbuild-11.13.0.tar.gz and mendix-11.13.0.tar.gz both 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: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 — 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.1 and there is no listen-address flag. Verified against a booted app: a foreign Host header is served 200 via /etc/hosts, nip.io and localtest.me alike, and the client uses relative URLs. /etc/hosts is recommended over public wildcard DNS for locked-down containers, where localtest.me resolves to ::1.

run --local honours the configured Application root URL

The project's own configuration is where a custom host name belongs — versioned with the app, per configuration, no flag to repeat:

alter settings configuration 'Default'
  ApplicationRootUrl = 'http://backend.local:8080/';

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).

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-port warns.

Validation

  • go build ./... and the affected cmd/mxcli/docker tests pass on the rebased base
  • New unit tests: TestApplicationRootURLFrom (Default wins wherever it sits, case-insensitive, first-with-a-URL otherwise) and TestCustomHostRootURL (including the stock localhost:8080 value)
  • End-to-end on Mendix 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
  • make check-skill-mdl passed on the docs changes before the rebase; a full make build sweep 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

claude added 6 commits August 7, 2026 10:21
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
ako force-pushed the claude/mxbuild-diagnostics-spike-emta6h branch from c501fde to a87de0a Compare August 7, 2026 11:19
claude added 2 commits August 7, 2026 11:41
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
@ako
ako merged commit 9236202 into main Aug 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants