diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a00f1..a149d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templat Credit the person or project that surfaced the change — the credit line is the thank-you. ## 2026-09-16 +- new(deploy-to-sandbox): **a deployment is untested until a test has run against that URL — local green is a prerequisite, never a substitute.** Seven steps for promoting an app to a cloud sandbox: enumerate what the target lacks (a Mendix Free App Sandbox has no M2EE admin API, no runtime-settings UI, no deploy API, no allowlist, and cold-starts); 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, never PASS; verify config through behaviour rather than presence; package it as one command; and decide the handover shape from the evidence. Field evidence 2026-09-16 — 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 it and **none was about the app**: a fixed `waitForTimeout(2500)` tuned to a warm local runtime (a poll for rendered `mx-name-*` widgets read all six items back seconds later); an ownership guard refusing a REMOTE run over a stale LOCAL port out of `stack.env`, whose port-ambiguity premise cannot apply to a named host; and eight journeys whose oracle is the local database reporting a healthy sandbox as broken (`DB read failed ... for PlantA`). After the fix, 4 passed / 0 failed / 8 skipped of 12, and J1 went 0/3 roles to 3/3. Two rules carry most of the value: fix a guard's premise rather than reaching for its `ALLOW_UNVERIFIED=1` escape hatch, which teaches operators to keep the bypass permanently on; and a skipped assertion that reports PASS is green-by-absence — Maurits Visser, from a MOC/PSSR app replacement +- learn(ui-preflight-pages): **Step 0 — enumerate what already reaches a capability before building a page or adding a way into one.** Positive recipe, not a ban: list the existing entry points with the reference tools (`mxcli impact`, `mxcli callers`, `DESCRIBE PAGE ` for buttons and snippet calls already placed), then read the target page's own `Layout:`. Two things it prevents, both measured 2026-09-16: `SEARCH ''` is a **text grep, not a reference check** — it did not surface the `show_page` action sitting in the home page's widget tree, so two chat pages that already had floating buttons (`btnAgentFab`, `btnGraphFab`, each passing a `ChatContext`) were diagnosed as unreachable; and a browser scrape of navigation captions could not match an **icon-only** floating button, so a second silent instrument agreed with the first and the pair was read as confirmation. The layout half is the sharper rule: a page on a popup layout (`ConversationalUI.Layout_FloatingPopup` and kin) 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, so route one through navigation only where the requirement asks for a dedicated page. Done when you can name every existing entry point and say why another is needed — Maurits Visser, from a MOC/PSSR app replacement +- learn(learned-detection-gaps): register row — **a navigation menu item pointing at a popup-layout or parameterised page passes the entire static ladder.** `check --references` reports **0 errors**, exec succeeds, and `SHOW NAVIGATION MENU` reads the entry back cleanly; the first oracle is live runtime or reading the target page's own `Layout:` and parameter list. Note precisely what the green certifies — the *reference* is valid, the page exists — and that nothing on the ladder checks whether a nav item is a legal way to reach it — Maurits Visser, from a MOC/PSSR app replacement +- process(iterative-build-loop): **"committing after an exec — enumerate, then name", resolving a collision between two of this toolkit's own rules.** `mxcli exec` rewrites the `.mpr` *and* every `mprcontents/*.mxunit` it touched; a blind `git add -A` is banned (it sweeps in a concurrent session's staged work, and in a gutted-tree state commits a ~108 MB blob), so commits pass explicit pathspecs — but **an explicit pathspec cannot name a file you did not know changed.** The resolution is `git status --short` after every exec, before writing the pathspecs, with `mprcontents/` as a directory pathspec being the right grain. Field evidence 2026-09-16: a commit named the `.mpr` and the script and left the exploded navigation unit behind; git reported `2 files changed`, the tree looked finished, and the `.mpr` disagreed with its own contents in the commit — an internally inconsistent Team Server checkout that nothing in the exec, the check or the commit mentioned, surfaced only because a stop hook noticed a dirty tree — Maurits Visser, from a MOC/PSSR app replacement +- new(agent-permission-friction): **two different things refuse an agent's command, they render identically, and only one can be fixed by editing a file — telling them apart is the skill.** 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/sandboxed environments (cloud, container, web session) where a local CLI user never sees it, carries a bracketed category label, and **cannot be allowlisted away** — so the guidance is: if you cannot tell, assume classifier and reshape, because reshaping is free and rule-editing when the rule was never the problem is what cost roughly **fifteen turns** in the field run (2026-09-16). Four checks before the word *blocked* reaches the user, highest-yield first: (1) **is the command line compound** — a rule matches the START of the line, so `./mxcli exec ...` matches an allowlisted `./mxcli` rule and `cd /path && ./mxcli exec ...` matches **nothing**, which is invisible from the refusal text because the refusal is about `cd`, and the remedy is free since the working directory persists between calls; (2) does it merely *resemble* something dangerous — renaming a credential variable on the command line reads as exfiltration prep, a heredoc near a credential is refused even when the body only *discusses* one (writing the file and passing a path worked every time and the heredoc worked none), and anything that reads as redirecting traffic or rewriting the agent's own config; (3) **a refusal is sticky for the rest of the turn** — an unrelated, fully permitted command was refused with the *same* label and succeeded unchanged on a fresh turn, so a second denial is not evidence about the second command and the move is to end the turn; (4) only then read the settings the harness actually reads, because in the field run the correct rule had been there since scaffold. Completion criterion carries its denominator: the word *blocked* is legal only after naming which of the four checks failed. Carries its own placement rule, learned the same day: this knowledge does **not** go in a project `CLAUDE.md`, which `mxcli init` overwrites and which Copilot/Cursor/Windsurf never read — it lives here, referenced not copied, and reaches projects through the baseline routing table in their generated `CLAUDE.local.md`. And it deliberately ships **no table of harness settings paths or rule syntaxes** (authoring rule 5): every tool has some allow/deny list, they are all spelled differently and they all move, so the file says read your own harness's config and reports only the principle that generalises — Maurits Visser, from a MOC/PSSR app replacement +- process(skill-routing): `agent-permission-friction` routed **baseline** for all agents, trigger-phrased on *"any refused, denied or blocked command — BEFORE rewriting a permission rule and before telling the user a tool is blocked"*. Baseline rather than situational because a session in a denial loop is not browsing a situational table, and on this repo's standing rule that a skill living only there goes unnoticed by projects that are not hunting for it. Baseline budget after the add: **77,323 of 80,000 words** — headroom is now under 3,000, so the next baseline routing needs a displacement, not an append — Maurits Visser, from a MOC/PSSR app replacement +- process(contrib/inbox): **three drops from a deploy-and-configuration retrospective — the arc from first deploy to a configured, working app cost about three days, and five of its six walls were knowable in advance.** (1) *"Read it from the environment" is only a design if the environment can be written to* — an app whose credentials were designed to come from environment variables was deployed to a **Mendix free sandbox, which has no configuration surface at all**: no constants tab in the free-app portal, Deploy API v4 has no constants endpoint (24 environment sub-resources probed, **all 404**), Deploy API v1 has one but is a licensed-node API that rejects PATs by design (four auth-header combinations tried, and a full-rights key authenticates and still does not apply), and the app's own `/xas/` protocol reaches app data and never platform configuration. The tier was in hand the whole time — `"licenseType": "free"` was in the very first `/apps/{id}` response, days earlier — and nothing in the process asked for it, so the proposal is an environment-capability question at architecture time plus the one-call probe that answers it. Carries a correction worth having: **Mendix has no "password constant"** — a constant has Name, Type, Default Value, Documentation, Export Level and Exposed to Client, and hiding a credential from users is attribute-level entity access (the way `MxGenAIConnector` omits `AccessToken` from its read grant), so an instruction to "mark it as a password in Studio Pro" had sat in a repo for weeks being followed by nobody because it could not be followed. (2) *A tolerant parser upstream of a strict one is a silent-failure machine* — a GenAI configuration key supplied at **1345 characters, `1345 % 4 == 1`, which base64 can never be**; Node's decoder swallowed the stray `=` and reported success, Mendix's rejected the import with a message pointing at the portal, i.e. at the one place that was not the problem. Plus the false-green sibling: a chat path returning **HTTP 200 on both calls** with the reply stuck `Loading`, empty, and no trace rows past **153s** — the transport succeeded and the product did not happen. And the convention that did work, a guard string naming which of *unconfigured* / *broken* / *empty* you are in, at the cost of one string literal. (3) *`cd X && cmd` defeats every Bash permission allowlist* — rules match the **start** of the command line, so an allowlisted `./mxcli` call prefixed with `cd` matches nothing; ~15 turns were lost writing new rules that also could not match, against an allowlist that had never needed changing, and the working directory persists between calls so the fix is free. Expensive because session cost tracks turn count × context size, not output length — Maurits Visser, from a MOC/PSSR app replacement - fix(assemble-prototype.js): **a screen's inline `