From 00eb9530c6cf7ee6a252127a6c27321a8b03b562 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Thu, 8 Oct 2026 07:22:28 +0800 Subject: [PATCH 1/2] fix(sync): sync-project.sh no longer exits 1 silently before its bin/ refresh The CLAUDE.md duplicate-routing check looked up the "mxcli-project-toolkit Integration" heading with an unguarded grep | head | cut under set -euo pipefail. A CLAUDE.md that cites query-the-model.md or learned-mdl-preflight.md without that heading (every mxcli init v0.25 CLAUDE.md) made grep exit 1, and errexit ended the whole sync there: no bin/ crash-net refresh, no tests/e2e install, no lint-rule restore, no message. Introduced by 9a0350b (2026-09-18). Guard it with || true, and ledger_row_prefix too. New fixture tests/wave2/test-sync-dup-heading-exit.sh: 6/6 on this script, 4 of 6 fail on the pre-fix one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MpdPDrT1dsCR54JrgWmNso --- CHANGELOG.md | 1 + bin/sync-project.sh | 11 +++- tests/wave2/test-sync-dup-heading-exit.sh | 65 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100755 tests/wave2/test-sync-dup-heading-exit.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e37d24f0..61b81c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 (TramiusECI, TramiusSCR, TramiusSVC, 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). — TramiusECI session 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-.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 diff --git a/bin/sync-project.sh b/bin/sync-project.sh index 0b5be28c..d2cf58ae 100755 --- a/bin/sync-project.sh +++ b/bin/sync-project.sh @@ -752,7 +752,10 @@ fi LEDGER_ROW_RE='bug-logs/mxcli-bugs\.md' # ledger_row_prefix — 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)" @@ -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 diff --git a/tests/wave2/test-sync-dup-heading-exit.sh b/tests/wave2/test-sync-dup-heading-exit.sh new file mode 100755 index 00000000..4aaedd33 --- /dev/null +++ b/tests/wave2/test-sync-dup-heading-exit.sh @@ -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 (TramiusECI/SCR/SVC), 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 ] From d3f198535250ea14a3abee6f558efefaa48daf28 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:34:33 +0000 Subject: [PATCH 2/2] test(sync): neutral wording for the field evidence in the dup-heading fixture The fixture header named consuming projects by client name; the public toolkit cites field runs without naming clients. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- tests/wave2/test-sync-dup-heading-exit.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b81c44..28905e8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +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 (TramiusECI, TramiusSCR, TramiusSVC, 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). — TramiusECI session +- 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-.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 diff --git a/tests/wave2/test-sync-dup-heading-exit.sh b/tests/wave2/test-sync-dup-heading-exit.sh index 4aaedd33..504c0424 100755 --- a/tests/wave2/test-sync-dup-heading-exit.sh +++ b/tests/wave2/test-sync-dup-heading-exit.sh @@ -4,7 +4,7 @@ # "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 (TramiusECI/SCR/SVC), every sync since 9a0350b. +# 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). #