Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a039864
docs(wrap-up): name the unfiled-bugs line and file-bugs.sh at wrap-up
claude Nov 12, 2026
c6a4fc7
fix(placeholders): count slots that span lines
claude Nov 12, 2026
7d0621e
fix(design-reaches-app): normalise CSS, whole-class and var-fallback …
claude Nov 12, 2026
e113e0d
fix(exec): print one MXTK-RESULT line
claude Nov 12, 2026
f812af0
docs(wrap-up): keep the pointer in the cutover checkpoint only; name …
claude Nov 12, 2026
9d9d93a
fix(changelog): design-reaches-app line under Unreleased, figures fro…
claude Nov 12, 2026
26bc1bb
fix(exec): MXTK-RESULT review fixes: failed on both restore paths, di…
claude Nov 12, 2026
9ed2425
fix(changelog): the timing is for the committed pair, not a sheet size
claude Nov 12, 2026
29ca2b7
fix(exec): the closing line no longer says 'restore' on paths that ne…
claude Nov 12, 2026
dc6bac9
fix(gate-check): state the multi-line placeholder decision; the lefto…
claude Nov 13, 2026
6a8fcb7
fix(test-stack-up): the local lane can start on a project that has no…
claude Nov 13, 2026
02c2b3e
fix(gate-check): print a long unfilled placeholder as {{NAME…}} in th…
claude Nov 13, 2026
b82bf73
fix(test-stack-up): auto never switches database by itself; JDK probe…
claude Nov 13, 2026
6d82cd0
fix(test-stack-up): only a missing JDK blocks; older or unreadable JD…
claude Nov 13, 2026
b621385
Merge remote-tracking branch 'origin/claude/loving-gates-hauc1n-w1-de…
claude Nov 13, 2026
4926f9c
Merge remote-tracking branch 'origin/claude/loving-gates-hauc1n-w1-re…
claude Nov 13, 2026
c827d09
Merge remote-tracking branch 'origin/claude/loving-gates-hauc1n-w1-pl…
claude Nov 13, 2026
765953b
Merge remote-tracking branch 'origin/claude/loving-gates-hauc1n-w1-lo…
claude Nov 13, 2026
717ef08
test(status-existing-app): fill wrapped placeholder slots in the T9d …
claude Nov 13, 2026
d550021
Merge remote-tracking branch 'origin/claude/loving-gates-hauc1n-w1-pl…
claude Nov 13, 2026
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m
`docs/RELEASE-PROCESS.md` §8.

## Unreleased
- fix(test-stack-up): **the local lane gets explicit database choices, probes its JDK before building (exit 4 only when no JDK is found at all; an older JDK than expected, an unreadable version or `21-ea` only warns and the start proceeds, so a Mendix 10 project on JDK 17 keeps working; no silent container), and snapshots a split model first** — `RUN_LOCAL_DB` in stack.conf: `auto` (default) never switches database by itself: no database option, no TCP probe, mxcli arguments identical to before (Postgres on another port or host keeps working through the model's settings); if the start fails with `database not reachable` the script names the two choices, `hsqldb` (built-in file database, data separate from Postgres; used only when set) and `ensure` (`--ensure-db`, creates the local role and database); `postgresql` requires the server to answer on `RUN_LOCAL_DB_HOST`. A non-default mode is warned and published as `LOCAL_DB=<mode>` in stack.env. The split-model snapshot adds `/.mpr-snapshots/` to the project's `.gitignore` the first time. Cold start measured on a copy of a real split-model project: master fails with `database not reachable`, `RUN_LOCAL_DB=hsqldb` serves HTTP 200 in 46 s, model still split — outside review of v2026.11.12
- fix(placeholders, gate-check): **an agent-stub slot whose text wraps onto a second line is now counted** by the shared slot count (`bin/lib/placeholders.sh`: init, sync, status, `check-stubs-filled.sh`) and by the build-ready placeholder check, which now FAILs on it instead of warning (maintainer decision 2026-11-13, supersedes the #509 warning): a project whose agent stubs still have the multi-line `PROJECT_LOCAL_SOURCES_OF_TRUTH` slot unfilled goes from a warning to a FAIL at build-ready; a fresh set of stubs reads 37 unfilled slots, not 36 (mdl-agent's `PROJECT_LOCAL_SOURCES_OF_TRUTH`, four lines long); `sync-project.sh`'s "untouched stub" (overwrite) test is deliberately unchanged, left per-line on purpose, so a stub with only that slot unfilled is still never overwritten — outside review of v2026.11.12
- new(exec): **`exec.sh` prints one `MXTK-RESULT exec verdict=<applied|applied-lint|applied-unverified|partial|restored|failed|refused> applied= verified= lint_new=<N|unknown|blind|skipped|na> exit=` line on every path that reached a verdict, before the closing lines, so an orchestrator reads the outcome without knowing that exit 4 means "applied, lint only"**; a failed restore reads `failed` / `applied=unknown` on the partial path too; no exit code or existing line changes; new fixture `tests/wave2/test-exec-result-line.sh` — outside review of v2026.11.12
- fix(design-reaches-app): **the CSS readers now normalise both stylesheets first and match whole classes (#670)**: a one-line `:root{--a:1;--b:2}` read as 1 token with `}` in its value, a minified built sheet as having no bare `:root`, a rule inside `@media` or a multi-line selector list was missed, and `var(--x, fallback)` did not count as using `--x`. All five fixed by one `normalise_css()` step (comments dropped, one selector/declaration/`}` per line, `;` inside `url(...)`/strings kept). On the committed real pair (design and built sheets of a fictional test app, 2026-11) tokens arrived went from 9 of 10 to 32 of 38 and theme knobs bound from 10 of 46 to 31 of 46; the check takes about 7 s instead of 3 s on that pair, because it now reads all the tokens. **Can turn a project red:** class presence was a substring match, so `.card` counted as arrived when only `.card-header` existed; it is now a whole-class match, and a project that passed on that false reading can now report `0 of N design-system classes` (FAIL) or fewer classes arrived. No verdict names or exit codes changed — #670; outside review of v2026.11.12
- docs(wrap-up): **the closing action of `checkpoint-cutover.md` now names the gate-check "N toolkit/mxcli bug(s) logged but not filed" line and the command to run (`bin/file-bugs.sh --auto` or `--links`)**; the line was already printed on a full run and on `--closeout`, but nothing at wrap-up said what to do with it; `--file` stays human-only; no always-loaded words added — outside review of v2026.11.12

## v2026.11.12
- new(parallel-builds): **`skills/parallel-builds.md`, an optional reference outside the pipeline (experimental tier, no stage map, no agent stub; used only when the user asks): several builders on one app in git worktrees, and how to merge their branches without losing a module (#741)**: a plain `git merge` of the model left one branch's units unregistered and both build gates reported 0 errors; the skill sets up worktrees (ports, database, number blocks, owners), forbids the plain merge, names the merge paths per CLI version, and checks `mxcli fix hashes`, `show modules` and the shared units before the build gate; forbids `diag --check-units --fix` after a suspect merge; a field run, 2026-11, lost a 63-unit module. Verbatim `fix hashes` capture under `tests/wave2/fixtures/worktree-merge/`. Guard script, exec.sh merge mode and lint delta are not built — field reports #741 and #744; LoanDesk worktree merge test
Expand Down
25 changes: 16 additions & 9 deletions bin/gate-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2435,8 +2435,16 @@ check_build_ready() {
# positive made deleting the safety net the cheapest way to get a green gate.
# A check that punishes the correct state trains people to break it.
local remaining
remaining=$(sed 's/{{DOUBLE_BRACE}}//g' "$agents_dir"/*.md 2>/dev/null \
| grep -o '{{[^}]*}}' | sort -u | tr '\n' ' ')
# tr joins each file's lines first so a placeholder wrapped over several lines counts (#509);
# per file, and the body excludes `{`, so the `'{{'` quoted in prose on line 11 of every stub
# cannot run on to a later slot's `}}` (see bin/lib/placeholders.sh).
# A slot longer than 80 characters (a wrapped one carries its whole hint text) is printed as
# {{NAME…}} so the failure line stays readable; shorter slots print unchanged.
remaining=$(for _af in "$agents_dir"/*.md; do
[ -f "$_af" ] && sed 's/{{DOUBLE_BRACE}}//g' "$_af" 2>/dev/null | tr '\n' ' '; echo
done | grep -o '{{[^{}]*}}' | sort -u \
| awk '{ if (length($0) > 80) { n=$0; sub(/^\{\{/, "", n); sub(/[^A-Za-z0-9_].*$/, "", n); print "{{" n "…}}" } else print }' \
| tr '\n' ' ')
if [ -n "$missing_agents" ]; then
echo " ✗ missing agent(s):$missing_agents — run bin/init-agents.sh"
fails=$((fails+1))
Expand All @@ -2449,18 +2457,17 @@ check_build_ready() {
echo " ✗ agent(s) still have unfilled placeholders: ${remaining}— complete them (agent-roles.md)"
fails=$((fails+1))
else
# A placeholder whose description spans lines (`{{NAME — prose…` on one line, `…}}` on a
# later one — mdl-agent's PROJECT_LOCAL_SOURCES_OF_TRUTH is four lines long) never matched
# the per-line grep above, so a stub with it untouched read "no unfilled placeholders"
# (#509). WARN, not FAIL: every project scaffolded before this check would flip to FAIL on
# a line nobody was ever told about; whether it should block is a maintainer call.
# Decision (maintainer, 2026-11-13, supersedes the #509 warning): an unfilled multi-line
# placeholder FAILs build-ready, like any other unfilled slot. The joined-lines count above
# already catches it; this perl scan is kept only for the one shape that count cannot match,
# a slot whose own text contains a single `{` (the count's body is `[^{}]*`).
local multi=""
multi=$(sed 's/{{DOUBLE_BRACE}}//g' "$agents_dir"/*.md 2>/dev/null \
| perl -0777 -ne 'while (/\{\{([A-Z][A-Z0-9_]+)[^}\n]*\n.*?\}\}/sg) { print "{{$1…}}\n" }' 2>/dev/null \
| sort -u | tr '\n' ' ')
if [ -n "$multi" ]; then
echo " ✓ all 6 agents present, no single-line placeholders"
echo " ⚠ multi-line placeholder(s) still unfilled: ${multi}— complete them (agent-roles.md); WARN only, not counted as a FAIL"
echo " ✗ agent(s) still have unfilled multi-line placeholders: ${multi}— complete them (agent-roles.md)"
fails=$((fails+1))
else
echo " ✓ all 6 agents present, no unfilled placeholders"
fi
Expand Down
7 changes: 6 additions & 1 deletion bin/lib/placeholders.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
# Bash 3.2 + POSIX sed/grep. Never fails: an unreadable file is skipped.

_mxtk_slots() { # $1=file → one slot name per line
sed 's/{{DOUBLE_BRACE}}//g' "$1" 2>/dev/null | grep -o '{{[^}]*}}' \
# tr joins the lines first: a slot whose text wraps onto a second line is still ONE slot
# (mdl-agent's PROJECT_LOCAL_SOURCES_OF_TRUTH is four lines long; the per-line grep missed it).
# The body excludes `{` too: line 11 of every agent stub quotes a bare `'{{'` in prose, and
# once lines are joined that `{{` would otherwise run on to the next real slot's `}}`,
# swallowing every slot in between.
sed 's/{{DOUBLE_BRACE}}//g' "$1" 2>/dev/null | tr '\n' ' ' | grep -o '{{[^{}]*}}' \
| sed -e 's/^{{ *//' -e 's/[^A-Za-z0-9_].*$//' -e 's/}}$//'
}

Expand Down
76 changes: 64 additions & 12 deletions project-bin/check-design-reaches-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,56 @@ report() { printf 'FAIL %s\n %s\n' "$1" "$2"; VIOLATIONS=$((VIOLATIONS + 1
warn() { printf 'WARN %s\n %s\n' "$1" "$2"; WARNINGS=$((WARNINGS + 1)); }
fault() { printf 'FAULT %s\n %s\n' "$1" "$2"; FAULTS=$((FAULTS + 1)); }

# ── Normalise both stylesheets once (#670) ─────────────────────────────────────────────────
# Every reader below is line-based and was written for a pretty-printed sheet: one declaration
# per line, `:root {` alone on its line. A minified or one-line sheet (`:root{--a:1;--b:2}`)
# read as ONE token whose value kept its `}`, and a minified built sheet as having no bare
# :root at all — the report then printed denominators like "1 of 1" over almost nothing.
# So: drop /* comments */, then put every selector (`sel {`), declaration (`prop: value;`) and
# closing `}` on a line of its own. `;` `{` `}` inside (...) or "..." / '...' (url(data:...;base64,..)) do not
# split; a multi-line selector list collapses onto one line; a rule inside @media gets its own
# line. A sheet that was already one-declaration-per-line comes out with the same lines.
normalise_css() {
awk '
function emit(s) { gsub(/[ \t\r]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s); return s }
BEGIN { SPECIAL = "[/*\"(){};\\\\\047]"; buf = ""; depth = 0; q = ""; inc = 0 }
{
line = $0
if (!inc && q == "" && depth == 0 && line !~ SPECIAL) { buf = buf " " line; next }
n = length(line)
for (i = 1; i <= n; i++) {
c = substr(line, i, 1)
if (inc) { if (c == "*" && substr(line, i + 1, 1) == "/") { inc = 0; i++; buf = buf " " } continue }
if (q != "") {
buf = buf c
if (c == "\\") { buf = buf substr(line, i + 1, 1); i++ } else if (c == q) q = ""
continue
}
if (c == "/" && substr(line, i + 1, 1) == "*") { inc = 1; i++; continue }
if (c == "\"" || c == "\047") { q = c; buf = buf c; continue }
if (c == "\\") { buf = buf c substr(line, i + 1, 1); i++; continue }
if (c == "(") { depth++; buf = buf c; continue }
if (c == ")") { if (depth > 0) depth--; buf = buf c; continue }
if (depth == 0 && c == "{") { s = emit(buf); print (s == "" ? "{" : s " {"); buf = ""; continue }
if (depth == 0 && c == ";") { s = emit(buf); if (s != "") print s ";"; buf = ""; continue }
if (depth == 0 && c == "}") { s = emit(buf); if (s != "") print s; print "}"; buf = ""; continue }
buf = buf c
}
q = ""; buf = buf " "
}
END { s = emit(buf); if (s != "") print s }
' "$1"
}
NORM_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cdra.XXXXXX")"
trap 'rm -rf "$NORM_DIR"' EXIT
DESIGN_N="$NORM_DIR/design.css"; BUILT_N="$NORM_DIR/built.css"
normalise_css "$DESIGN" > "$DESIGN_N"
normalise_css "$BUILT" > "$BUILT_N"

# A whole-class match: `.card` is present when `.card` is followed by a character that cannot
# continue a class name, never when only `.card-header` exists (a substring match counted it).
has_class() { grep -qE -- "$(printf '%s' "$2" | sed 's/\./\\./g')([^A-Za-z0-9_-]|\$)" "$1"; }

# ── Pass 0: read the design system ──────────────────────────────────────────────────────────
# Tokens: custom properties declared in a :root block. Values too, because a knob may be bound
# by repeating the literal rather than by var() — still bound, and still correct.
Expand All @@ -179,15 +229,15 @@ TOKENS="$(awk '
inroot && /^[[:space:]]*}/ { inroot = 0 }
inroot && match($0, /--[a-z0-9-]+[[:space:]]*:/) {
name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name)
rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest)
rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest)
if (!(name in seen)) { seen[name] = 1; print name "\t" rest }
}
' "$DESIGN")"
' "$DESIGN_N")"
TOKEN_COUNT="$(printf '%s\n' "$TOKENS" | grep -c '^--' || true)"

# Classes: any `.name` appearing at the head of a rule. Compound selectors contribute each.
CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN" \
CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN_N" \
| grep -oE '\.[A-Za-z_-][A-Za-z0-9_-]*' | sort -u)"
CLASS_COUNT="$(printf '%s\n' "$CLASSES" | grep -c '^\.' || true)"

Expand All @@ -211,7 +261,7 @@ BUILT_ROOT="$(awk '
map[name] = rest
}
END { for (k in map) print k "\t" map[k] }
' "$BUILT")"
' "$BUILT_N")"

# Themed palettes. A design system may keep its whole palette in scoped blocks,
# `:root.theme-light { ... }`, `:root.theme-dark { ... }`, with no bare `:root` at all. Pass 1
Expand All @@ -227,21 +277,21 @@ scoped_root() {
inscope && /^[[:space:]]*\}/ { inscope = 0; next }
inscope && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) {
name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name)
rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest)
rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest)
key = sel "\t" name; if (!(key in map)) order[++n] = key; map[key] = rest
}
END { for (i = 1; i <= n; i++) print order[i] "\t" map[order[i]] }
' "$1"
}
DESIGN_SCOPED="$(scoped_root "$DESIGN")"
BUILT_SCOPED="$(scoped_root "$BUILT")"
DESIGN_SCOPED="$(scoped_root "$DESIGN_N")"
BUILT_SCOPED="$(scoped_root "$BUILT_N")"
# Names the design declares in a bare `:root`; every other token is themed-only.
DESIGN_BARE_NAMES="$(awk '
/^[[:space:]]*:root[[:space:]]*\{[[:space:]]*$/ { inroot = 1; next }
inroot && /^[[:space:]]*\}/ { inroot = 0; next }
inroot && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) { n = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", n); print n }
' "$DESIGN" | sort -u)"
' "$DESIGN_N" | sort -u)"

KNOB_TOTAL=0
KNOB_BOUND=0
Expand All @@ -263,10 +313,12 @@ if [ -f "$CUSTOM_VARS" ]; then
val="$(printf '%s\n' "$BUILT_ROOT" | awk -F'\t' -v k="$knob" '$1 == k { v = $2 } END { print v }')"
[ -z "$val" ] && continue
bound=0
vals="$(printf '%s' "$val" | tr -d ' ')"
# (a) bound by reference
while IFS="$(printf '\t')" read -r tname tval; do
[ -z "$tname" ] && continue
case "$val" in *"var($tname)"*) bound=1; break ;; esac
# `var(--x)` and `var(--x, fallback)` both use --x
case "$vals" in *"var($tname)"*|*"var($tname,"*) bound=1; break ;; esac
# (b) bound by repeating the literal. Only for values that look like a value, so an
# empty or inherit-ish token cannot mark every knob bound.
if [ -n "$tval" ] && [ "${#tval}" -ge 3 ]; then
Expand All @@ -289,7 +341,7 @@ fi
TOKENS_ARRIVED=0
while IFS="$(printf '\t')" read -r tname tval; do
[ -z "$tname" ] && continue
grep -qE -- "$tname[[:space:]]*:" "$BUILT" && TOKENS_ARRIVED=$((TOKENS_ARRIVED + 1))
grep -qE -- "$tname[[:space:]]*:" "$BUILT_N" && TOKENS_ARRIVED=$((TOKENS_ARRIVED + 1))
done <<EOF
$TOKENS
EOF
Expand Down Expand Up @@ -347,7 +399,7 @@ EOF
CLASSES_ARRIVED=0
MISSING_CLASS_SAMPLE=""
for cls in $CLASSES; do
if grep -qF -- "$cls" "$BUILT"; then
if has_class "$BUILT_N" "$cls"; then
CLASSES_ARRIVED=$((CLASSES_ARRIVED + 1))
elif [ "$(printf '%s' "$MISSING_CLASS_SAMPLE" | wc -w)" -lt 6 ]; then
MISSING_CLASS_SAMPLE="$MISSING_CLASS_SAMPLE $cls"
Expand Down Expand Up @@ -389,7 +441,7 @@ if [ -n "$MDLSOURCE" ]; then
| sed "s/.*'\\(.*\\)'/\\1/" | tr ' ' '\n' | grep -E '^[A-Za-z_-][A-Za-z0-9_-]*$' | sort -u)"
for cls in $MODEL_CLASSES; do
MODEL_CLASS_TOTAL=$((MODEL_CLASS_TOTAL + 1))
if ! grep -qF -- ".$cls" "$BUILT"; then
if ! has_class "$BUILT_N" ".$cls"; then
MODEL_CLASS_MISSING=$((MODEL_CLASS_MISSING + 1))
if [ "$(printf '%s' "$MODEL_MISSING_SAMPLE" | wc -w)" -lt 8 ]; then
MODEL_MISSING_SAMPLE="$MODEL_MISSING_SAMPLE $cls"
Expand Down
Loading
Loading