Skip to content

Sync ako/mxcli: bundled skills adopt the Agent Skills standard, plus microflow-layout and OData fixes - #959

Open
ako wants to merge 18 commits into
mendixlabs:mainfrom
ako:main
Open

Sync ako/mxcli: bundled skills adopt the Agent Skills standard, plus microflow-layout and OData fixes#959
ako wants to merge 18 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

7 commits since v0.19.0. Mostly one theme — the bundled skills become discoverable — plus three fixes where the reported symptom and the real cause were different things.

Skills adopt the Agent Skills standard (#906)

The 67 bundled skills were flat <name>.md files with no frontmatter, written only to .ai-context/skills/. Nothing discovered them: routing was a hand-maintained table in the generated CLAUDE.md that had drifted to 12 of 67 named there and 31 of 67 in the README — 1.09 MB of guidance reachable through a 26 KB file listing a sixth of it. mxcli init created no .claude/skills/ at all.

  • Adopt the standard: frontmatter on every skill, .claude/skills/ written by mxcli init, so an agent finds a skill by description instead of by a stale table.
  • Split the nine largest into SKILL.md + reference files. The standard loads a skill in three tiers, and advises keeping SKILL.md under 500 lines; nine were 740–1906 and are the ones agents load most often, so the whole length was paid on every use. write-microflows went 1906 → 587 with four reference files.
  • The generated per-project widget docs were named skills and were not skills — no frontmatter, no SKILL.md, an _index.md whose leading underscore hides it from a glob. They landed at exactly the depth Claude Code scans, so they looked discoverable and were silently skipped. Now a real skill, and something routes to it.
  • One frontmatter block per skill. The migration prepended a block to all 67, including the one file that already had one — and everything after the first --- block is body, so that skill shipped its own frontmatter as prose. Also names the build step that was missing.

Microflow layout — two rewrites that stranded geometry

OData

  • CREATE OR MODIFY ODATA CLIENT never re-fetched $metadata. The modify branch updated every property except the cached contract, so a backend that gained entity sets and attributes re-imported as "Unchanged OData client" and SHOW CONTRACT ENTITIES kept listing the old shape. The only way out was DROP + recreate, which invalidates the client ID the existing external entities point at

claude and others added 18 commits August 21, 2026 14:33
The modify branch of createODataClient updated every property of a consumed
OData client except the cached contract. Only the create path fetched
$metadata, so svc.Metadata / svc.MetadataHash kept the snapshot taken when the
client was first created.

Reported against mxcli-formula1: the backend's F1LiveNowApi gained two entity
sets (Trace, Stints) and four attributes on Order, the contract file was
refreshed from the running backend, and re-running the CREATE OR MODIFY
reported "Unchanged OData client". SHOW CONTRACT ENTITIES kept listing three
entity types and the CREATE OR MODIFY EXTERNAL ENTITIES that followed imported
the old shape without saying so. The only way out was DROP ODATA CLIENT +
recreate, which invalidates the client ID the existing external entities point
at.

The modify branch now normalizes MetadataUrl the way the create path does — it
stored the raw value, so a relative './contracts/x.xml' ended up as a URL
neither Studio Pro nor fetchODataMetadata can open — and re-reads the contract
before writing.

Three behaviours are deliberate:

  * The refresh runs off the *effective* URL, so a CREATE OR MODIFY that omits
    MetadataUrl still converges the cache.
  * A failed fetch is a warning and keeps the cached contract. Losing one that
    was merely unreachable is worse than serving a stale one.
  * An unchanged contract leaves the service untouched, so the write is elided
    (ADR-0008) and the statement still reports "Unchanged".

ALTER ODATA CLIENT SET MetadataUrl has the same staleness and is not changed
here: it carries no design-time credentials, so a blind re-fetch of an
authenticated URL would warn where it used to be silent.

Verified on mxbuild 11.13.0 against a real project: 3 entity types cached on
create, "Unchanged" on a re-run with the file untouched, "Refreshed $metadata:
5 entity types" after the contract was replaced, all five sets imported by
CREATE OR MODIFY EXTERNAL ENTITIES, mx check 0 errors. The unit tests fail with
the reported symptom when the refresh is stubbed out, while the unchanged-
contract control passes either way.

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

CREATE OR MODIFY MICROFLOW moved every activity to where the script
asked and left the start where the PREVIOUS layout had put it, joined to
its own first activity by a long, mostly-empty diagonal across the
canvas. Measured on a real 11.13.0 project: activities at 360;340, start
at 40;200.

The cause was the fix for the opposite report. mendixlabs#884 was a describe->exec
round-trip MOVING a hand-placed start (145;200 came back as 100;200),
fixed by carrying the stored position over on every rewrite -- which then
pinned the start of every rewritten flow. Both reports are real and they
pull opposite ways, so neither is answerable without asking where the
stored value came from. A start at the derived spot (first activity X
minus one spacing unit, on its centre line) is mxcli's own arithmetic
handed back: it carries no intent, so it is re-derived and follows the
activities. A start anywhere else was placed by a person and is kept.

@start(x, y) states the position outright and beats both. It goes on the
first statement -- the one the start flows into -- because the start has
no statement of its own, the same placement @merge already uses for the
other implicit node. Without it there was no way to move a start once one
had been preserved, which is the other half of what mendixlabs#951 reported.
DESCRIBE emits it only for a non-derived start, so an ordinary
description does not grow a line restating its own arithmetic while one
carrying a hand-placed start round-trips exactly.

Emitted from BOTH describers: formatMicroflowActivities and
formatMicroflowActivitiesWithSourceMap are near-duplicates, and the first
cut patched only the second, so `describe microflow` dropped the line.
The test now runs every case through both.

Neither `mx check` nor a green build detects any of this: the Mendix
model carries no geometry rules, so a stranded start is a valid document
that builds and runs and is merely drawn wrong -- 0 errors on mxbuild
11.13.0 before and after. Verified by reading coordinates back off the
stored document.

Controls, each reverted individually and reproducing only its own
symptom: unconditional carry-over -> the stranded start returns; builder
arm removed -> @start does nothing; either describer's emission removed
-> the lossy round-trip returns; visitor arm removed -> @start does not
reach the AST. End to end on a real project: the mendixlabs#951 rewrite now lands
the start at 200;340, a hand-placed 145;200 survives a rewrite that does
not mention it, and Administration.SaveNewAccount describe->exec leaves
all nine coordinates identical.

Nanoflows and rules share the builder and the describer, so @start works
there too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
fix(microflows): stop a rewrite stranding the StartEvent (mendixlabs#951)
fix(odata): re-fetch $metadata on CREATE OR MODIFY ODATA CLIENT
…endixlabs#953)

Two defects in addStructuredInheritanceSplit, both invisible below Studio Pro.

1. The builder ended with `fb.posX = mergeX` — the merge's own centre — so
   whatever followed END SPLIT was drawn on top of the merge, joined by a
   zero-length sequence flow. addEnumSplit steps to mergeX + HorizontalSpacing/2
   and addIfStatement to mergeX + MergeSize + HorizontalSpacing/2; three
   builders, three conventions, and the type split's was zero.

   The IF convention is the one adopted here: the spacing constants are
   centre-to-centre and tuned for a 40px edge gap, and clearing a MergeSize-wide
   merge before a full-width activity needs MergeSize + half a pitch. CASE's
   HorizontalSpacing/2 leaves a following activity's left edge exactly touching
   the merge (measured: merge 890, activity 970, both edges at 910). That is a
   lesser, pre-existing nit and is deliberately left alone — changing it would
   re-lay-out every enum split ever written.

2. Branch width came from every branch body concatenated into one list and
   measured as a single left-to-right run. Branches are stacked vertically, so
   only the widest matters; summing them slid the merge right by an
   activity-plus-spacing per extra branch. layout.go's own
   measureInheritanceSplitStatement always took the max, so the builder
   disagreed with its own measurer. appendInheritanceBodies — the helper whose
   name invited this — is replaced by inheritanceBranchBodies, which keeps the
   branches apart.

Measured with mxcli describe (which prints the stored coordinates, so no
modeler is needed), mxbuild 11.13.0:

                         merge   after END SPLIT   branches
  three branches  before  1480   1480 (stacked)    720
                  after    920   1040              720
  four branches   before  1760   1760 (stacked)    720
                  after    920   1040              720   <- no drift
  uneven branches before  1760   1760 (stacked)    720, 880
                  after   1200   1320              720, 880

The control is the enum split with the same graph shape: byte-identical before
and after (merge 890, end 970), as are IF/ELSE and every other example.
mx check reports 0 errors on both the broken and the fixed project — the model
is valid either way, which is why this shipped. describe -> exec of the fixed
microflow reports "Unchanged", so the geometry round-trips.

The split builders had no positional coverage at all before this; the new tests
assert coordinates and pin the enum split so a later attempt to unify the two
cannot move it silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…endixlabs#906)

The 67 skills mxcli ships were flat `<name>.md` files with no frontmatter,
written only to `.ai-context/skills/`. Nothing discovered them. Routing was a
hand-maintained table in the generated CLAUDE.md, and it had drifted: 12 of 67
named there, 31 of 67 in the skills README, 1.09 MB of guidance reachable only
through a 26 KB file that listed a sixth of it.

`mxcli init` also created no `.claude/skills/` at all — it wrote
`.claude/commands/`, `.claude/lint-rules/` and `.claude/settings.json` — so in a
Claude Code session on an mxcli project, not one of the skills was visible to
the tool the project is mostly used with.

Each skill is now a `<name>/SKILL.md` directory with `name` and `description`
frontmatter, and init writes the tree twice: `.ai-context/skills/` for every
tool (referenced by the generated OpenCode, Cursor, Continue, Windsurf and Aider
configs) and `.claude/skills/`, the only path Claude Code scans. The
`description` says what the skill covers and when to reach for it, so the index
lives in the file it describes and cannot fall out of step with it. The CLAUDE.md
table stays as a shortcut to the ones worth reading first, no longer as the index.

The format is not new here: mxcli's three skill packs have shipped as SKILL.md
with frontmatter since they were introduced, and `mxcli skill add` already
installs them to `.claude/skills/<name>/`. This gives the prose skills the same
shape. Claude Code has supported it since 2.0.20, and merged custom commands
into skills while keeping `.claude/commands/*.md` working — the same
converge-but-keep-the-old-form-working line taken here.

Upgrading a project retires the flat files older binaries wrote, which the sync
has never done before. That is deliberate: the sync only ever added and
overwrote, which was right while the layout was stable and exactly wrong across
a layout change — without it every upgraded project keeps 67 orphans beside the
new tree, and an agent reading the directory cannot tell which copy is current.
Only names mxcli itself ships are removed, and only once the replacement exists,
so a skill the user wrote — which the docs explicitly invite — is never touched.

Verified end to end: init writes 67 SKILL.md to both locations, all with valid
frontmatter; a simulated upgrade retires the legacy files, keeps the user's own,
and is silent on the second run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
The per-project widget docs were named skills, written into a skills directory,
and were not skills: no frontmatter, no SKILL.md, an `_index.md` whose leading
underscore hides it from a glob. In the `.claude` case they landed at exactly
the depth Claude Code scans — `.claude/skills/widgets/` — so they looked
discoverable and were silently skipped. Nothing routed an agent to them either:
one line at the end of a 1,395-line skill said how to GENERATE them.

The content was the weaker half of the two channels that exist. Measured on a
42-widget project, before:

  134 enumeration rows naming   0 permitted values
   23 object rows exposing      0 child properties
   75 descriptions cut mid-sentence at 77 chars ("...to denote number posit...")

`mxcli widget describe` reads the SAME mpk.PropertyDef and printed all of it —
enum sets, nested children, categories. Only this renderer dropped them, on
exactly the properties an author cannot guess. After: 171 of 171 enumeration
rows name their values, 213 nested rows appear, 0 truncations. Pipes in cells
are escaped, which they never were — one in a description silently split the row
into extra columns.

Four changes:

1. The renderer emits enum values, nested object children (recursively, system
   properties still excluded), whole descriptions, and the property group.

2. `_index.md` becomes `SKILL.md` with generated frontmatter whose description
   NAMES the project's widgets — "Badge, Area chart, Data grid 2 …". A
   hand-written skill cannot do that, so the listing now answers "does this
   project have a chart widget" for no context cost. The index links each
   per-widget file, which is what makes them supporting files rather than inert
   ones. The old `_index.md` is removed on regeneration.

3. `.def.json` files are refreshed BEFORE rendering. They carry the MDL child-slot
   routing, and without them the example collapses to a bare one-liner — so
   `mxcli widget docs` on a fresh project emitted a data grid documented as
   `PLUGGABLEWIDGET '…' widget1` with nothing about columns, while `refresh
   catalog` happened to produce the full version. The output depended on which
   command ran last. Widgets with no extractable def now fall back to the
   embedded registry definition, which covers the built-ins that are deliberately
   never extracted (combobox, gallery, image, …) and were being reported as
   failures: 9 such widgets, now 4, and those four are leaf widgets with no child
   blocks to route.

4. The body routes onward instead of pretending to be the whole reference —
   `mxcli widget describe <name>` for data read live from the .mpk, and for the
   dynamic visibility rules these files do not render at all. `create-page` now
   points at the skill rather than at the generator command.

Docs are written to `.ai-context/skills/widgets/` AND `.claude/skills/widgets/`,
matching what `mxcli init` does for the bundled skills; it used to be either/or,
so a Claude project with `.ai-context/` present got its widget docs in only one
of the two trees.

Corpus grows 61 KB → 149 KB, which is the point of supporting files: none of it
loads until a reader opens one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
fix(microflows): stop SPLIT TYPE stacking its merge and what follows (mendixlabs#953)
…ce files

The Agent Skills standard loads a skill in three tiers — every description
always, the SKILL.md body when the skill is used, a supporting file only when
the body sends a reader to one — and advises keeping SKILL.md under 500 lines.
Nine of the bundled skills were between 740 and 1906, and they are the ones
agents load most often, so their whole length was paid on every use.

  write-microflows       1906 -> 587    (4 reference files)
  create-page            1395 -> 444    (2)
  odata-data-sharing     1369 -> 304    (2)
  java-actions           1209 -> 541    (2)
  generate-domain-model  1145 -> 234    (2)
  atlas-design            811 -> 438    (2)
  write-oql-queries       800 -> 565    (1)
  test-microflows         778 -> 499    (1)
  system-module           740 -> 432    (1)

The body keeps the decisions, the shape and the gotchas — what is needed every
time. The catalogues and long build-outs move out: the widget catalogue (862
lines of create-page), the four OData walkthroughs, the domain-model syntax
tables, the Atlas inventories, the anti-pattern and unsupported-syntax lists.

Sections are moved WHOLE and never rewritten, so nothing can be lost in the
edit. Verified mechanically: every `## ` heading that existed before still
exists, in the body or in a reference file, for all nine.

Two properties this depends on now have tests:

  * Every supporting file is linked from its SKILL.md. An unlinked one is dead
    weight — Claude follows a link it can see and nothing else — and that cannot
    be checked by reading a single file.
  * No SKILL.md exceeds 700 lines. Deliberately looser than the documented 500:
    a body that is all essential beats one that hit a number by dropping
    something. It is what pulled in the last three skills, which were over the
    bound with no supporting files at all.

The bound test also removed the need for an allowlist, which is why nine were
split rather than the six largest.

One real break the split caused, now fixed: a link from moved content to a
sibling skill (`../generate-domain-model/SKILL.md`) needs another `../` from
inside `reference/`. The five remaining broken relative links in the skill set
pre-date this change and point at repo paths that do not resolve from a shipped
project either way.

`make check-skill-mdl` still finds and validates the same 206 MDL blocks — the
checker recurses, so moving a block into `reference/` keeps it covered — and
`mxcli init` delivers all 17 reference files into both skill trees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
… that was missing

Three findings from a real verification run against a two-app Mendix solution
(mxcli-formula1 finding 68).

1. `custom-widgets/SKILL.md` shipped with TWO frontmatter blocks. The mendixlabs#906
   migration prepended a `name`/`description` block to all 67 skills, including
   the one file that already had one under its pre-rename name
   (`mendix-custom-widgets`). Everything after the first `---` block is body, so
   the old block rendered as a stray fence and a duplicate `name`. The two are
   merged, keeping the old block's better trigger words — child slots
   (TEMPLATE/FILTER), `.def.json`, engine internals — which the migration had
   dropped.

   The pre-state was known: 1 of 68 files already had frontmatter was measured
   and written down before the migration ran, and the script still did not
   branch on it.

2. Nothing could see it. `TestEmbeddedSkillsCarryAgentSkillsFrontmatter` matches
   the FIRST block and validates it, which is correct — and precisely why a
   second block is invisible. A rename-and-prepend is exactly the shape of change
   that produces one, so the test now rejects a second block outright rather than
   checking the first one harder.

3. `make test` did not depend on `sync-all` while `make build` did. The embed
   dirs under cmd/mxcli are generated from .claude/, so a bare `go build` after
   the skills layout changed left `cmd/mxcli/skills` stale and failed six tests —
   and the failure said "the embed directive is broken", naming a directive that
   was fine instead of the build step that was missing. `make test` now syncs,
   and the message names `cmd/mxcli/skills` and `make sync-skills`.

Verified by reproduction: emptying `cmd/mxcli/skills` now prints the new message,
and `make test` restores the directory and passes on its own. The frontmatter
guard fails against the unfixed file and passes after.

The report attributed the first defect to PR #222. It was not: that diff touches
only the `description:` inside the first block, and the stray `---` appears in it
as unchanged context. `git log -S 'name: mendix-custom-widgets'` places it in the
migration commit. Recorded in the symptom table, because the reasoning error —
blaming the change you happen to be looking at — is the reusable part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
feat(widgets): make the generated widget docs a real, complete skill
refactor(skills): split the largest skills into SKILL.md plus reference files
fix(skills): one frontmatter block per skill, and name the build step that was missing
Resolves the one predicted conflict: #222 appended a sentence to the
`custom-widgets` description while #224 rewrote that line to merge the two
frontmatter blocks the migration had left behind. The resolution keeps #224's
single block (the stray `name: mendix-custom-widgets` block is gone) and #222's
pointer to the generated `widgets` skill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
feat(skills): widget skill and progressive disclosure (merges #222 and #223 into main)
Upstream cut v0.19.0, which rewrote CHANGELOG.md by moving [Unreleased]
into a [0.19.0] section. The fork kept appending to [Unreleased], so the
two sides rewrote the same region and the merge conflicts. It is the only
conflicting file, and the only file upstream changed.

Resolved semantically rather than by picking a side:

  - [0.19.0] is taken from upstream verbatim -- it is the release record.
  - [Unreleased] keeps only the entries describing work NOT in that
    release. 18 of the fork's 20 [Unreleased] bullets were already inside
    upstream's [0.19.0]; carrying them forward would have listed released
    work as unreleased.

The two that remain are both mendixlabs#951 (`@start(x, y)`, and the rewrite no
longer stranding its start event).

Verified: 0 bullet titles lost from either side, and the 8 duplicate
titles in the result are the same 8 present in both inputs, so none were
introduced.

Note: the other unreleased work on this branch -- the Agent Skills
migration (mendixlabs#906), SPLIT TYPE (mendixlabs#953) and the OData $metadata re-fetch --
has no CHANGELOG entry on either side. Not added here; conflict
resolution should not invent release notes.
Merge upstream v0.19.0 into main (resolves the CHANGELOG release-cut conflict)
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