diff --git a/CHANGELOG.md b/CHANGELOG.md index cdadb8b2..4c6faaca 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(dispatching, sync-project, obligation-check): **review follow-ups for the 2026-10-14 integration batch.** `dispatching.md` names app-restart exits 4 and 5 beside 3 and 1. `test-test-scratch.sh` drops `ls | grep -q` under pipefail for here-strings; sync step 4d no longer aborts on empty doctor output; `_ob_has_denominator` strips NUL bytes so bash stops warning. — integration 2026-10-14 reviews - feat(lint): **Lint Batch 4 — SEC014 external role can write state, audit or ownership members; CONV009 reads its `maxObjects` option.** SEC014 ([#268](https://github.com/MendixMau/mxcli-project-toolkit/issues/268)): a partner role with `create, read *, write *` on self-owned entities passed every stock rule, so it could set its own record's status through the client API. Lint showed only CONV006 +4, which was re-baselined, and a manual review found it five days later. The new rule flags an external role (named in `lint-config.yaml` as `external_roles: "A, B"`, or `"none"`) that writes all members, a `Status`/`State`/`Result`/`Stage`/`Phase` enumeration, an `…At`/`…By` attribute, or the association its own XPath uses for ownership. It is a warning in the security tier. A `_rule` finding marks the rule blind when the option is blank or given as a YAML list (mxcli passes a list as `None`), when a role name does not exist, or when the catalog is not FULL. Each message is under 160 characters and names the place to fix it, because lint-gate prints only that much. An **absent** option is not a `_rule` finding, so it does not block every exec (the #229 pattern). Instead `bin/sync-project.sh` prints a NOTE on every sync through new `bin/lib/sec014-notice.sh` until the option is set. It is a NOTE rather than a counted warning, so a pristine scaffold still syncs clean under `--strict` (fixture `tests/wave2/test-sec014-notice.sh`, 14 cases; `test-bug12-sync.sh` 61/61). `"none"` switches a security rule off, so it is set only on the user's answer in chat and recorded in `PROJECT.md`. Right after configuring, re-baseline with `lint-gate.sh --update-baseline --reason "SEC014 configured" --approved-in-chat`, because the 0 → N jump would otherwise be blamed on the next script. CONV009 ([#267](https://github.com/MendixMau/mxcli-project-toolkit/issues/267)): the stock rule hard-codes 15, so the `maxObjects: 20` example in the config `mxcli init` writes did nothing. The toolkit copy reads `maxObjects` and `countLoopBodies`, with the **default kept at 15** (raising it is a maintainer call). It replaces the stock file through `STOCK-HASHES.txt`, which gains conv009 hashes for v0.17.0/0.24.0/0.25.0 and a full v0.26.0 set, captured by `mxcli init` from the official release assets; `test-stock-hashes.sh` T4 passes live on all four. `lint-that-actually-runs.md` now says `options:` reaches only rules that call `get_option()`. 32 cases in `tests/lint-rules/test-batch4-rules.sh`; the harness gained `get_option`, `permissions_for`, `attributes_for`, `user_roles` and `role_mappings`. Field run (official mxcli 0.26.0, two model copies): stock CONV009 stayed at 5 with `maxObjects: 20`, while the toolkit copy gave 5 at 15, 10 at 10 and 6 at 20 with loop bodies counted. SEC014 gave 1 true finding with the model's external role configured, and 7 with every role configured, matched by a SQL replay. Both rules load on 0.17.0 and 0.24.0 too. CONV009's "every flow reports 0 objects" self-check now needs `activities_for()` to find a real object in a zero-count flow, so an early greenfield model with empty microflows is not reported blind. `lint-rules/README.md` and the TXT002 header now give the supported way to turn a toolkit rule off. ISO dates in particular are a toolkit opinion, so a project with a non-ISO display standard sets `rules: TXT002: enabled: false` in `lint-config.yaml` and records that in `PROJECT.md`. It must not delete the file, which sync would re-install. On mxcli 0.26.0, a rule with 1 finding gave 0 once disabled. After #308, there is one opt-out story, always the user's decision (asked in chat, recorded in `PROJECT.md`), and it never deletes a `.star` file. `enabled: false` in `lint-config.yaml` stops a rule reporting; this is preferred. `.claude/lint-rules-disabled.txt` with a `# reason:` stops sync installing a toolkit rule, and refuses stock-seeded rules, a list that now includes conv009. `rules: SEC014: enabled: false` is the same decision as `external_roles: "none"`. While SEC014 is undecided, `project-bin/lint-gate.sh` also prints one uncounted `note: SEC014 not configured …` line on stderr on every run (no exit-code change, no lint row). Both it and the sync NOTE now count `external_roles` only when it sits under the `SEC014` key. `test-sec014-notice.sh` has 24 cases. — MendixMau - fix(lint-gate): **rules that `mxcli init` reverted to stock now stop the lint run as BLIND instead of showing up as a rise on the next script** ([#264](https://github.com/MendixMau/mxcli-project-toolkit/issues/264)). `bin/lint-gate.sh` reads `.claude/.mxtk-lint-receipt` (written by `bin/lib/install-lint-rules.sh`) before linting: a stock-seeded rule (the file names `mxcli init` writes, now marked `stock` in the receipt) whose md5 no longer matches and whose `# mxtk-lint-rule:` header is gone, or any receipt-listed rule that is missing, is named, the remedy `bin/sync-project.sh ` (fallback `--upgrade-lint-rules all`) is printed, and the run exits 1 with `BLIND — lint rules reverted to stock` (exec.sh logs `LINT BLIND`, not `LINT ROSE`); `--update-baseline` is refused over reverted rules, also under the `LINT_RULES_REVERTED_OK=1` look-anyway override. Field cases: CONV010 26 -> 169 blamed on a pages-only exec, and a re-seed that sat five days while the baseline absorbed stock counts. So the remedy always clears it, `bin/lib/install-lint-rules.sh` restores such a stock-seeded rule even when its body matches no known stock hash (a newer mxcli's seed, or a hand edit that also lost the header: it was reported LOCALLY MODIFIED and never written), labelled `header missing — restored from toolkit, your copy kept as .reseeded-` with the newest 3 copies kept; a headerless toolkit-only or configurable rule is still a local edit, never overwritten. The installer also writes each receipt row's kind, prunes rows for rules no longer shipped, and honours `.claude/lint-rules-disabled.txt`: one line per rule with `# reason: …` (`#\s*reason:` in both tools), a user decision recorded in `PROJECT.md`; a line with no reason, or naming a stock-seeded rule, is ignored with a warning (`lint-rules/README.md` had told projects to just delete txt002). Fixture `tests/wave2/test-lint-receipt-revert.sh` builds its receipt with the real installer and uses a genuine v0.25.0 stock body: 55/55 here, 28 fail on master's lint-gate and installer. — AlvinnYeo - learn(learned-mdl-preflight): **a microflow change near a `return` in a dense flow is planned as drop + create after `show access`; `check -p --references` does not run exec's free-room test** ([#263](https://github.com/MendixMau/mxcli-project-toolkit/issues/263)). STOP row 28 and BUG-156: on v0.25.0 `check` printed "no flow change exec would refuse" and exec then refused "there is no free room for the fragment". Upstream issue drafted, not filed. — AlvinnYeo diff --git a/bin/lib/obligation-check.sh b/bin/lib/obligation-check.sh index b449c6dd..af0c17ba 100755 --- a/bin/lib/obligation-check.sh +++ b/bin/lib/obligation-check.sh @@ -253,7 +253,7 @@ _ob_find() { # Deliberately loose — the wording belongs to the skill, only the shape is mechanical. _ob_has_denominator() { grep -qiE -- '[0-9]+[[:space:]]+of[[:space:]]+[0-9]+|[0-9]+[[:space:]]*/[[:space:]]*[0-9]+' \ - <<<"$(head -c 4000 "$1" 2>/dev/null | tr -d '\r')" + <<<"$(head -c 4000 "$1" 2>/dev/null | tr -d '\r\0')" } # _ob_stale → prints "+" and returns 0 if the mark has diff --git a/bin/sync-project.sh b/bin/sync-project.sh index 95fad085..97ba2fc4 100755 --- a/bin/sync-project.sh +++ b/bin/sync-project.sh @@ -1479,9 +1479,9 @@ if [ "$WIRED" -eq 1 ] && [ -x "$SCRIPT_DIR/doctor.sh" ] \ # rc 3 (an mxcli before v0.26 cannot check) and rc 4 (the check itself failed) are not # findings about the model, so doctor's WARN line for them is relabelled, not shown as one. if [ "$_ps_rc" -eq 3 ] || [ "$_ps_rc" -eq 4 ]; then - printf '%s\n' "$_ps_out" | grep -v '^[[:space:]]*$' | sed -e 's/^ WARN / info /' -e 's/^/ /' + printf '%s\n' "$_ps_out" | grep -v '^[[:space:]]*$' | sed -e 's/^ WARN / info /' -e 's/^/ /' || true else - printf '%s\n' "$_ps_out" | grep -v '^[[:space:]]*$' | sed 's/^/ /' + printf '%s\n' "$_ps_out" | grep -v '^[[:space:]]*$' | sed 's/^/ /' || true fi case "$_ps_rc" in 0|2) ;; diff --git a/skills/dispatching.md b/skills/dispatching.md index a720e313..93211ae8 100644 --- a/skills/dispatching.md +++ b/skills/dispatching.md @@ -56,7 +56,9 @@ app." The helper stops only the pid that `run --local` recorded for that project otherwise (`ui-loop.md` § "What watch picks up"). The old lead-only rule came from a broad kill in one lane that took down another lane's app; the helper cannot do that, and the rule cost 24 implementer restart-resumes (41.6M tokens, 8.5% of subagent tokens) in the 2026-10-12 run (#289). If the -helper refuses (exit 3) or the app does not come back (exit 1), the implementer reports +helper refuses (exit 3), the app does not come back (exit 1), a process it did not record +holds the app or admin port (exit 4, nothing started; the recorded app may already be stopped), or the stop left a live +MxTest after-startup in place (exit 5, nothing started), the implementer reports `BLOCKED NEEDS_RESTART` with its output, as before. **Prefix you do not control.** Every subagent turn also carries the harness's skill listing, diff --git a/tests/wave2/test-test-scratch.sh b/tests/wave2/test-test-scratch.sh index 4609943f..19db6b92 100755 --- a/tests/wave2/test-test-scratch.sh +++ b/tests/wave2/test-test-scratch.sh @@ -53,8 +53,8 @@ printf '%s' "$R" | grep -qx '.mxcli' && bad "the live run --local record was cop printf '%s' "$R" | grep -qx 'deployment' && bad "deployment/ was copied" || ok "deployment/ not copied" [ "$(printf '%s' "$R" | tail -1)" = 1 ] && ok "port lock held during the run" || bad "no port lock during the run" [ "$(cat "$P/Fixture.mpr")" = model ] && ok "live .mpr untouched" || bad "live .mpr was written" -ls "$WORK/tmp" | grep -q 'mxtk-test-ports' && bad "port lock left behind" || ok "port lock released" -ls "$WORK/tmp" | grep -q 'Fixture-test' && bad "scratch copy left behind" || ok "scratch copy deleted" +grep -q 'mxtk-test-ports' <<<"$(ls "$WORK/tmp")" && bad "port lock left behind" || ok "port lock released" +grep -q 'Fixture-test' <<<"$(ls "$WORK/tmp")" && bad "scratch copy left behind" || ok "scratch copy deleted" [ -f "$P/.mpr-snapshots/.exec.lock" ] && bad "exec lock left behind" || ok "exec lock released" echo "-- exit code passes through" @@ -72,7 +72,7 @@ has "names the holder" "$OUT" "held by PID $H" kill -KILL "$H" 2>/dev/null; wait "$H" 2>/dev/null echo "-- dead holder's port lock is taken over (the SIGKILL above left it)" -ls "$WORK/tmp" | grep -q 'mxtk-test-ports' && ok "[setup] stale port lock present" || bad "[setup] no stale lock" +grep -q 'mxtk-test-ports' <<<"$(ls "$WORK/tmp")" && ok "[setup] stale port lock present" || bad "[setup] no stale lock" rm -f "$P/.mpr-snapshots/.exec.lock" OUT="$(run e -- tests/a.test.mdl)"; RC=$? [ "$RC" -eq 0 ] && has "taken over out loud" "$OUT" "taking it over" || bad "stale port lock blocked (exit $RC)" @@ -157,7 +157,7 @@ if command -v node >/dev/null 2>&1; then has "prints the git repair" "$OUT" "git checkout -- 'mprcontents/4e/70/4e70063d-53e1-477a-ab92-6b763330c293.mxunit'" has "prints the MDL repair with HEAD's value" "$OUT" "alter settings runtime (AfterStartupMicroflow: 'Orders.ASU_Startup');" [ -f "$WORK/rec.k" ] && bad "mxcli ran on the injected model" || ok "mxcli not run" - ls "$WORK/tmp" | grep -q 'Fixture-test' && bad "a copy was made" || ok "nothing copied" + grep -q 'Fixture-test' <<<"$(ls "$WORK/tmp")" && bad "a copy was made" || ok "nothing copied" node "$SETAS" "$GU" "$UNIT" "MxTest.TestRunner" OUT="$(run l -- tests/a.test.mdl)"; RC=$? [ "$RC" -eq 3 ] && ok "MxTest.TestRunner → refused (exit 3)" || bad "TestRunner exit $RC"