Skip to content

Enumerate before you add: entry-point recipe, detection-gap row, exec commit rule, and three inbox drops - #78

Open
MendixMau wants to merge 12 commits into
masterfrom
claude/mxcli-project-setup-clhy8g
Open

MendixMau wants to merge 12 commits into
masterfrom
claude/mxcli-project-setup-clhy8g

Conversation

@MendixMau

Copy link
Copy Markdown
Owner

Direct lane (changes to skills/, bin/)

What changed and why (one paragraph):

Two chat pages that already had floating buttons on an app's home page were diagnosed as unreachable and given navigation menu entries. The entries were redundant, and they were also broken — both pages carry ConversationalUI.Layout_FloatingPopup and take a ChatContext parameter, and a navigation item cannot pass a page parameter, so clicking one would have opened a context-less dead-end modal. The whole static ladder passed it: check --references reported 0 errors, exec succeeded, SHOW NAVIGATION MENU read the entry back cleanly. This PR lands the recipe that prevents it (ui-preflight-pages.md Step 0), the register row that names what the green result actually certifies (learned-detection-gaps.md), and — from the same incident's revert commit — a rule resolving a collision between two of this repo's own conventions (iterative-build-loop.md). Plus three contrib/inbox/ drops and one baseline skill from the deploy-and-configuration retrospective that ran alongside.

Contents

  • skills/ui-preflight-pages.md → Step 0 (baseline, fires before any page work). Before building a page or adding a way into one, enumerate the entry points that exist using the reference tools — mxcli impact, mxcli callers, DESCRIBE PAGE <HomePage> for buttons and snippet calls already placed. SEARCH '<PageName>' is a text grep, not a reference check: it did not surface the show_page action in the home page's widget tree. A browser scrape of navigation captions could not match an icon-only floating button either, so a second silent instrument agreed with the first and the pair was read as confirmation. Then read the target page's Layout: — a popup layout is a popup, not a destination. Written as a positive recipe rather than a prohibition (authoring rule 3: the failure here is shape, not discipline).
  • skills/learned-detection-gaps.md — one register row. Note precisely what the green certifies: the reference is valid, the page exists. Nothing on the ladder checks whether a nav item is a legal way to reach it.
  • skills/iterative-build-loop.md — "committing after an exec — enumerate, then name". mxcli exec rewrites the .mpr and every mprcontents/*.mxunit it touched; a blind git add -A is banned; so commits pass explicit pathspecs — but an explicit pathspec cannot name a file you did not know changed. Resolution is git status --short after every exec, before writing the pathspecs, with mprcontents/ as a directory pathspec.
  • skills/agent-permission-friction.md (new, routed baseline for all agents) — two different things refuse an agent's command and they render identically. A permission rule is a deterministic prefix match that ships with the repo; a safety-classifier refusal is a judgement that exists only in managed/sandboxed environments and cannot be allowlisted away. Four checks before the word blocked reaches the user, highest-yield first, starting with: a rule matches the START of the command line, so an allowlisted tool prefixed with cd matches nothing.
  • Three contrib/inbox/ drops — configuration surface as a deployment-target requirement; secret-ingest validation and guard strings; the cd-prefix allowlist defeat.

Field evidence — which real project this ran against / was observed in, and what it measured or fixed there:

A MOC/PSSR app replacement, 2026-09-16. The navigation entries were written, check --references-clean at 0 errors, exec'd against the real .mpr, read back green via SHOW NAVIGATION MENU, committed and pushed to both GitHub and Team Server — and were still wrong on every count. Reverting them is what surfaced the commit-pathspec defect: the revert's first commit named the .mpr and the script and left the exploded navigation unit behind, git reported 2 files changed, and the .mpr disagreed with its own contents in the commit. The permission-friction skill's fifteen-turn cost and the retrospective's four probed configuration routes are measured on the same project, over the preceding three days.

  • No client data anywhere in the diff
  • For a new/changed instrument: no instrument changed — skills and inbox files only
  • For a new skill: routing row added (bin/lib/skill-routing.tsv + bin/render-routing.sh)
  • CHANGELOG.md line appended in this PR, crediting the source project or person (CI-checked)
  • New bug entries headed ## BUG-DRAFT-<slug>: — no new bug entries in this PR

Verification

Per the scoped-testing rule, verified by inspection rather than a suite run:

  • bin/render-routing.sh --check → in sync, all skills routed or exempted.
  • bin/check-docs-numbering.sh → clean on the staged docs.
  • bin/check-pr-discipline.sh → clean (changelog rides along; no BUG-number collisions vs origin/master).
  • The ten greps tests/wave2/test-bug09-13.sh makes over iterative-build-loop.md all still match — the change is purely additive.

Two things for the merge desk

  1. Baseline word budget is at 79,522 of 80,000 after this merge with master. Headroom is under 500 words. The next baseline routing needs a displacement, not an append.
  2. bin/check-no-client-data.sh fails on master today, independently of this PR. Two files already merged there trip the denylist: contrib/inbox/2026-09-14-moc-app-replacement-bugs.md and contrib/inbox/2026-09-14-moc-app-replacement-patches.md. I did not touch them, and per the repo's own rule I have not narrowed the denylist or gitignored them. Flagging rather than working around it.

This branch was 56 commits behind master and has been merged up; the six conflicted files were all render outputs of skill-routing.tsv (which merged cleanly), resolved by taking master's side and re-rendering. CHANGELOG.md was resolved by keeping both sides.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M


Generated by Claude Code

The scorer already excludes <td>, because a wireframe's table cells are
sample rows and a page that correctly binds them contains none of that
literal text. The same is true of a bound value anywhere else, and there
the structure gives nothing away: an <h1> holding a record's number and
name looks exactly like an <h1> holding page copy.

Measured on a MOC/PSSR app replacement's project detail page: 54%, all
three misses being sample values the page could not contain — the
record's own title, and a mocked attachment's filename twice. The page
was right, the denominator was wrong, and that number was about to be
read off as a gate verdict.

So the marker is declared, not guessed. An element carrying
class="bound" leaves the text corpus and the class denominator. Guessing
was the alternative and it does not work: a heading is bound or it is
not, and only the person who drew the screen knows which.

Fixture grows to 18 assertions, including that a bound heading leaves
the DENOMINATOR and not merely the missed list — otherwise the score
would be 1 of 2 and still look clean. Known-bad control without the
marker fails 5 of 18. The same page scores 100% with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…elity.js

From the moc-app-replacement project's overnight session: 4 of 8 admin
pages went unstyled and unreachable through two build scripts because
neither the per-script UI loop nor the module-close review actually ran
the fidelity harness that already exists in this toolkit. Proposes
adding an explicit "run page-fidelity.js now" step plus a reachability
question to ui-loop.md -- not applied directly, per this repo's own
baseline-test-before-rewrite rule for behaviour-shaping skills.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…nd the first did not

A change-governance app went through two UI passes on the same day, on the same
design system. The user called the first "poor" and the second "much better".
The CSS authoring did not get better in between. The closing check did.

  Pass 1 verified that the SCRIPT EXECUTED.
  Pass 2 verified that THE PIXEL CHANGED.

Every green signal in pass 1 was true and irrelevant: mx check clean, lint
clean, MDL suite green, two journeys green, the design port reporting its
tokens correctly landed in the right file — over an app whose grid header was
white on white and whose entire table stylesheet had never matched an element.

New skill, learned-css-that-never-applied.md. Deliberately NOT a restatement of
ui-loop.md ("judge from the screenshot") or check-design-reaches-app.sh ("did
the classes arrive"). Both are right and both were already here. A screenshot
tells you something is wrong; it cannot tell you whether your rule matched
nothing, matched too much, or matched and lost. That is the missing rung.

Three ways a perfectly correct rule paints nothing, all three met in one run:

  1. MATCHES NOTHING. ds.css styled .ds-tablewrap / table.ds-table. DataGrid2
     renders a row as <div class="tr">; there is no <table> on any page. Not
     one table rule had ever applied, through a full design port and two build
     phases. Separately, three top-bar selectors were guessed in one sitting
     and all three matched zero elements; one read of the live element chain
     named the real carrier.
  2. MATCHES TOO MUCH. The instinct after (1) is to widen. .mx-page painted the
     nav bar with the page ground: white menu text on a white bar.
  3. LOSES THE CASCADE. .mx-listview-empty named the right element, sat ~11,000
     lines after Atlas, and still lost — Atlas's 0,2,1 against a bare class's
     0,1,0, so source order was never consulted. And that rule sets
     border-style:none/background:transparent: Atlas is ERASING the element,
     not failing to style it.

The diagnostic is two reads before touching the rule: computed style, then a
parentElement walk only if the element was not found. The three possible
answers have three different fixes, which is the point — a blind rewrite would
have been wrong in all four field misses, and in case 2 the rewrite WAS the
outage.

Completion criterion carries a denominator, per authoring rule 4: every
component class a pass claims to have shipped gets one computed-style read
against a named element in the running app, or is reported unverified. The run
that justifies that wording had row tone implemented, screenshotted, and
looking fine while rendering a rejected project amber — two value reads caught
it, the eye did not.

Also records the check that comes first and that no CSS instrument can see,
because nothing is wrong with the CSS: at the moment the user called the UI
poor, four complete semantic tint sets, a five-step elevation scale and a
styled empty state were all present, all correct, and bound to ZERO widgets.
The design system was finished and the app was grey. Measure it as "N of M
component classes have at least one carrier in the model" — a zero-carrier
class is never fixed by rewriting it, only by binding it. Three of the five
improvements the user reacted to were pure binding of CSS already in the file.

Second, into learned-mdl-cannot-express.md: when MDL cannot set a class, CSS
usually can. The spec carried per-row tone as an M-sized MDL task prescribing
DynamicRowClass — a property that exists in neither MDL nor Studio Pro, written
months earlier against an imagined API. Whatever class MDL CAN bind is a hook
the stylesheet reads from anywhere in that subtree, so :has() off the
already-bound DynamicCellClass gave three pages row tone with zero MDL. Written
as a positive recipe rather than a prohibition (authoring rule 3): the failure
it retires is stopping at "MDL has no such property" when the requirement was
never about MDL.

Routed baseline for mdl/review/gate at stages 5-6, on this repo's own rule that
a skill living only in the situational table goes unnoticed by projects not
hunting for it — and not being reached is exactly what this one records.
render-routing.sh --check: surfaces in sync, baseline 75206 words of 80000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…0.21.0

This toolkit told four days of sessions that `mxcli marketplace install` would
corrupt their project. It stopped being true on 2026-09-06.

mendixlabs/mxcli#879 is closed in v0.21.0. Install no longer shells out to
`mx module-import`; it copies the module's units with mxcli's own writer, so
MPR v2 survives. The destructive path is opt-in behind --allow-format-change,
so the corruption can now only be produced deliberately.

Re-probed 2026-09-10 on a real 501-unit split-model project, four modules:

  501 -> 1075 .mxunit files     split format preserved
  .mpr 84 KB -> 156 KB          an index, not the 35 MB blob #879 produced
  native mxbuild                0 errors
  app boots; two e2e journeys unchanged (33/33, 12/12)

The cost of not re-probing is the finding here, not a footnote. The project
that found the fix carried "marketplace install is BLOCKED, needs a human in
Studio Pro" in its register for four days after it stopped being true — with
both .mpk files already downloaded and sitting in the project root the whole
time, because the blocker was never the download. retesting-learned-rules.md
exists for exactly this and nobody ran it. A STOP is a claim about a binary
version, and it expires.

So the entry now also carries the zero-risk way to re-probe a formerly
corrupting command on a REAL project, which is reusable well beyond this bug:
git clone --depth 1 file://$(pwd) into a scratch dir, install into the clone,
count .mxunit and run mx check there, decide from the measurement. Cheaper
than the documented recovery, and it answers what a changelog cannot.

The ledger entry is retitled RESOLVED with the old content kept verbatim below
a HISTORICAL RECORD heading, and the superseded prevention rule marked as
applying to v0.20.0 and earlier rather than deleted — a ledger entry that
vanishes reads as a bug never found, and projects pinned to older binaries
still need it.

THREE NEW TRAPS REPLACED THE OLD ONE. All live on v0.21.0, none self-announcing:

1. Marketplace packages do not carry their dependencies, and the failure does
   not say "module missing" — it says 406 individual CE1613 element errors.
   The entry carries the aggregation one-liner that turns that wall into a
   shopping list. Email Connector 6.4.3's closure is MxModelReflection +
   Encryption + CommunityCommons, which checks at 0 errors.

2. The two `mxcli fix` repairs the installer prints are mandatory, not
   advisory: 128 units and 12 units changed on the confirming run. Both
   subcommands exist precisely because Mendix's own tools would collapse
   MPR v2 without them.

3. A bundled widget can silently fail to land, and the installer cannot
   self-repair it. widgets/RichText.mpk landed on the probe and not on the
   real run — 33 widgets against 34, 3 x CE0462 over an otherwise clean model
   — and a re-run refuses with "already installed". Repair is to unzip it out
   of the .mpk the installer already downloaded. Count widgets/ before and
   after every install.

One thing genuinely still needs Studio Pro, and it is now scoped rather than
assumed: SAML + UserCommons install cleanly but leave CE0066 ("Entity access
is out of date"). There is no `mxcli fix security`, and forcing the recompute
with an MDL GRANT was tried and does not clear it.

bug-lookup.sh 879 now leads with the fix rather than the STOP.
render-routing.sh --check: surfaces in sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…er push

platform-link.md is the project-birth skill — create the app, PAT scopes,
adopt a model without rewriting history, deploy. Nothing covered the thing
every later session does: which remote does the team run from, and did my
push land.

Field run: a MOC/PSSR app replacement, 2026-09-07 to 2026-09-12. A Team
Server push was reported to the user as blocked three times across two
sessions and was not blocked any of those times. The knowledge existed the
whole time, in two places that could not be reached: platform-link.md at
tier ondemand, and a project-local .ai-context skill that cannot travel to
the next project.

- skills/teamserver-alignment.md: read the remotes rather than cache them,
  settle-then-push order, and the four checks that must fail before the
  word "blocked" reaches the user. Completion criterion is three matching
  values — remote, sha pushed, sha read back.
- Routed baseline for mdl/gate/review at stages 5-7. Baseline rather than
  situational is the fix; not being reached was the incident.
- platform-link.md gains a pointer and keeps its birth-skill scope.
- Rendered surfaces regenerated via bin/render-routing.sh; --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
… fit the length afterwards

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
The arc from first deploy to a configured, working app cost about three days.
Five of its six walls were knowable in advance, and these are the three that
generalise beyond one project.

1. A configuration mechanism is a requirement on the deployment target, not a
   free choice of the designer. Credentials designed to come from environment
   variables met a Mendix free sandbox, which has no configuration surface at
   all - four routes probed to exhaustion. The tier was in the very first
   /apps/{id} response days earlier and nothing asked for it. Proposes an
   environment-capability question at architecture time plus the one call that
   answers it, and corrects a factual error that had sat in a repo for weeks:
   Mendix has no password constant, and hiding a credential from users is
   attribute-level entity access.

2. A tolerant parser upstream of a strict one is a silent-failure machine. A
   secret at 1345 characters, which base64 can never be, decoded fine in Node
   and was rejected by Mendix with a message pointing at the wrong place.
   Filed with its false-green sibling - HTTP 200 on both calls and a reply
   that never arrived - and with the guard-string convention that did work.

3. cd X && cmd defeats every Bash permission allowlist, because rules match
   the start of the command line. Roughly fifteen turns went into writing new
   rules that also could not match, against an allowlist that had never needed
   changing.

Inbox drops rather than skill edits: these want triage to decide where they
land, and item 3 is harness friction rather than Mendix process. Item 1 also
offers a headless /xas/ driver for promotion, which has not been prepared
here because it has not yet met the field-proof bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
Two different things refuse an agent's command, they render identically in
the transcript, and only one of them can be fixed by editing a file. A
permission rule is a deterministic prefix match that ships with the repo and
behaves the same for every user on every device. A safety-classifier refusal
is a judgement, exists only in managed or sandboxed environments, and cannot
be allowlisted away at all. Conflating the two is what turned one
mis-diagnosis into roughly fifteen dead turns on 2026-09-16.

The skill is four checks before the word "blocked" reaches the user, ordered
by yield. The first is that a rule matches the START of the command line, so
an allowlisted tool call prefixed with cd matches nothing - invisible from
the refusal text, because the refusal is about cd, and free to fix because
the working directory persists between calls.

Placement is part of the finding rather than an afterthought. The rule was
first written into a project CLAUDE.md, which is the wrong home twice: mxcli
init overwrites it, and Copilot, Cursor and Windsurf never read it. So it
lives in skills/, referenced not copied, and reaches projects through the
baseline routing table their generated CLAUDE.local.md already carries.

It deliberately ships no table of harness settings paths or rule syntaxes.
Every agent tool has some allow/deny list, they are all spelled differently,
and they all move; a cached table would go stale and be trusted anyway. The
file states the principle and tells the reader to read their own config.

render-routing.sh --check passes: surfaces in sync, all skills routed or
exempted. Baseline budget is now 77,323 of 80,000 words, so the next
baseline routing needs a displacement rather than an append.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…itting one

Three additions from one incident on a MOC/PSSR app replacement, 2026-09-16.
Two chat pages that already had floating buttons on the home page were
diagnosed as unreachable and given navigation menu items. The entries were
redundant, and broken.

ui-preflight-pages.md gains Step 0, written as a recipe rather than a ban:
before building a page or adding a way into one, list the entry points that
exist, using the reference tools - impact, callers, DESCRIBE PAGE of the home
page for buttons and snippet calls already placed. SEARCH '<PageName>' is a
text grep, not a reference check, and it did not surface the show_page action
sitting in the home page's widget tree. A browser scrape of navigation captions
could not match an icon-only floating button either, so a second silent
instrument agreed with the first and the pair was read as confirmation.

Then read the target page's own Layout. A page on a popup layout is a popup,
not a destination: it is opened by a button passing a context object, and a
navigation item cannot pass a page parameter. Route one through navigation only
where the requirement asks for a dedicated page.

learned-detection-gaps.md gains the register row, because the whole static
ladder passes this. check --references reports 0 errors, exec succeeds, and
SHOW NAVIGATION MENU reads the entry back cleanly. What the green certifies is
that the reference is valid - the page exists. Nothing on the ladder checks
whether a nav item is a legal way to reach it.

iterative-build-loop.md gains a commit rule, resolving a collision between two
of this toolkit's own rules. exec rewrites the .mpr and every mprcontents unit
it touched; a blind add -A is banned; so commits pass explicit pathspecs - but
an explicit pathspec cannot name a file you did not know changed. The revert
commit here named the .mpr and the script and left the exploded navigation unit
behind. Git reported 2 files changed and the tree looked finished, while the
.mpr disagreed with its own contents in the commit. Read git status --short
after every exec, before writing the pathspecs, with mprcontents/ as a
directory pathspec.

Verified by inspection rather than a suite run, per the scoped-testing rule:
render-routing.sh --check passes (surfaces in sync, all skills routed;
baseline 77,640 of 80,000 words), check-docs-numbering.sh passes on the three
staged docs, and the ten greps test-bug09-13.sh makes over
iterative-build-loop.md all still match - the change is purely additive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…t-setup-clhy8g

# Conflicts:
#	CHANGELOG.md
#	README.md
#	agents/gate-agent.md
#	agents/mdl-agent.md
#	agents/review-agent.md
#	bin/gate-check.sh
#	skills/conversion-runbook.md
The merge of origin/master conflicted in six files that are all render outputs
of bin/lib/skill-routing.tsv, which itself merged cleanly. Resolving by taking
master's side and re-rendering is correct - but the render ran AFTER git add,
so the merge commit captured the pre-render versions and the rendered ones sat
in the working tree.

Effect on the pushed branch: agent-permission-friction and teamserver-alignment
were present in the table and missing from README.md, conversion-runbook.md and
the mdl, gate and review agent surfaces. render-routing.sh --check against the
committed tree reports all five as drifted.

The --check that passed earlier was run against the working tree, which was
rendered. Same shape as the mprcontents defect this branch documents: a green
check over a superset of what was actually committed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
"Everything runs great locally" is never evidence about a deployment. On a
real project, an e2e suite that had never once run against its sandbox
reported FAIL demo.admin Administrator 0/6 [] on an app whose navigation
was entirely correct. Three localhost assumptions produced that, and none
of them was about the app: a fixed 2500ms post-login sleep tuned to a warm
local runtime, an ownership guard refusing a remote run over a stale local
port, and eight journeys whose oracle is a database the platform does not
have.

Seven steps: enumerate what the target lacks; poll for readiness instead of
sleeping for it; audit every guard for a localhost premise; report a
non-assertable check as SKIPPED with its denominator rather than PASS;
verify configuration through behaviour, not presence; package it as one
command; decide the handover shape from the evidence.

Two rules carry most of the value. Fix a guard's premise rather than
reaching for its escape hatch — using ALLOW_UNVERIFIED=1 would have
"worked" while teaching operators to keep the bypass permanently on. And a
skipped assertion reported as PASS is green-by-absence, the defect the
obligation check exists to prevent.

Routed ondemand at stages 7-8; surfaces re-rendered, --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants