Repository navigation
Field runs 2026-10-15: #379 PASS, #384 PASS (sync --upgrade-bin not scoped; copy 87-134 s), #374 PASS (.bak in mprcontents) #390
Description
Field-run report for open PRs #379, #384 and #374. It ran on one real field project (Mendix 11.15.0, mxcli v0.26.0) on macOS arm64 with bash 3.2.
Verdicts:
- fix(lock): reap tickets close the stale-takeover race; one shared lock in _common.sh #379: PASS
- feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384: PASS (a, b). For c, the Ctrl-C half was NOT RUN
- mxpatch: shared patch core +
mxcli fix hashesverify around exec.sh --patch (#294, #295) #374: PASS
Findings that need action
sync-project.sh --upgrade-bin Xis not a scoped install. It still runs a full sync: 244 tracked files changed, a file was deleted, about 30 files were added and the git hooks were reinstalled. Any instruction or doc that uses it as "install just these scripts" is wrong. A--only-binmode, or a doc fix, would help (feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384 field step).- feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384
--on-copycopy cost. The copy took 86.7 s unloaded and 133.8 s under load, against a 107–138 ms swap, for a model of 549 units. That makes each--on-copyrun 4–7 minutes long on this machine. - feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384: a refusal because Studio Pro holds the model writes no BUILD-LOG row, while the dirty-model refusal does.
- feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384: the dirty-model guard runs before the Studio Pro guard, so a dirty model hides whether the Studio Pro refusal works. This is minor, but it confused the field test.
- mxpatch: shared patch core +
mxcli fix hashesverify around exec.sh --patch (#294, #295) #374: the patcher leaves<unit>.mxunit.bakinsidemprcontents/. It shows up as untracked in the model folder and is easy to commit by accident. - mxpatch: shared patch core +
mxcli fix hashesverify around exec.sh --patch (#294, #295) #374: the BUILD-LOG row carries no hash baseline or verify result. In this run the patch also repaired a mismatch that was already in the model, and that is visible only in stdout. - Field confirmation of mxpatch: shared patch core +
mxcli fix hashesverify around exec.sh --patch (#294, #295) #374's bug: committed HEAD of the field project had a ContentsHash MISMATCH on exactly the workflow unit that an old-style--patch(2026-10-02) had rewritten.
| origin/master | 484e7ff |
| macOS | 26.6.2, Darwin 25.6.0 arm64 (Darwin Kernel Version 25.6.0 … RELEASE_ARM64_T6031 arm64) |
| bash | GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) |
| mxcli | v0.26.0 (2026-10-11T18:37:07Z) (field project) |
| Mendix / Studio Pro | 11.15.0, MPR v2 (mprcontents) |
| Python | 3.14.4 |
| Field project | 1 real app with 549 units, 1 workflow, 27 pages; the git working tree was clean of tracked changes at the start |
| PR | Verdict |
|---|---|
| #379 lock race | PASS |
| #384 exec-on-copy | PASS for a and b. For c, only the no-op recovery half was run; the Ctrl-C half was NOT RUN (see below) |
| #374 mxpatch + fix hashes | PASS |
PR #379: lock reap tickets (fix/lock-reap-tickets @ 81981f2)
Commands, run in a worktree of the branch:
bash tests/wave2/bench-lock-race.sh
bash tests/wave2/bench-lock-race.sh --old
bash tests/wave2/test-lock-reap.sh project-bin/_common.sh
The three ran at the same time on the same machine.
RESULT subject=new rounds=200 contenders=10 overlaps=0 overlap_rounds=0 stolen=0 starved=0 probe_fail=0 kills=558 acquired=1642
exit=0
RESULT subject=old rounds=200 contenders=10 overlaps=21 overlap_rounds=9 stolen=18 starved=0 probe_fail=0 kills=552 acquired=1648
exit=1
PASS=45 FAIL=0 (test-lock-reap.sh, exit=0)
PASS.
- New code: overlaps=0, stolen=0, starved=0, probe_fail=0, exit 0.
--old: overlaps=21 across 9 rounds and 18 stolen locks, so the harness does catch the race on macOS with bash 3.2.- Fixture: 45/45.
PR #384: exec-on-copy (feat/exec-on-copy @ fff1cd5)
Install: deviated from the brief
Step 2 said to install with sync-project.sh <project> --upgrade-bin exec.sh --upgrade-bin _common.sh --upgrade-bin test-scratch.sh.
--upgrade-bin does not limit the sync to those scripts. It only allows overwriting a locally modified copy, and a full sync still runs. On the field project it:
- changed 244 tracked files: all of the skills dirs, CLAUDE.md, PROJECT.md, settings.json, .gitignore, 20 bin/ scripts and the tests/e2e engine
- deleted
.claude/vscode-mdl.vsix - added about 30 untracked files
- reinstalled 3 git hooks
- reported "2 warning(s) — this project is NOT fully in sync" (property-sets)
The model was not touched. I reverted with git checkout -- . and removed the files the sync had added; the hooks pass install-project-hooks.sh --check. Then I copied only exec.sh, _common.sh and test-scratch.sh from the PR into bin/. Every helper the new exec.sh calls that the project lacks (metrics-ledger.sh, app-restart.sh, doctor.sh, exec-approval.sh) is behind a -x guard and is skipped.
To put things back at the end I used git checkout (the project's bin/ scripts are committed), not the clone's sync-project.sh. The clone is checked out at d16f578, not origin/master, so its sync would have left different bin/ files behind.
a) Studio Pro closed: ./bin/exec.sh --on-copy <4-line ALTER PAGE … SET Visible script>
→ on-copy: the gate passed on the copy — promoting it to the live model...
✓ promoted by rename in 138 ms (copy took 133828 ms); scratch copy removed.
✓ verification stamp written (pass — exec.sh <script>.mdl)
✓ Script applied to <model>.mpr.
elapsed: 438s (check + snapshot + exec + mxbuild gate)
⚠ slower than a healthy run — run doctor.sh --quick …
exit=0
BUILD-LOG row:
| 2026-10-15T11:10:48+0800 | `<script>.mdl` | pass | ✅ applied | mxbuild clean (no errors file written); lint could not run (lint-gate exit 2) · change: ui-only · approval: auto (PROJECT.md) · on-copy: promoted (copy 133828 ms, swap 138 ms) |
PASS. The copy was built and checked, then promoted, with one BUILD-LOG row and exit 0.
Warts:
- Copy time: 133.8 s for a model of 549 units and about 94 KB of .mpr. The fix(lock): reap tickets close the stale-takeover race; one shared lock in _common.sh #379 benches were running at the same time. An unloaded repeat run (b2 below) took 86.7 s. Either way, copying costs far more than the swap (107–138 ms), and it makes
--on-copyruns 4–7 minutes long on this machine. - The script was an already-applied, supposedly idempotent
SET, but the re-run was not byte-identical: the page unit went from 39146 to 39212 bytes. This comes from mxcli, not the PR. - The
execon the copy warned1 unit(s) … do not match the .mpr's ContentsHash index. That mismatch was already in HEAD; see mxpatch: shared patch core +mxcli fix hashesverify around exec.sh --patch (#294, #295) #374.
b) Studio Pro holds the model
- b1: Studio Pro was opened, and the run was refused, but for the wrong reason. The uncommitted 66-byte change from (a) tripped the "Uncommitted model changes" guard first, so the Studio Pro check was never reached. The model was untouched, and a 🚫 blocked BUILD-LOG row was written.
- b2:
FORCE_EXEC=1 ./bin/exec.sh --on-copy …promoted (exit 0, copy 86659 ms, swap 107 ms). Investigation: Studio Pro had opened a different clone of the same app, there was no<model>.mpr.lockbeside the field model, and Studio Pro had no handle on the field project. So promoting was correct, and this was not a PR defect. - b3: To test the guard directly, I wrote
<model>.mpr.lockcontaining{"ProcessId":<pid of a live sleep>}(the formatsp_holds_modelreads) and ran both forms:
./bin/exec.sh --on-copy <script>
✗ Studio Pro has <model> open (lock PID 21940 alive) — refusing exec (split-brain corruption risk).
→ Close the project in Studio Pro (or quit SP), then re-run.
--on-copy / --recover-promote: FORCE_EXEC does not override this — a promotion is refused while Studio Pro holds the model.
exit=1
FORCE_EXEC=1 ./bin/exec.sh --on-copy <script>
(same three lines)
exit=1
git status and the model checksums were identical before and after, and no on-copy dir was left behind. PASS. It was refused up front, FORCE_EXEC did not override, and the live model was untouched. The lock was simulated, not written by a real Studio Pro.
Minor: this refusal writes no BUILD-LOG row, while the dirty-model refusal (b1) does.
c) Interrupted promotion
The Ctrl-C half was NOT RUN: the brief said "only if I say yes", and no yes was given. The no-op half was run:
./bin/exec.sh --recover-promote (twice)
Nothing to recover: no interrupted --on-copy promotion beside the model.
Both runs printed this.
After step 2
git checkout -- bin/exec.sh bin/_common.sh <model>.mpr mprcontents, plus removing bin/test-scratch.sh. git diff --stat:
.claude/loop/lint-last.json | 7 +++----
docs/BUILD-LOG.md | 3 +++
PR #374: mxpatch + fix hashes (feat/mxpatch-and-fix-hashes @ 01b9b42)
mxcli is v0.26.0, so the step applies. Install: the PR's exec.sh and _common.sh were copied into bin/ (not synced, for the reason above). The patcher ran from the PR tree, beside its mxpatch.py.
Choosing the patcher (both dry runs are read-only):
wf-add-path-terminators.py <mpr> Module.Workflow:paths found: 0 … No parallel split in this unit -- nothing to do.exit 0. It would write nothing, so I didn't use it.wf-set-call-captions.py: all 5 call-microflow captions had already been edited by hand. I picked one real write on 1 caption, with--expect-captiongiving its current value.
Field confirmation of the bug, before the run. Standalone mxcli fix hashes on committed HEAD:
Checked 549 unit(s) in <model>.mpr against 549 .mxunit file(s).
MISMATCH 74215fad-… mprcontents/74/21/74215fad-….mxunit [Workflows$Workflow <the workflow>]
1 mismatch(es), 0 missing file(s), 0 orphan file(s).
exit=1
It's the very workflow unit an old-style --patch (wf-add-outcome-persistent-ids.py, BUILD-LOG 2026-10-02) had rewritten without updating the index, which is exactly what this PR fixes.
Command:
./bin/exec.sh --patch <pr>/project-bin/wf-set-call-captions.py Module.Workflow \
--captions "Module.ACT_X=<caption> (field test)" --expect-caption "Module.ACT_X=<caption>" --apply
Output, trimmed:
→ ContentsHash baseline: mxcli fix hashes (read-only)...
⚠ the index is ALREADY out of step before this patch (1 mismatch(es), 0 missing file(s), 0 orphan file(s).).
Not this patch's doing, and not held against it. Repair it on its own (rewrites the
index from the files, never the files): ./mxcli fix hashes -p <model>.mpr --repair
→ Executing …/wf-set-call-captions.py...
captions to set: 1
note: the index was already stale for 74215fad-….mxunit before this patch (stored u2ZSfud+…); this write corrects it
wrote 74215fad-….mxunit (10580 bytes) and its ContentsHash W9sDEt6DRRMRsoFI3BbnEWecFM+1k7zX0+gpOrYPEgM=
backup: <project>/mprcontents/74/21/74215fad-….mxunit.bak
→ ContentsHash verify: mxcli fix hashes (read-only)...
✓ index matches every unit file
✓ mxbuild: 0 errors — model is clean.
✓ Script applied to <model>.mpr.
elapsed: 103s
exit=0
BUILD-LOG row:
| 2026-10-15T11:41:45+0800 | `patch: wf-set-call-captions.py` | pass | ✅ applied | mxbuild clean (no errors file written); lint could not run (lint-gate exit 2) · change: logic · approval: auto (PROJECT.md) |
Independent check afterwards:
mxcli fix hashesgave0 mismatch(es), 0 missing file(s), 0 orphan file(s).and exit 0.describe workflowread back the new caption.
PASS. No new hash issue, the pre-existing mismatch was repaired as a side effect, mxbuild reported 0 errors, and exit was 0.
Warts:
- The patcher leaves
<unit>.mxunit.bakinsidemprcontents/.fix hashesdoesn't count it, but git shows it as untracked inside the model folder, and agit add mprcontentswould commit it. - The BUILD-LOG row says nothing about the hash baseline or verify: not "baseline 1 mismatch, verify clean", and not that the patch repaired a mismatch that was already there. The evidence is only in stdout.
- The baseline message advises
fix hashes --repair, but the patch then fixed that unit anyway. That's fine, but the message could say the unit the patch touches will be rewritten with a fresh hash.
Restore: git checkout -- bin/exec.sh bin/_common.sh <model>.mpr mprcontents and removing the .bak. The model is back at HEAD, including its pre-existing mismatch.
Final state of the field project
git diff --stat, after both restores:
.claude/loop/lint-last.json | 7 +++----
docs/BUILD-LOG.md | 4 ++++
Model and bin/ are at HEAD. The only untracked files are the 10 that were there at the start. The 4 BUILD-LOG rows were kept as evidence.
All three worktrees were removed. Nothing was changed, committed or pushed in the toolkit clone (only git fetch).
Activity
mxpatch: keep unit backups outside mprcontents/ (field run #390)
exec.sh --patch: hash baseline/verify in the BUILD-LOG row, repaired-…
fix(exec): #390 field run — copy cost, Studio Pro refusal row, recove…
exec.sh --patch: a rewritten unit left stale again is a NEW issue (#374…
feat(sync-project): --only-bin installs just the named scripts (#396)
mxpatch: shared patch core + `mxcli fix hashes` verify around exec.sh…