Skip to content
Closed
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 @@ -17,6 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are.

## Unreleased

- fix(sync): **`sync-project.sh` no longer exits 1, silently, before its `bin/` and `tests/e2e/` refresh.** The CLAUDE.md duplicate-routing check (section 3) looked up the "mxcli-project-toolkit Integration" heading with an unguarded `grep | head | cut` under `set -euo pipefail`; when CLAUDE.md cites `skills/query-the-model.md` or `skills/learned-mdl-preflight.md` without that heading, which is every CLAUDE.md mxcli init v0.25 writes, grep's exit 1 ended the script with no message. Every step after it never ran: the crash-net refresh, the tests/e2e install and the lint-rule restore. Introduced by 9a0350b (2026-09-18). Now `|| true`, and `ledger_row_prefix` gets the same guard. Field: three consuming projects of one organisation (2026-10-07): the dry run went from exit 1 after "Would rewrite: CLAUDE.md" to exit 0 listing 7 and 8 skipped `bin/` refreshes. New fixture `tests/wave2/test-sync-dup-heading-exit.sh` (4 of 6 fail on the pre-fix script). — MendixMau (field run, existing apps)
perf/faster-ui-loop
- perf(ui-loop): **fewer mxbuilds, no fixed sleeps, legible screenshots.** Field run 2026-10-07 (a requirements-driven build): `exec.sh` was 22% of tool time at two mxbuilds per exec, journeys and page waits slept a fixed time, and screenshots were 11.5% of re-read tokens. `exec.sh` now skips the pre-flight baseline mxbuild when the model is byte-identical to its last *clean* pass: the verification stamp gains an `errors:` field (`model-stamp.sh check --clean` accepts only `errors: 0`, so a delta-gate pass over pre-existing errors never counts; `SKIP_BASELINE=0` is unchanged). New `tests/e2e/settle.js` waits for the page's own state (no visible Mendix progress/loading indicator, DOM size stable) instead of a fixed time; `journey-runner.js` uses it after each action (an explicit `settleMs` still wins) and `page-audit.js` per page. Probed in a real browser: on a page that loads in 1.8 s the old 1200 ms sleep and networkidle+600 both captured it half-loaded, settle waited for it; on a fast page settle took 775 ms vs 1200. `SETTLE_MODE=fixed` restores the old waits for an A/B run. `page-audit.js` also saves a viewport shot `page-audit-<Page>.top.png` beside the full-page one, and `ui-loop.md` says to look at viewport shots: a tall full-page shot reaches the model at ~1024 px high, text unreadable. `shrink-image-read.sh` now works off macOS (ImageMagick or Pillow, python when jq is absent; it was a silent no-op without `sips`). — MendixMau
- perf(ui-loop): **a stale app reloads instead of restarting, journeys stop sleeping, worst pages are looked at first.** Field run 2026-10-07, existing app. `test-stack-up.sh` records which model the running app was built from (`.claude/loop/served-model`); when the model has changed since, it hot-reloads with `mxcli docker reload` (adding `--skip-check` when the exec.sh gate already passed this model) and restarts only if that fails: 93 s instead of 179 s. `ui-loop.md` and the script header carry the measured table and point at `mxcli run --local --watch` as the fast loop (~18 s per page change). `journey-runner.js` waits on page state after a combobox action instead of fixed pauses, and a combobox step now opens the widget it names: it used to open the page's first combobox, so on a form with three every step but the first failed. `page-audit.js` clicks the first copy of a nav item a user could actually click, not just the first match: with the sidebar collapsed, every in-nav page faulted while the same item sat in the top menu bar. It also writes `page-audit-look-order.txt`, all pages worst first (`module-review.md` §4a: order, not selection, every page is still looked at). `design-audit.js` drops its fixed 1.2 s wait. Same 7-step journey on the same app: 56 s with `SETTLE_MODE=fixed`, 24 s with settle, 11/11 pass both ways, 3 runs each. — MendixMau
Expand Down
11 changes: 9 additions & 2 deletions bin/sync-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,10 @@ fi
LEDGER_ROW_RE='bug-logs/mxcli-bugs\.md'
# ledger_row_prefix <file> — the path prefix the retired row's cell carried ('' when relative).
ledger_row_prefix() {
grep -m1 "^|.*$LEDGER_ROW_RE" "$1" | sed -n 's/.*[|` ]\([^|` ]*\)bug-logs\/mxcli-bugs\.md.*/\1/p'
# `|| true`: both callers only reach here after a grep for the same row matched, but a
# no-match under pipefail + errexit would end the whole sync with no message (see the
# _dup_from_line note in section 3) — never leave that to the callers.
grep -m1 "^|.*$LEDGER_ROW_RE" "$1" | sed -n 's/.*[|` ]\([^|` ]*\)bug-logs\/mxcli-bugs\.md.*/\1/p' || true
}
LEDGER_ROWS_BEFORE=0
[ -f "$CL" ] && LEDGER_ROWS_BEFORE="$(grep -c "^|.*$LEDGER_ROW_RE" "$CL" 2>/dev/null || true)"
Expand Down Expand Up @@ -899,7 +902,11 @@ if [ -f "$PROJECT_DIR/CLAUDE.local.md" ]; then
# a flat assertion. Same rule as the ledger-row warn above: CLAUDE.md is bootstrap-project.md's
# (an LLM merge), not this script's, so sync never edits it — only names the fix.
if [ -f "$PROJECT_DIR/CLAUDE.md" ] && grep -qE 'skills/(learned-mdl-preflight|query-the-model)\.md' "$PROJECT_DIR/CLAUDE.md"; then
_dup_from_line="$(grep -n 'mxcli-project-toolkit Integration' "$PROJECT_DIR/CLAUDE.md" | head -1 | cut -d: -f1)"
# `|| true`: no heading is the COMMON case (every CLAUDE.md mxcli init v0.25 writes cites
# the skills without it). Unguarded, grep's exit 1 fails the pipeline under pipefail and
# errexit ended the whole sync here, silently, before the bin/ and tests/e2e refresh —
# every sync from 9a0350b (2026-09-18) to this fix. Regression: tests/wave2/test-sync-dup-heading-exit.sh.
_dup_from_line="$(grep -n 'mxcli-project-toolkit Integration' "$PROJECT_DIR/CLAUDE.md" | head -1 | cut -d: -f1 || true)"
if [ -n "$_dup_from_line" ]; then
_dup_words="$(tail -n "+$_dup_from_line" "$PROJECT_DIR/CLAUDE.md" | wc -w | tr -d ' ')"
else
Expand Down
65 changes: 65 additions & 0 deletions tests/wave2/test-sync-dup-heading-exit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Fixture for sync-project.sh exiting 1, silently, in its CLAUDE.md duplicate-routing check when
# CLAUDE.md cites a Baseline-only skill (skills/query-the-model.md) but has NO
# "mxcli-project-toolkit Integration" heading — the shape every CLAUDE.md written by mxcli init
# v0.25 has. `grep -n heading | head | cut` returned grep's exit 1 through pipefail, errexit ended
# the script, and every step after it (bin/ crash-net refresh, tests/e2e install, lint rules)
# never ran. Field: three consuming projects of one organisation, every sync since 9a0350b.
#
# Run against the pre-fix script and T1-T3 must fail (exit 1, no bin/ line, no summary).
#
# Every fixture is built by the REAL bin/init-project.sh, under /tmp. No real project is touched.
#
# usage: test-sync-dup-heading-exit.sh /path/to/bin/sync-project.sh
export MXTK_LEAKGUARD_DENYFILE="${TMPDIR:-/tmp}/mxtk-fixture-denylist.$$"
export MXTK_SYNC_SKIP_CLONE_CHECK=1

set -uo pipefail

SYNC="${1:?usage: test-sync-dup-heading-exit.sh /path/to/sync-project.sh}"
case "$SYNC" in /*) ;; *) SYNC="$PWD/$SYNC" ;; esac
TK="$(cd "$(dirname "$SYNC")/.." && pwd)"
INIT="$TK/bin/init-project.sh"
WORK="$(mktemp -d /tmp/dupheading.XXXXXX)"
trap 'rm -f "$MXTK_LEAKGUARD_DENYFILE"; rm -rf "$WORK"' EXIT
PASS=0; FAIL=0

ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ FAIL-detail: /'; }

mkproj() {
d="$WORK/$1"; mkdir -p "$d"; : > "$d/Fixture.mpr"
MXTK_NO_GUIDE=1 "$INIT" "$d" >/dev/null 2>&1
[ -f "$d/CLAUDE.local.md" ] && [ -f "$d/bin/exec.sh" ] || { echo "FIXTURE ERROR: init did not scaffold $d"; exit 2; }
echo "$d"
}

# --- no heading: the case that died -----------------------------------------------------
P="$(mkproj noheading)"
grep -q 'mxcli-project-toolkit Integration' "$P/CLAUDE.md" && { echo "FIXTURE ERROR: init's CLAUDE.md already has the heading"; exit 2; }
printf '\nRead skills/query-the-model.md before asking the user.\n' >> "$P/CLAUDE.md"
rm -f "$P/bin/exec.sh" # something the bin/ step, past the check, must report
OUT="$("$SYNC" "$P" --dry-run 2>&1)"; RC=$?
[ "$RC" -eq 0 ] && ok "T1 no heading: sync exits 0" || bad "T1 no heading: sync exits 0 (got $RC)" "$(printf '%s\n' "$OUT" | tail -5)"
printf '%s\n' "$OUT" | grep -q 'Would install: bin/exec.sh' \
&& ok "T2 no heading: the bin/ step past the check still runs" \
|| bad "T2 no heading: the bin/ step past the check still runs" "$(printf '%s\n' "$OUT" | tail -5)"
printf '%s\n' "$OUT" | grep -qE 'artifact\(s\) would be updated|All copied artifacts up to date' \
&& ok "T3 no heading: sync reaches its summary line" \
|| bad "T3 no heading: sync reaches its summary line"
printf '%s\n' "$OUT" | grep -q 'cites Baseline-only skills' \
&& ok "T4 no heading: the duplicate-routing warning is still raised" \
|| bad "T4 no heading: the duplicate-routing warning is still raised"

# --- heading present: the case the check was written for keeps working ------------------
P2="$(mkproj withheading)"
printf '\n## mxcli-project-toolkit Integration\n\nRead skills/query-the-model.md first.\n' >> "$P2/CLAUDE.md"
OUT2="$("$SYNC" "$P2" --dry-run 2>&1)"; RC2=$?
[ "$RC2" -eq 0 ] && ok "T5 heading: sync exits 0" || bad "T5 heading: sync exits 0 (got $RC2)"
printf '%s\n' "$OUT2" | grep -qE 'section \(~[0-9]+ word' \
&& ok "T6 heading: warning sizes the section" \
|| bad "T6 heading: warning sizes the section"

echo ""
echo "test-sync-dup-heading-exit: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]