Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m

## Unreleased

- fix(doctor, exec, page-scope, rehost, wf-add-path-terminators, module-close): **mxcli v0.29 compatibility, and doctor advises the latest mxcli**: on v0.29.0 (which `--install` fetches by default) four instruments read new output wrong. (1) `fix property-sets` prints `MIXED` rows and exits 1: doctor reported "could not classify" (rc 4, shown as info by sync) — now result `mixed`, a WARN naming the rows and saying the repair cannot clear them; (2) the repair also exits 1 while a MIXED name remains, so `--fix-property-sets` rolled back a repair that had landed — the apply step now passes when only MIXED is left (FAILED still fails); (3) `fix hashes` now verifies MPR v1 with new text, which exec.sh read as an error, so every `--patch` on a Mendix < 10.18 project went unverified — both v1 forms are read; (4) since v0.28 `SHOW MODULES` / `list modules` write `Protected AddOn|Solution` in Source, so a first-party module set to Protected dropped out of page-scope (FAULT, 0 modules) and out of the rehost module list — the export level no longer counts as an origin. `wf-add-path-terminators.py` refuses below Mendix 10.21 (the terminator type does not exist there; written in, the project will not load), module-close notes that v0.29 `docker check` checks the stored project (a CE0463 it now reports is real), and the stale "stay on v0.24" pin advice is replaced. **New:** the normal doctor run compares the project mxcli with the latest release (redirect of the asset URL, 5 s, nothing downloaded): WARN with release link and upgrade path when behind, a note when `MXCLI_VERSION` pins it, one quiet note offline, never a FAIL; `MXTK_NO_LATEST_CHECK=1` skips it. Golden input captured from the real v0.28.0/v0.29.0 binaries on the mxpatch fixture model and mutated copies (CAPTURE files); against master the new cases fail (exec-hash-verify 4, property-sets 7, mxpatch 1, page-scope-protected 3, rehost-list-modules 1, doctor-mxcli-latest 8). Field run: page-scope and doctor against real v0.29.0/v0.28.0 binaries in a cloud session — mxcli v0.29 audit (2026-10-30)
- fix(skills): **skill text no longer tells agents that mxcli v0.25–v0.29 cannot do things it now does**: STOP 21 said `ALTER NAVIGATION` does not exist and restatement is the only write — it now points at `alter navigation`/`alter menu` (v0.28, mendixlabs/mxcli#1522) and in-loop `alter microflow`/`replace body of` (v0.29, ako/mxcli#1693), keeping its describe-first rule; STOP 1b now writes the bare `Editable: <expr>` (v0.25) and limits the quoted-form trap to mxcli < v0.28 (#1567); `learned-mdl-cannot-express.md` marks file upload (`filemanager`, v0.27), tab `Badge:` (v0.29), app title (v0.29), role visibility `Visible: roles(…)` (v0.27) and `sidebartoggle` (v0.28) buildable from their version; four `learned-detection-gaps.md` rows (terminators, `if` without `else` in a handler, `Class` on a snippet call, property sets below 10.24.24) and `platform-link.md` now name the mxcli version that fixed them; three passages no longer assume mxcli auto-creates `<Module>.User` (gone since v0.27, #1474). `mdl-scan` STOP-1 now also catches the bare `Visible:/Editable: <expr>` form inside a customContent column (it matched only `[`), since STOP 1b now recommends that form. Nothing deleted: old-version guidance stays, version-gated, because nothing was re-probed. Baseline 64,193 → 64,252 words. Source: mxcli v0.29 compatibility audit 2026-10-30
- fix(wire-company-brain): **re-wiring a company brain works on macOS, and a rewrite that fails no longer reports "Updated"**: the block was handed to awk as `-v blk=…`, which BSD awk (macOS) refuses when the value holds a newline, so every re-run (a moved brain, a changed block) left the files untouched while printing `Updated: … rewritten in place`; the block now goes through the environment (`ENVIRON["BLK"]`), and a failed rewrite or append prints `FAILED: …`, leaves the file alone and is not counted. Pinned by `tests/wave2/test-company-brain.sh` T2c (a stub awk with BSD's `-v` rule; 4 of its 5 checks fail on 2ba8158) — reported in #598 from a field-run project, macOS 26 / BSD awk 20200816
- fix(doctor, sync-project, speed-setup): **doctor says when the speed fixes never reached a project**: `bin/exec.sh`, `bin/_common.sh` or `bin/lint-gate.sh` without the 2026-10-29 speed code (marker per file) gives a Machine health warning "speed fixes not active", and the Recommended fix leads with the exact `sync-project.sh --upgrade-bin ...` command (exec.sh always with its `_common.sh`; each copy backed up) before any other advice, because every other number measures the old scripts; `sync-project.sh` names the speed fixes on a hand-edited exec.sh / _common.sh that has the exit-code fix but not the speed work; `skills/speed-setup.md` has Claude show each local edit in one line and ask before replacing. Field: an English colleague's ExpenseTracker analysis after the toolkit update still showed a full mxbuild on every change (45.5 min, 57% of all exec time) with 15 drifted scripts in bin/; reproduced here with the real pre-#587 exec.sh / _common.sh / lint-gate.sh — doctor names all three, sync names exec.sh and refreshes the two unedited ones — Maurits (mauritscgvisser)
Expand Down
103 changes: 86 additions & 17 deletions bin/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,18 @@ set -u
# script and the repair inherits exec.sh's snapshot, mxbuild gate and restore.
if [ "${2:-}" = "--property-sets-apply-step" ]; then
{ [ -n "${3:-}" ] && [ -x "$3" ]; } || { echo "--property-sets-apply-step: no runnable mxcli given" >&2; exit 2; }
exec "$3" fix property-sets -p "$1"
# mxcli v0.29.0+ also exits 1 while a MIXED name remains (cmd_fix_property_sets.go: "Exits 1
# when a unit could not be repaired, when a MIXED name remains"), even when every repair it made
# landed. mxcli leaves MIXED units untouched, so that exit is not a failed repair: report 0 and
# let the gate decide, else exec.sh --patch rolls the good repairs back. FAILED still exits 1.
_psa_o="$("$3" fix property-sets -p "$1" 2>&1)"; _psa_r=$?
printf '%s\n' "$_psa_o"
if [ "$_psa_r" -eq 1 ] && printf '%s\n' "$_psa_o" | grep -q '^ MIXED ' \
&& ! printf '%s\n' "$_psa_o" | grep -qE '^ FAILED |could not be normalized'; then
echo "(exit 1 was for the MIXED name(s) above only — mxcli leaves those as they are; the repair stands)"
exit 0
fi
exit "$_psa_r"
fi

TOOLKIT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
Expand Down Expand Up @@ -201,6 +212,10 @@ probe_runs() {
# What the dry-run prints and how it exits is mxcli's cmd/mxcli/cmd_fix_property_sets.go at
# v0.26.0: exit 0 = nothing to do; exit 1 = a ` NEEDS ` or ` FAILED ` row per unit, OR an
# error such as "project not found" (both exit 1, so the rows decide, never the code alone).
# v0.29.0 adds ` MIXED ` rows (ako/mxcli#1505): a property name some elements of a type carry and
# others lack — Mendix's merge engine cannot compare those revisions, and mxcli leaves them alone
# (no measured value fills them). Exit 1 with only MIXED rows is result "mixed": a merge hazard
# the repair cannot clear, reported, never "could not classify" (captured 2026-10-30).
# Anything else is "could not classify", never clean. Support is read from `mxcli fix --help`
# listing the subcommand: `mxcli fix property-sets --help` exits 0 on v0.24/v0.25 too (cobra
# prints the parent's help for an unknown subcommand), so it cannot be the probe. Golden
Expand All @@ -209,7 +224,7 @@ probe_runs() {
# The result is recorded in <project>/.claude/.property-sets-receipt (machine-local, like the
# doctor receipt): line 1 "<date> <time> <result> <detail>", line 2 "mxcli: <--version line>".
# Line 2 is what --if-stale and --quick compare: the check is owed again when mxcli changes.
# Only a settled answer is recorded — clean, unproven, needs, failed, or unsupported (the support probe
# Only a settled answer is recorded — clean, unproven, needs, failed, mixed, or unsupported (the support probe
# ran and the subcommand is absent). An error or a timeout is never written, and --if-stale
# skips the run only on a recorded clean/unproven/unsupported, so a one-off failure (locked model,
# unreadable version, a probe that crashed) is re-checked on the next sync instead of being
Expand Down Expand Up @@ -250,19 +265,23 @@ ps_bounded() {
rm -f "$_pb_f"
return "$_pb_rc"
}
# ps_classify <exit> <output> — sets PS_RESULT (clean|needs|failed|error) and PS_DETAIL.
# ps_classify <exit> <output> — sets PS_RESULT (clean|needs|failed|mixed|error) and PS_DETAIL.
ps_classify() {
_psc_checked="$(printf '%s\n' "$2" | grep -m1 -E '^Checked [0-9]+ unit' || true)"
_psc_needs="$(printf '%s\n' "$2" | grep -c '^ NEEDS ' || true)"
_psc_failed="$(printf '%s\n' "$2" | grep -c '^ FAILED ' || true)"
_psc_mixed="$(printf '%s\n' "$2" | grep -c '^ MIXED ' || true)"
_psc_mx=""; [ "$_psc_mixed" -gt 0 ] && _psc_mx=", $_psc_mixed MIXED property name(s) mxcli leaves alone"
if [ "$1" -eq 0 ] && printf '%s\n' "$2" | grep -q 'no readable Mendix version'; then
PS_RESULT=error; PS_DETAIL="mxcli read no Mendix version from the model, so nothing was checked"
elif [ "$1" -eq 0 ]; then
PS_RESULT=clean; PS_DETAIL="${_psc_checked:-exit 0}"
elif [ "$1" -eq 1 ] && [ "$_psc_needs" -gt 0 ]; then
PS_RESULT=needs; PS_DETAIL="$_psc_needs unit(s) need their property set completed, $_psc_failed cannot be normalized"
PS_RESULT=needs; PS_DETAIL="$_psc_needs unit(s) need their property set completed, $_psc_failed cannot be normalized$_psc_mx"
elif [ "$1" -eq 1 ] && [ "$_psc_failed" -gt 0 ]; then
PS_RESULT=failed; PS_DETAIL="$_psc_failed unit(s) hold a value in a key this Mendix version does not declare"
PS_RESULT=failed; PS_DETAIL="$_psc_failed unit(s) hold a value in a key this Mendix version does not declare$_psc_mx"
elif [ "$1" -eq 1 ] && [ "$_psc_mixed" -gt 0 ]; then
PS_RESULT=mixed; PS_DETAIL="$_psc_mixed property name(s) differ between elements of one type (MIXED) — no measured value fills them"
else
PS_RESULT=error
PS_DETAIL="exit $1: $(printf '%s\n' "$2" | grep -v -e '^[[:space:]]*$' -e '^WARNING: This is a' | head -1)"
Expand All @@ -289,7 +308,7 @@ ps_unproven_warn() {
note "the proof is Mendix's own: mx diff <base>.mpr <mine>.mpr out.json with this version's mx; exit 0 is clean."
}
ps_write_receipt() {
case "$PS_RESULT" in clean|unproven|needs|failed|unsupported) ;; *) return 0 ;; esac
case "$PS_RESULT" in clean|unproven|needs|failed|mixed|unsupported) ;; *) return 0 ;; esac
[ -n "$PS_VER" ] || return 0
[ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR" ] || return 0
mkdir -p "$PROJECT_DIR/.claude" 2>/dev/null || return 0
Expand Down Expand Up @@ -341,14 +360,16 @@ ps_check() {
else
ok "property sets: $PS_DETAIL — every unit has the property names Studio Pro writes"
fi ;;
needs|failed)
needs|failed|mixed)
warn "property sets: $PS_DETAIL — Studio Pro's merge/diff can throw on these"
printf '%s\n' "$PS_OUT" | grep -E '^ (NEEDS|FAILED) ' | head -10 | while IFS= read -r l; do note "$l"; done
_psk_rows="$(printf '%s\n' "$PS_OUT" | grep -cE '^ (NEEDS|FAILED) ' || true)"
printf '%s\n' "$PS_OUT" | grep -E '^ (NEEDS|FAILED|MIXED) ' | head -10 | while IFS= read -r l; do note "$l"; done
_psk_rows="$(printf '%s\n' "$PS_OUT" | grep -cE '^ (NEEDS|FAILED|MIXED) ' || true)"
[ "$_psk_rows" -gt 10 ] && note " … $((_psk_rows - 10)) more"
[ "$PS_RESULT" = needs ] && note "repair (snapshot + mxbuild gate + restore): bin/doctor.sh --fix-property-sets ${PROJECT_DIR:-<project>}"
[ "$(printf '%s\n' "$PS_OUT" | grep -c '^ FAILED ' || true)" -gt 0 ] && \
note "FAILED units are left alone by mxcli and make the guarded repair roll back — resolve them first." ;;
note "FAILED units are left alone by mxcli and make the guarded repair roll back — resolve them first."
[ "$(printf '%s\n' "$PS_OUT" | grep -c '^ MIXED ' || true)" -gt 0 ] && \
note "MIXED names are left alone by mxcli and --fix-property-sets cannot clear them. Report them with your Mendix version (mendixlabs/mxcli asks for exactly that); until a value is measured, a merge touching these units can throw." ;;
*) warn "property sets: could not classify the check — $PS_DETAIL (not recorded; the next run re-checks)"
printf '%s\n' "$PS_OUT" | grep -v '^[[:space:]]*$' | head -5 | while IFS= read -r l; do note " $l"; done ;;
esac
Expand All @@ -375,7 +396,7 @@ ps_apply() {
ps_check
}
ps_exit_code() {
case "$PS_RESULT" in clean|unproven) echo 0 ;; not-run) echo 2 ;; unsupported) echo 3 ;; needs|failed) echo 1 ;; *) echo 4 ;; esac
case "$PS_RESULT" in clean|unproven) echo 0 ;; not-run) echo 2 ;; unsupported) echo 3 ;; needs|failed|mixed) echo 1 ;; *) echo 4 ;; esac
}

# --property-sets / --fix-property-sets: only this check, then exit. Stands alone so
Expand Down Expand Up @@ -409,7 +430,7 @@ if [ -n "$PS_MODE" ]; then
PS_VER="$(ps_version "$PMXCLI_PROBE")"
if [ -n "$PS_VER" ] && ! ps_stale; then
PS_RESULT="$(sed -n '1s/^[^ ]* [^ ]* \([^ ]*\).*/\1/p' "$(ps_receipt)")"
# Only a settled "nothing to do here" short-circuits; needs/failed re-check every time.
# Only a settled "nothing to do here" short-circuits; needs/failed/mixed re-check every time.
case "$PS_RESULT" in
clean|unsupported)
ok "already checked for $PS_VER: $(sed -n 1p "$(ps_receipt)")"
Expand All @@ -425,6 +446,53 @@ if [ -n "$PS_MODE" ]; then
exit "$(ps_exit_code)"
fi

# mxcli_latest_advice <version line> — installed vs the latest mxcli release. Advice only: never a
# FAIL, never blocks, never downloads. A pin ($MXCLI_VERSION) is respected — noted, not nagged.
# Latest is read from the redirect of releases/latest/download/<asset> (one request, not followed,
# 5 s cap); offline or no curl is one quiet note. MXTK_MXCLI_LATEST=vX.Y.Z supplies the
# answer without the network (fixtures, air-gapped machines); MXTK_NO_LATEST_CHECK=1 skips it.
# MXTK_MXCLI_FLOOR is the oldest release this toolkit's instruments are checked against.
MXTK_MXCLI_FLOOR="${MXTK_MXCLI_FLOOR:-v0.28.0}"
mxtk_semver_lt() { # <a> <b> — 0 when vA.B.C < vX.Y.Z; 1 otherwise or unparseable
printf '%s %s\n' "${1#v}" "${2#v}" | awk '{
n = split($1, a, "."); m = split($2, b, ".")
if (n < 3 || m < 3) exit 1
for (i = 1; i <= 3; i++) { if (a[i] + 0 < b[i] + 0) exit 0; if (a[i] + 0 > b[i] + 0) exit 1 }
exit 1 }'
}
mxcli_latest_advice() {
[ "${MXTK_NO_LATEST_CHECK:-0}" = 1 ] && return 0
_ml_have="$(printf '%s\n' "$1" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
[ -n "$_ml_have" ] || return 0
_ml_new="${MXTK_MXCLI_LATEST:-}"
if [ -z "$_ml_new" ] && command -v curl >/dev/null 2>&1; then
# The redirect of the asset URL --install itself uses (not followed: nothing is downloaded;
# -q first, so a ~/.curlrc with `location` cannot make curl follow it and fetch ~100 MB).
# Not releases/latest: a proxy that allows release downloads can refuse that page (403,
# measured in a cloud sandbox 2026-10-30, where this redirect answered v0.29.0).
_ml_new="$(curl -q -sS --max-time 5 -o /dev/null -w '%{redirect_url}' \
https://github.com/mendixlabs/mxcli/releases/latest/download/mxcli-linux-amd64 2>/dev/null \
| sed -n 's|.*/releases/download/\(v[0-9][0-9.]*\)/.*|\1|p')"
fi
if [ -z "$_ml_new" ]; then
note "latest mxcli release: not checked (offline, or no curl) — installed $_ml_have"
return 0
fi
if ! mxtk_semver_lt "$_ml_have" "$_ml_new"; then
ok "mxcli $_ml_have is the latest release"
elif [ -n "${MXCLI_VERSION:-}" ]; then
note "mxcli pinned by MXCLI_VERSION=$MXCLI_VERSION; latest is $_ml_new — move when the team chooses"
else
warn "mxcli $_ml_have is behind the latest release $_ml_new (advice only — nothing is blocked)"
note "what's new: https://github.com/mendixlabs/mxcli/releases/tag/$_ml_new"
note "upgrade: move the old binary aside (mv mxcli mxcli.$_ml_have), then bin/doctor.sh --install ${PROJECT_DIR:-<project>}"
note "to stay on $_ml_have deliberately: set MXCLI_VERSION=$_ml_have and this becomes a note"
fi
mxtk_semver_lt "$_ml_have" "$MXTK_MXCLI_FLOOR" && \
note "this toolkit's instruments are checked against $MXTK_MXCLI_FLOOR and later; $_ml_have output may not parse"
return 0
}

# recommended_fix — ONE block that ends both the normal run and --bench: the first option that works on THIS
# machine, in this order. Read-only, cheap probes; never installs anything and never asks for IT's help.
# 0. MH_STALE (machine_health): the project runs older local copies of the speed scripts -> upgrade them
Expand Down Expand Up @@ -1333,10 +1401,10 @@ if [ "$INSTALL" -eq 1 ]; then
# 2026-08-31): mxcli-{darwin|linux}-{amd64|arm64}, mxcli-windows-amd64.exe. Windows gets
# the .exe name so Git Bash's transparent foo -> foo.exe mapping serves the project
# convention ./mxcli unchanged. $MXCLI_VERSION pins a release tag (e.g. v0.16.0) for
# teams that standardise; default is latest. v0.25.0 froze `mdl 1`: DESCRIBE output and
# `-c` input changed shape (navigation, DynamicClasses, `show entity` refused). The
# toolkit's instruments read both shapes, so a project mid-build may stay on its version
# (MXCLI_VERSION=v0.24.0) and move when it chooses.
# teams that standardise; default is latest. The toolkit's instruments are checked against
# $MXTK_MXCLI_FLOOR (v0.28.0) through v0.29.0 (golden captures, 2026-10-30); a project
# mid-build may stay on its version (MXCLI_VERSION=<its ./mxcli --version>) and move when
# it chooses. The normal doctor run says when the installed mxcli is behind the latest.
# Git Bash on Windows ARM64 runs emulated, so `uname -m` says x86_64 there. On Windows the
# real hardware shows in the `uname -s` suffix (...-ARM64) and in PROCESSOR_ARCHITEW6432
# (set for emulated processes) / PROCESSOR_ARCHITECTURE; those win over `uname -m`.
Expand Down Expand Up @@ -1395,7 +1463,7 @@ if [ "$INSTALL" -eq 1 ]; then
note " $_STEP. mxcli (~90 MB) -> $PROJECT_DIR/$MXCLI_DEST"
note " the MDL CLI every session here uses; from $MXCLI_URL"
[ -z "${MXCLI_VERSION:-}" ] && note " (latest release — set MXCLI_VERSION=vX.Y.Z to pin the team's version;"
[ -z "${MXCLI_VERSION:-}" ] && note " a project already building on v0.24 can stay there: MXCLI_VERSION=v0.24.0)"
[ -z "${MXCLI_VERSION:-}" ] && note " a project mid-build can stay on its version: MXCLI_VERSION=<its ./mxcli --version>)"
fi
if [ "$NEED_TOOLCHAIN" -eq 1 ]; then
_STEP=$((_STEP + 1))
Expand Down Expand Up @@ -1855,6 +1923,7 @@ if [ -n "$PROJECT_DIR" ]; then
if [ "$MXCLI_EXIT" -eq 0 ]; then
ok "mxcli runs ($PROBE_HOW): $(probe_line)"
note "at: $PMXCLI_PROBE"
[ "$QUICK" != 1 ] && mxcli_latest_advice "$(probe_line)"
else
bad "mxcli exists but cannot run (exit $MXCLI_EXIT): $PMXCLI_PROBE"
printf '%s\n' "$PROBE_OUT" | grep -v '^[[:space:]]*$' | head -3 | while IFS= read -r l; do note " $l"; done
Expand Down
Loading
Loading