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(sync): **the exec-approval item-3 rewrite now works on BSD awk (stock macOS) and `sync-project.sh` no longer prints "Rewrote" for a rewrite that failed.** `_ea_rewrite_item3` passed the multi-line replacement through `awk -v`, which BSD awk rejects ("newline in string", exit 2); the file stayed unchanged and sync still reported the rewrite on every run. The replacement is now streamed from a temp file (as the Q9 repair does), the helper returns 2 when awk fails, and the caller prints a `WARN:` instead of "Rewrote" and does not count a change. `test-exec-approval.sh` gains 3 assertions (failing-awk shim: no "Rewrote", a WARN, file unchanged). No other multi-line `awk -v` call site exists (the rest are single-line values) ([#426](https://github.com/MendixMau/mxcli-project-toolkit/issues/426)) — MendixMau
- process(testing): **unit tests are now off by default and `affected-tests.sh` enforces it; each project picks its test strategy with the developer at CAC-5.** Fast-tracked by the maintainer on 2026-10-16 because users report tests firing on every change (the baseline-text A/B is waived; E3 follows as a check). Moments: every change = gate plus what it touched (unit only for affected modules with unit on); module done = that module's journeys (+ its unit tests if on); build complete and before every later deploy = everything once; "run everything" any time. The CAC-5 Offer records it in PROJECT.md `## Test strategy` (Status `ASSUMED` in auto mode). `affected-tests.sh` reads `Unit tests default:` (`no` or absent means unit off, one `unit off` line, never a `.test.mdl` run row); turn unit on per project with `Unit tests default: yes`, per module with `Unit modules: Shop, Admin` in PROJECT.md or `Unit tests: yes` in the module brief; the strategy row's Unit cell is a scope only and never turns unit on. A grant on a microflow or page the same script changes is part of that subject and no longer selects the owning module's tests. `module-review.md` PROVE now runs `--moment module --module <M>`. `testing-shape.md`, `checkpoint-build.md`, `iterative-build-loop.md`, `conversion-runbook.md` and the exec.sh logic lane text updated. `tests/wave2/test-test-strategy-docs.sh` and the extended `test-affected-tests.sh` pin it. Baseline words stay at or below master (75,219 vs 75,225). Split out of #409; found by the field test of the release candidate — the maintainer's decision, 2026-10-16
- learn(skills): **five lab findings land as compact rows, each paid for by trimming prose in the same files, so always-loaded words do not grow (baseline 75,225 to 75,204, budget 80,000).** Enum value `New` and `AutoCreatedDate`/`AutoOwner` (detection gaps), `[%CurrentDateTime%]` not `currentDateTime()`, expression return types (Decimal from date diffs and `div`, absolute `secondsBetween`, `formatDecimal` on pages), CE2729 from the default home page and page microflow grants, Atlas selector carriers for list views, page body and timelines, `-small` spacing suffixes do not exist, `display: contents` rows paint nothing, CSS pseudo text is invisible to `innerText` — lab field runs, Refs #276 (TK-020, TK-021, TK-022, TK-024)
- fix(design-check): **a design system whose palette lives only in scoped `:root.<theme>` blocks no longer reads as "0 of N tokens carry the design's value".** Pass 2b compared every token against a bare `:root` of the built sheet, so a themed-only palette always warned, a false warning that teaches agents to ignore the check. A token the design declares only in scoped blocks is now compared block for block against the same-named block in the built sheet, and a wrong value is still named with its block (knob binding stays bare-only: a knob bound inside a theme the app is not wearing is not bound). Synthetic fixtures `ds-themed.css`, `built-themed.css`, `built-themed-wrong.css`; 5 new cases in `test-design-reaches-app.sh` (50 total) — TK-015 part (b)
Expand Down
75 changes: 39 additions & 36 deletions bin/sync-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1511,54 +1511,57 @@ _ea_rewrite_item3() { # $1=file $2=start-prefix $3=end-prefix $4=replacemen
grep -qF -- "$needle" <<<"$_blk" || return 1
fi
[ "$DRY_RUN" -eq 1 ] && return 0
_tmp="$(mktemp "${TMPDIR:-/tmp}/eaitem3.XXXXXX")" || return 1
awk -v start="$start" -v endp="$endp" -v repl="$repl" '
_tmp="$(mktemp "${TMPDIR:-/tmp}/eaitem3.XXXXXX")" || return 2
_rf="$(mktemp "${TMPDIR:-/tmp}/eaitem3r.XXXXXX")" || { rm -f "$_tmp"; return 2; }
# BSD awk (stock macOS) rejects a newline inside -v ("awk: newline in string"), so the
# replacement text is streamed from a file, as the Q9 repair does. Return 2 (not 0) when awk
# fails so the caller warns instead of reporting a rewrite that never happened.
printf '%s\n' "$repl" > "$_rf" || { rm -f "$_tmp" "$_rf"; return 2; }
if awk -v start="$start" -v endp="$endp" -v rf="$_rf" '
BEGIN { skip = 0 }
{ if (skip) { if (index($0, endp) == 1) { skip = 0 } else { next } }
if (!skip && index($0, start) == 1) { print repl; skip = 1; next }
if (!skip && index($0, start) == 1) {
while ((getline l < rf) > 0) print l
close(rf); skip = 1; next }
print }
' "$f" > "$_tmp" && cat "$_tmp" > "$f"
rm -f "$_tmp"
return 0
' "$f" > "$_tmp" && [ -s "$_tmp" ]; then
cat "$_tmp" > "$f"
rm -f "$_tmp" "$_rf"
return 0
fi
rm -f "$_tmp" "$_rf"
return 2
}
# Report one item-3 rewrite attempt: $1=rc of _ea_rewrite_item3 $2=label $3=dry text $4=done text
_ea_report_item3() {
case "$1" in
0) if [ "$DRY_RUN" -eq 1 ]; then echo "Would rewrite: $2 item 3 — $3"
else echo "Rewrote: $2 item 3 — $4"; fi
CHANGES=$((CHANGES + 1)) ;;
2) warn "$2 item 3 rewrite FAILED (awk error or temp file); file left unchanged." "Re-run sync, or edit item 3 by hand." ;;
esac
}
_ea_try_item3() { # $1=file $2=label $3=md/hash text $4=prefix of "3. **" — tries both wordings
_p="$4"; _rc=0
_ea_rewrite_item3 "$1" "${_p}3. **Never run" "${_p}4. **" "$3" || _rc=$?
if [ "$_rc" -ne 1 ]; then
_ea_report_item3 "$_rc" "$2" "carries the old \"without asking the user first — every time\" wording" "the old \"without asking the user first — every time\" wording is now the exec-approval knob (bin/exec-approval.sh)."
return
fi
_rc=0
_ea_rewrite_item3 "$1" "${_p}3. **Before any" "${_p}4. **" "$3" '`ask` (the default)' || _rc=$?
_ea_report_item3 "$_rc" "$2" "still says \`ask\` is the default; exec approval now defaults to \`auto\`" "flipped from \"\`ask\` (the default)\" wording to auto-default (bin/exec-approval.sh)."
}
if [ "$WIRED" -eq 1 ]; then
_EA_NEW3_MD="$(wiring_item3_md "$TOOLKIT_ROOT")"
_EA_NEW3_HASH="$(wiring_item3_hash "$TOOLKIT_ROOT")"
for rel in AGENTS.md CLAUDE.md .github/copilot-instructions.md .cursorrules .windsurfrules; do
f="$PROJECT_DIR/$rel"
[ -f "$f" ] || continue
if _ea_rewrite_item3 "$f" "3. **Never run" "4. **" "$_EA_NEW3_MD"; then
if [ "$DRY_RUN" -eq 1 ]; then
echo "Would rewrite: $rel item 3 — carries the old \"without asking the user first — every time\" wording"
else
echo "Rewrote: $rel item 3 — the old \"without asking the user first — every time\" wording is now the exec-approval knob (bin/exec-approval.sh)."
fi
CHANGES=$((CHANGES + 1))
elif _ea_rewrite_item3 "$f" "3. **Before any" "4. **" "$_EA_NEW3_MD" '`ask` (the default)'; then
if [ "$DRY_RUN" -eq 1 ]; then
echo "Would rewrite: $rel item 3 — still says \`ask\` is the default; exec approval now defaults to \`auto\`"
else
echo "Rewrote: $rel item 3 — flipped from \"\`ask\` (the default)\" wording to auto-default (bin/exec-approval.sh)."
fi
CHANGES=$((CHANGES + 1))
fi
_ea_try_item3 "$f" "$rel" "$_EA_NEW3_MD" ""
done
f="$PROJECT_DIR/.aider.conf.yml"
if [ -f "$f" ] && _ea_rewrite_item3 "$f" "# 3. **Never run" "# 4. **" "$_EA_NEW3_HASH"; then
if [ "$DRY_RUN" -eq 1 ]; then
echo "Would rewrite: .aider.conf.yml item 3 — carries the old \"without asking the user first — every time\" wording"
else
echo "Rewrote: .aider.conf.yml item 3 — the old \"without asking the user first — every time\" wording is now the exec-approval knob (bin/exec-approval.sh)."
fi
CHANGES=$((CHANGES + 1))
elif [ -f "$f" ] && _ea_rewrite_item3 "$f" "# 3. **Before any" "# 4. **" "$_EA_NEW3_HASH" '`ask` (the default)'; then
if [ "$DRY_RUN" -eq 1 ]; then
echo "Would rewrite: .aider.conf.yml item 3 — still says \`ask\` is the default; exec approval now defaults to \`auto\`"
else
echo "Rewrote: .aider.conf.yml item 3 — flipped from \"\`ask\` (the default)\" wording to auto-default (bin/exec-approval.sh)."
fi
CHANGES=$((CHANGES + 1))
fi
[ -f "$f" ] && _ea_try_item3 "$f" ".aider.conf.yml" "$_EA_NEW3_HASH" "# "
fi

# ── Per-harness auto-run allow-list: report only, never write ────────────────────────────
Expand Down
39 changes: 39 additions & 0 deletions tests/wave2/test-exec-approval.sh
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,45 @@ else
bad "sync-project.sh not found/executable at $SYNC — cannot check the 2026-09-15 repair"
fi

# --- sync-project.sh: a failed item-3 rewrite is reported as a failure, never as "Rewrote" ----
# (#426) A shim awk that always exits 2 stands in for BSD awk rejecting the replacement.
if [ -x "$SYNC" ]; then
SP3="$WORK/syncproj3"; mkdir -p "$SP3/bin" "$WORK/failawk"
: > "$SP3/Fixture.mpr"
printf 'placeholder\n' > "$SP3/PROJECT.md"
printf 'placeholder\n' > "$SP3/CLAUDE.local.md"
printf 'placeholder\n' > "$SP3/intake.md"
mkdir -p "$SP3/.github"
for f in AGENTS.md CLAUDE.md .github/copilot-instructions.md .cursorrules .windsurfrules; do
printf '%s\n' "$OLDBLOCK" > "$SP3/$f"
done
# all six entry points present, so wire-agents --check passes and only the item-3 repair runs
printf '%s\n' "$OLDBLOCK" | sed 's/^/# /' | sed 's/[[:space:]]*$//' > "$SP3/.aider.conf.yml"
# fails only the replacement-streaming call (the one that passes rf=), like BSD awk would
printf '#!/bin/sh\nfor a in "$@"; do case "$a" in rf=*) echo "awk: simulated failure" >&2; exit 2;; esac; done\nexec /usr/bin/awk "$@"\n' > "$WORK/failawk/awk"
chmod +x "$WORK/failawk/awk"
BEFORE3="$(cat "$SP3/CLAUDE.md")"
cp -R "$SP3" "$SP3.base"
OUT4B="$(bash "$SYNC" "$SP3.base" 2>&1)"
OUT4="$(PATH="$WORK/failawk:$PATH" bash "$SYNC" "$SP3" 2>&1)"
printf '%s' "$OUT4" | grep -q 'Rewrote: CLAUDE.md item 3' \
&& bad "sync-project.sh: prints 'Rewrote' for an item-3 rewrite that failed" \
|| ok "sync-project.sh: no 'Rewrote' message when the item-3 rewrite fails"
printf '%s' "$OUT4" | grep -q '⚠️ CLAUDE.md item 3 rewrite FAILED' \
&& ok "sync-project.sh: failed item-3 rewrite is reported as a WARN" \
|| bad "sync-project.sh: failed item-3 rewrite gave no WARN"
# the failures must reach the WARNINGS counter: the same fixture synced with a working awk
# gives the baseline count, and the six failed rewrites must add to it
N0="$(printf '%s' "$OUT4B" | sed -n 's/^\([0-9][0-9]*\) warning(s).*/\1/p')"
N1="$(printf '%s' "$OUT4" | sed -n 's/^\([0-9][0-9]*\) warning(s).*/\1/p')"
[ "${N1:-0}" -ge $(( ${N0:-0} + 6 )) ] \
&& ok "sync-project.sh: failed item-3 rewrites are counted as warnings (${N0:-0} -> ${N1:-0})" \
|| bad "sync-project.sh: failed item-3 rewrites not counted as warnings (${N0:-0} -> ${N1:-0})"
[ "$BEFORE3" = "$(cat "$SP3/CLAUDE.md")" ] \
&& ok "sync-project.sh: file left unchanged when the rewrite fails" \
|| bad "sync-project.sh: file changed although the rewrite failed"
fi

echo ""
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
Loading