Conversation
…sed out Triggered by the irreducible-graph work landing (named join points, then Mode 2 and Mode 3 describe). Sources gained the two new describe-side files, the graph classifier and the proposal; the existing three kept. Three changes, grounded in the 13 mdl/executor findings since the last sync. A FIFTH SHAPE at the end of the survival ordering: "means something else". The other four all leave evidence — output that will not parse, a missing property, an invented clause, vanished structure. This one parses, executes, yields a valid model, drops nothing and invents nothing, and denotes a different program. No automated signal can see it; only comparing behaviour finds it. SOME DOCUMENTS CANNOT BE DESCRIBED FAITHFULLY AT ALL, which is a vocabulary limit rather than a careless describer: MDL's `if` is single-entry/single-exit against an arbitrary digraph, so the remedy is to extend the language, and per Boehm-Jacopini one sub-class has no faithful rendering at any effort. The beat earns its place by telling a reader whether to reach for a fix or a proposal. VERIFICATION rewritten around the trap that let shape five survive: a round trip that is a fixed point on the corpus you have is not evidence it is faithful, because the describer's inference rule was reverse-engineered from that corpus and agrees with it by construction — the disagreeing case has to be constructed. Plus three measurement rules that each hid a defect until applied: compare identities rather than counts; diff the whole corpus against a baseline binary (a refactor must be byte-identical, a feature must move only its targets); algebra is not behaviour. Also removes the quoted "83 of 248" count — the area has since more than doubled to 615, so the figure was simply wrong. The Do-not-duplicate line now says counts are computed, per CLAUDE.md's "coverage is computed, never quoted". Separately, three findings written 2026-09-13/14 used the shard filename (mdl-executor) as their `area` where the other 613 use the path (mdl/executor), so digest-status bucketed them into "(32 areas < 5)". Corrected in place before using those counts to choose this run's scope. This is the THIRD occurrence — the same correction was made on 2026-09-12 — and the repetition is the tell that correcting is not fixing: check-findings validates that the field exists, not that its value is in the vocabulary. Two SYNC_LOG rows appended, one for the correction and one for the page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The 2026-06-05 adoption plan stops at the cutover and still reads as though legacy were the default. This covers what it deferred to "Phase 5 — Cleanup" and never specified. The plan's whole job is refusing to treat one name as one job. Retiring the legacy ENGINE (mdl/backend/mpr, 2,808 lines, five production importers) is small, unblocked and reversible. Retiring the legacy SERIALIZER underneath it (sdk/mpr, 41,243 lines) is fifteen times larger and gated on consumers that never touched the engine seam — the public api/ package, the MCP backend, eight cmd/mxcli commands. One measurement reorders the sequence. The earlier plan deferred the mongo-driver v1->v2 migration "to the cutover", reasoning the engines could coexist on different module paths. They can, but the split does not fall on the engine flag: modelsdk/ is 117 files v2 and 0 v1; sdk/mpr is 113 v1 and 0 v2; the adapter between them straddles both. So deleting the legacy backend moves the driver migration NOT AT ALL — what unblocks v2 is deleting sdk/mpr, which is behind a product decision about the public API. Sequenced the other way, phase one looks like it owes a 41k-line migration and never starts. Everything in the "already true" section was verified rather than inherited: no "not supported by the modelsdk engine" message remains; errUnimplemented cannot fire on a default run (17 of 19 methods measured dead, the 2 live ones implemented, the set pinned by a passing test); the dependency runs one way (modelsdk does not import the legacy backend — a grep says it does and is matching a comment); legacy is strictly weaker. Also records what goes stale on removal, including that the modelsdk package doc still describes the engine as a read slice whose write methods are unimplemented and whose CLI prints a read-only warning — three false clauses, the warning having been gone for some time. Phases 3 and 4 are deliberately NOT scheduled: they need an answer about whether anything outside this repository depends on api/, which is a maintainer question rather than a measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Two of the workflow constructs only Studio Pro could set, which #466 made rewrites refuse rather than reset (phase 1 of PROPOSAL_workflow_studio_pro_constructs.md): user task … on created microflow Mod.MF create workflow … on workflow events (UserTaskStarted, …) microflow Mod.MF as '…' create workflow … on any workflow event microflow Mod.MF as '…' Both engines write and read them in the shape ako/TestApp (11.14.0) stores, describe emits them, and a rewrite that restates them proceeds; one that declares fewer than are stored is refused. Measured on mxbuild 11.13.0 first: on-created takes exactly (WorkflowUserTask, context) in either order (CE6683) and returns nothing (CE5012); a handler takes exactly (WorkflowEvent, WorkflowRecord, WorkflowActivityRecord) in any order (CE6691); and the build does NOT check event type names — an invented one builds at 0 errors — so mxcli refuses an unknown name (MDL-WF12) and one measured absent from the project version. Studio Pro stores the full list for "every event", so `any` expands per version from a table measured off the 11.6/11.10/11.13/11.14 mxbuilds (32/36/42/42), and is refused below 11.6. Fixes found on the way: - legacy parser read OnCreatedEvent as a string, so every on-created microflow read back as none; - none of the workflow rewrite guards worked on the legacy engine: its raw units carry arrays as primitive.A, which the guards' []any switches never matched, so a rewrite the modelsdk engine refused deleted stored state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(workflow): author event handlers and on-created microflows
…er problem Two questions from review, both of which the first draft got wrong by reasoning from line counts instead of measuring callers. MCP STAYS, and that is compatible. Its entire sdk/mpr dependency is ONE call — mpr.Open(path) for a deliberately read-only reader, since writes already go over MCP to Studio Pro. The naive port looks impossible and the reason is worth recording: MCP calls 37 methods on that reader and modelsdk/mpr.Reader has 7. But that is the wrong comparison — modelsdk/mpr is a unit/raw reader and the semantic decoding lives a layer up. Against the modelsdk BACKEND, 33 of 37 are already implemented. MCP composes the codec backend for reads; its 15,619 lines are MCP protocol surface and are untouched. api/ IS NOT DEPENDENT ON THE LEGACY BACKEND. It imports mdl/backend zero times — it bypasses the abstraction entirely and holds a concrete *mpr.Writer. The dependency is five symbols and seventeen methods, and FIFTEEN of the seventeen are already implemented on the modelsdk backend, with all seventeen already declared on FullBackend. The two missing (AddAttribute, UpdateAttribute) are missing precisely BECAUSE api/ is their only caller, and ALTER ENTITY's mutator is the template for both. The builder surface need not change, so the public API break the first draft treated as a gate is not required. The unifying finding, and the reason Phase 3 is now sequenced rather than deferred: unreachableUnimplemented is a CENSUS OF WHO BYPASSES THE ABSTRACTION, not dead weight. Its reason column names api/, the MCP backend, and the cmd/mxcli commands that hold a concrete reader on purpose. Those methods are on the interface because those callers exist and unreachable because those callers do not use a backend value. Closing the bypasses makes them reachable or genuinely deletable — and only after that is sdk/mpr's fate a question at all. Phase 3 accordingly drops from "L, gated on a product decision" to "M", the a/b/c options are replaced by a four-step sequence, and MCP is recorded as a fixed constraint rather than a decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Merging main moved three of the numbers the plan asserts: sdk/mpr 41,243 -> 41,418 lines and 113 -> 114 v1 files, mdl/executor 17 -> 18. The caller measurements that carry the argument are unchanged — api/ still imports mdl/backend zero times and needs 17 methods of which 15 are implemented, MCP still needs 33 of 37, the reachability test still passes, and there are still no "not supported by the modelsdk engine" messages. A plan whose figures rot is the specific failure this one was written to correct in its predecessor, so the verification line is now dated to the merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Phase 2 of PROPOSAL_workflow_studio_pro_constructs.md. An AI agent task
(Workflows$AIAgentTaskActivity, Mendix 11.9+) stores exactly the
call-microflow shape under a different $Type (ako/TestApp, 11.14.0), so it is
the call-microflow statement with `agent` added and, in the semantic model, a
CallMicroflowTask with IsAgent — every walker, validator, catalog edge, naming
rule and ALTER path applies unchanged; only $Type, describe and the rewrite
guard branch on it.
call agent microflow HR.Classify as aiAgentTask1 comment 'Classify'
with (Request = '$WorkflowContext')
outcomes true -> { … } false -> { };
Measured on mxbuild 11.13.0 by writing each shape as a call microflow and
switching only the $Type: everything builds the same (context, System.Workflow
or several parameters; Boolean and enumeration outcomes; boundary events)
except an agent microflow with no parameters, which fails CE1590 — the one
new check. Gated on workflows.ai_agent_task (11.9.0) for CREATE and ALTER.
Describe used to print an agent task as a comment, so a rewrite refused the
whole workflow; it now emits `call agent microflow` and the guard counts
restated agent tasks. The modelsdk engine writes and reads it; the retiring
legacy engine carries the $Type through rather than downgrading the task; the
MCP backend sends it as AIAgentTaskActivity — the shape Studio Pro 11.14's
ped_get_schema declares.
Verified in Studio Pro 11.14 over MCP: the agent task, handler and on-created
shapes are accepted and read back as written; the on-created and handler
signature errors match mxbuild's CE6683/CE6691 text; an agent task without
parameter mappings is flagged; an invented event type is refused at create.
Fixed on the way:
- MCP mapper hard-coded onCreatedEvent NoEvent and sent no onWorkflowEvent,
silently dropping phase 1 over MCP; and Studio Pro 11.14's workflow
constructor takes `context` and a string workflowName, so every workflow
create over MCP failed. The shape is now read off the live constructor
schema.
- describe printed a call microflow's caption only as a `--` comment, so
describe -> exec replaced it with the microflow's name; an authored caption
is now `comment '…'`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
api/ imported mdl/backend ZERO times. It was not "dependent on the legacy backend" — it bypassed the abstraction entirely, holding a concrete *mpr.Writer handed to api.New. That is why AddAttribute and UpdateAttribute sat unimplemented on the codec engine: api/ was their only caller and it did not call them through a backend value. api.New now takes a backend.FullBackend, with an api.Open convenience for the common case. The builder surface is untouched, so this is not a breaking change to anything but the constructor — and nothing in the repository imports the package, so the blast radius in-tree is its own tests. The gain is not tidiness: the same fluent builders now run against any backend, including a live Studio Pro over MCP, which was unreachable while the package held a concrete writer. Open owns the connection it makes; a backend passed to New belongs to the caller and Close leaves it alone. AddAttribute and UpdateAttribute are implemented on the codec backend and struck off unreachableUnimplemented, which failed loudly when they became reachable — exactly what that pin is for. Two properties are worth the tests they get: the attribute keeps its stored $ID (the runtime keys on identity), and the list keeps its order. The second is not free — the generated list offers only Append and Remove, so an in-place replace means rebuilding it, and the naive remove-then-add moves the edited attribute to the bottom of the entity. Control: breaking the rebuild fails the order test. THE SUITE THAT SHOULD HAVE VERIFIED ALL THIS HAD ONLY EVER SKIPPED. api/'s ten integration tests pointed at ../mx-test-projects/test-source-app, which is not in the repository, so every one of them skipped on every machine and in CI since they were written — the mendixlabs#808 shape, and it meant `go test ./api/` passing said nothing about a refactor that rewrote every call site in the package. They now run against the committed testdata/expr-checker fixture the codec backend's own tests use, and a missing fixture is FATAL rather than a skip, because a committed fixture's absence is a broken checkout rather than an optional dependency. Ten tests, zero skips, all passing against the codec engine. First step of Phase 3 of docs/plans/2026-09-14-retire-legacy-engine.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
MCP opens the project read-only and sends writes to Studio Pro, so it needs a semantic reader — and it held a concrete *mpr.Reader for one, because the codec backend did not offer three of the reads it takes: GetDomainModelByID, GetWorkflow and ListNavigationDocuments. That is the entire reason those three sat on FullBackend with no caller through a backend value; the reachability pin names MCP for each. Implements them in mdl/backend/modelsdk/mcp_bypass_reads.go. Each is a re-keying or a widening of a read this package already does, so all three share their conversion (domainModelFromGen, workflowFromGen, navProfileFromGen) with their siblings — workflowFromGen is extracted from ListWorkflows for this, so a single-item read cannot drift from the same item in a listing. Adds Backend.ConnectReadOnly: Connect opens ReadOnly:false and would lock the file Studio Pro owns, which is the one thing MCP must not do. Connect had no test — 190 in the package, not one called it — so the swap would have landed unverified with the suite green. connect_reads_test.go covers it, with two controls: reverting ConnectReadOnly makes the write-refusal test fail with the reported symptom, and a companion test shows the same write succeeds read-write, so the refusal is not incidental. The workflow read seeds a workflow first; the fixture has none, so looping over the listing would have passed without calling GetWorkflow at all. Step 2 of Phase 3 in docs/plans/2026-09-14-retire-legacy-engine.md. Five methods struck off unreachableUnimplemented across this and the api/ port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A layout whose placeholder is not named `Main` passed `mxcli check -p
--references` AND `mxcli exec` ("Created layout"), then failed the build
with CE0848 — with no DROP LAYOUT to undo it (mendixlabs#1063).
The rule, measured on Mendix 11.12.1 against a layout NO PAGE USES, so
none of it depends on a page binding to the layout:
placeholder Main -> 0 errors
placeholder Main + Content -> 0 errors (extra names are fine)
placeholder Content -> CE0848 "No placeholder with the name
'Main' found. There should be exactly one."
placeholder Main + Main -> CE0849 + CE0495
placeholder Main + Side + Side -> CE0495 "Duplicate name 'Side'."
So there are two independent halves, and CE0495 shows the second is not
about Main at all: placeholder names must be unique, whatever they are.
The root cause was a documented belief rather than an oversight. CLAUDE.md
said "Which placeholder is 'main' is a naming convention (22 of 22 name one
`Main`; a page binds by qualified name anyway)", the layouts skill said "the
convention is the mechanism", and the write-time guard implemented exactly
that: hasAnyPlaceholder accepted any placeholder under any name. The check
that existed matched the docs, which is why looking for a missing guard would
never have found this — the rationale was what needed verifying.
The rule now lives once, in types.CheckLayoutPlaceholderNames over bare
names, and is applied by both `mxcli check` (MDL081 for the Main rule,
MDL082 for duplicates, in the project-free pass so CI reaches it) and the
writer. Two copies in two currencies is how a resolver drifts.
An error rather than a warning: unlike MDL078 nothing here is a snapshot of
a Mendix asset that a later version might extend, so a flagged layout cannot
build today.
Verified end to end on mxbuild 11.12.1: the reporter's script is now refused
at check time (exit 1) and writes nothing, and `Main` + `Side` builds at 0
errors. Control: stubbing the shared rule to always-OK makes both the
executor and writer tests fail with the reported symptom.
Refs: mendixlabs#1063
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
`placeholder Main { }` inside a CREATE LAYOUT passed `mxcli check`, then
failed at exec with `layout "X" declares no placeholder` — a message that
flatly contradicts a script which plainly says `placeholder Main`. The
failed exec had already created the module (mendixlabs#1063).
One grammar rule serves two opposite jobs, told apart by shape rather than
by context: `if c.LBRACE() == nil` makes a bodiless placeholder a
DECLARATION widget, while the braced form is routed to
buildPagePlaceholdersV3 — the page-side job of FILLING a layout's slot. In
a layout there is no such job, so the braced form was dropped on the floor
and the layout ended up with zero placeholders.
When one parse rule is disambiguated by shape, the wrong shape has no error
path by construction: it silently means the other thing. Reaching for the
braced form here is also the natural mistake, since every other layout
element takes a body and `alter page` uses it for real. DESCRIBE emits the
bodiless form, so round-tripping never produces this and no test covered it.
The Builder now carries `inLayout`, saved and restored around the layout
body build because the same body builder serves pages, and collects the
dropped names into CreateLayoutStmt.BracedPlaceholders for MDL083 to report
at check time. The braced form is still dropped — recording it is a
diagnostic, not a decision to start honouring it.
Verified: the reported script is refused at check time and no longer creates
the module on the way to failing. Controls: a page's braced placeholder must
still fill its slot, and a layout's bodiless form must still produce a real
widget.
Refs: mendixlabs#1063
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Layouts were the only document mxcli could create and alter but not delete (mendixlabs#1063). Every other doctype has a DROP — pages, snippets, microflows, entities, queues, rules, folders — and the gap bit hardest on a layout mxcli itself had just written badly: the reported script created a layout that fails the build, and there was no headless way to remove it. This is wiring, not new capability. DeleteLayout already existed and worked on both engines, and is exercised on every CREATE OR REPLACE LAYOUT, whose replace half is a delete followed by a create. Only the grammar alternative, AST node, visitor branch and executor were missing. Pages still bound to the layout are named in a warning and the drop proceeds, rather than being refused. Three reasons, in order of weight: no other DROP in mxcli refuses on dependents; drop-then-recreate under the same name is how a layout is corrected, with the pages rebinding by qualified name (verified end to end — back to 0 errors); and the order of statements in a script is the author's business. The warning is load-bearing because of what the pages then do. Measured on 11.12.1, dropping a layout one page was bound to: [error] [CE1613] "The selected layout 'MyFirstModule.App_Mine' no longer exists." at Page 'MyFirstModule.Home_Web' [error] [CE1613] "The selected placeholder 'MyFirstModule.App_Mine.Main' no longer exists." at Content for placeholder 'Main' Both name the page end and never the layout that was removed, so an unwarned drop surfaces a build later attributed to a document the author did not touch. The warning names the pages and gives the `alter pages … where layout =` repoint. Control: a page bound to a different layout must not appear in the warning, an unused layout must produce none at all, and removing the warning makes the test fail with a bare "Dropped layout" line. Refs: mendixlabs#1063 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
CLAUDE.md said "Which placeholder is 'main' is a naming convention (22 of 22 name one `Main`; a page binds by qualified name anyway)" and the layouts skill said "the convention is the mechanism". mxbuild validates it as a rule, measured on 11.12.1 against a layout no page uses: CE0848 with no Main, CE0849 with two, CE0495 on any repeated name. That sentence is why the write-time guard accepted any placeholder under any name, so correcting it is part of the fix rather than housekeeping — the code matched the docs exactly, and the docs were wrong. Also documents DROP LAYOUT and the declared-vs-filled placeholder distinction (MDL083) across the same set: CHANGELOG, the layouts skill, MDL_QUICK_REFERENCE, `mxcli syntax` (new layout.drop topic), a bug-test example, and three findings records. Refs: mendixlabs#1063 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Phases 1 and 2 of docs/plans/2026-09-14-retire-legacy-engine.md. The codec engine has been the default since the 2026-06 cutover and nothing routes to legacy any more; it was five files away from having no non-test importers. Deleted: mdl/backend/mpr (the backend), mdl/enginecompare and the engine-diff make target (a package that compares two engines), and the `bson compare` command. Repointed mdl/repl, two examples and the test helpers at the codec backend, and dropped the nightly matrix's engines leg. --engine and MXCLI_ENGINE survive as a warning-only NO-OP rather than being removed. Deleting the flag would fail a script pinning `legacy` at argument parsing with "unknown flag", which says nothing about what changed; it now warns once and runs. Verified both spellings warn and still execute. Three things the plan did not anticipate, each invisible while two engines existed and none caught by the compiler: - errUnimplemented still told users to "rerun with MXCLI_ENGINE=legacy" — a runtime message naming a fallback that no longer exists is worse than none, because the user follows it into a second failure. It now asks for a report, which is what reaching it means. - setupTestEnv defaulted to the LEGACY backend, so most of mdl/executor's integration tests were exercising the retired engine rather than the one users get. They run on the codec engine now — coverage that was always intended and had silently not been happening. - TestODataService_EngineWriteParity compared two writers' key sets. The comparison is vacuous with one engine, so it is dropped and the property it was a means to is kept: a published OData service retains its role grants, which mx check reports as 0 errors either way. Renamed to match. Kept deliberately: the doctype gate's engine matrix, at one entry. Its selection still turns a stale MXCLI_TEST_ENGINES=legacy into a loud failure rather than a gate that selects zero engines and reports success. One test deleted: TestCustomHandlerLegacyRefuses, which asserted legacy's refusal of a construct it could not serialize. The codec engine's positive coverage of that construct is intact. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (543 pass), check-findings. Executor integration suite green at 562s, down from 642s now that the doctype gate runs once instead of twice; mdl/backend, api, marketplace and repl integration suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Retire the legacy engine: delete the backend, port api/ and MCP off it
…ec backend) into feat/workflow-ai-agent-task Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…emoval Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 3 step 4 of docs/plans/2026-09-14-retire-legacy-engine.md: re-run the reachability probe over what was left. Six of the eleven remaining entries were not bypasses awaiting a port — nothing anywhere wanted them. scripts/backend-reachability.sh reports DEAD for "nothing calls this through a backend value", and that one verdict covers three situations wanting opposite fixes: a BYPASS (a caller wants it but holds a concrete reader/writer — port the caller), an ORPHAN (no caller anywhere — delete), and a DUPLICATE (callers exist, but through a narrower package-local interface with a different signature — also delete). The probe cannot separate them; a grep for callers under any type can. Deleted, all measured DEAD: the whole WidgetSerializationBackend interface (SerializeWidget, SerializeClientAction, SerializeDataSource, SerializeWorkflowActivity), GetUnitTypes and UpdateLayout. - SerializeWidget/DataSource are superseded by WidgetBuilderBackend's *ToOpaque pair, whose own comment says "This replaces the direct mpr.SerializeWidget call" — the supersession was known and the old pair never removed. - SerializeClientAction and SerializeWorkflowActivity are reached through pagemutator's and wfmutator's own deps interfaces, which declare them returning bson.D rather than (any, error). The vestigial *Backend copy of SerializeWorkflowActivity carried a comment claiming ALTER WORKFLOW used it; ALTER WORKFLOW uses codecWorkflowDeps. - GetUnitTypes exists only on modelsdk/mpr.Reader; UpdateLayout was superseded by the page mutator when ALTER LAYOUT landed. Both generated stub files regenerated (276 -> 270 methods), mock stubs dropped. Census 11 -> 6, and all six that remain are genuine bypasses. This corrects a claim shipped in the previous PR: the census header and the plan both said the list "shrinks by closing a bypass, never by deleting methods", which holds for a bypass and is wrong for the other two. Both are rewritten to name the three causes, as is the probe script's own header, which still told the reader to fall back to the deleted engine. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl, check-findings. The census pin (TestNoReachableUnimplementedBackendMethods) passes at six entries with its control intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
feat(workflow): AI agent tasks with `call agent microflow`, verified in Studio Pro over MCP
Conflict in CHANGELOG only, and the usual shape: both sides appended under `## [Unreleased]`'s `### Added` and `### Fixed`. Both sides kept — verified zero deletions in the CHANGELOG diff against origin/main. Main's #468 deleted the legacy sdk/mpr BACKEND (mdl/backend/mpr) and the flag selecting it. Nothing in this branch touched it: the writer guard here is on the modelsdk path, and sdk/mpr the package (readers/writers) is untouched, so the merge is content-only. Re-verified on the merged tree — build, full test suite, lint, corpus sweep (0 failures), findings — and the three fixes still fire: MDL081 on the reported script, MDL083 on the braced form, DROP LAYOUT parses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Five TestConnect_* tests from #468 built the backend with an empty dial address, so defaultDial rewrote 127.0.0.1 to host.docker.internal wherever that name resolves. In a devcontainer the request went to the Docker host instead of the httptest listener and Connect failed with "connection refused"; on CI the name does not resolve, so the suite was green. Route the three constructor calls through backendFor, which passes the listener address explicitly the way fakePED.connectClient does. defaultDial is unchanged — it is correct for a real Studio Pro. Control: with the dial reverted to "" the same five tests fail in the devcontainer with the reported message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…de by, await all users
Phase 3 of PROPOSAL_workflow_studio_pro_constructs.md.
multi user task Vote 'Vote on the request'
page HR.VotePage
participants 80 percent
decide by threshold 60 percent fallback 'Reject'
await all users
outcomes 'Approve' { } 'Reject' { };
Rules: consensus / majority more than half / majority most chosen /
threshold <n> percent|votes, each with fallback '<outcome>'; veto
'<outcome>'; microflow M. Omitted clauses are what a rebuild has always
written, so existing describe output is unchanged.
The shapes come from Studio Pro, not gen: the examples were built in Studio
Pro 11.14 over MCP, saved to ako/TestApp, and the saved document is the
reader's fixture — "more than half" is an Absolute majority, a percentage
threshold Relative, and both outcome pointers hold the outcome's $ID.
Measured on mxbuild 11.13.0: fallback required for consensus/majority/
threshold (CE1866), veto outcome required (CE1867) — MDL-WF13 at check time,
also for a name that is none of the task's outcomes; a decision microflow
returns String (CE5012), parameters free; thresholds and participant counts
are not range-checked by the build, so not by mxcli.
Fixed on the way:
- a workflow rewrite silently reset every multi-user task's participant
count and "await all users" (never guarded); a rewrite not restating them,
or a stored completion rule, is now refused.
- MCP: multi-user tasks could not be created on Studio Pro 11.14 (the
constructor takes a taskPage element); PED's default consensus has no
fallback (CE1866) and its constructor drops a more-than-half majority's
fallback, which is now set by the outcome's $ID afterwards. Verified live:
created, read back, no errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
After the orphans and duplicates were deleted, the six remaining census entries
were all the same kind of caller: cmd/mxcli's bson dump/discover, diag,
extract-templates and examples/read_project — raw-unit debugging and export
commands holding a concrete sdk/mpr reader because raw access is the thing they
exist to provide.
Porting them was considered and declined. The backend interface speaks the
semantic model (ADR-0005); routing a BSON dumper through it would either widen
that interface with raw accessors, undoing the decision, or make the tools worse
at their only job. The framing this plan shipped ("the list shrinks by closing a
bypass") did not offer that option — a second way it was too narrow, alongside
the orphan/duplicate correction: some bypasses are correct.
unreachableUnimplemented becomes a standing record rather than a to-do list, and
its header now says so. It keeps its value as a tripwire: a NEW entry still
means either a new bypass appeared or a method was added that nothing calls, and
the two want opposite fixes, so the cause has to be established rather than a
row added to silence the failure.
sdk/mpr does not go away with this — those six commands still import it, as does
the workflow serializer #469 extended. What Phase 3 delivered is that no engine
path reaches it: the executor, the backends and api/ are clean. Removing the
serializer is downstream of Phase 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The plan's Phase 4 opens "with [sdk/mpr] gone" — and Phase 3 closed by keeping it, so its premise did not survive. Measuring it properly shows two migrations that do not gate each other, and the one worth doing first is not the driver migration. Also corrects §7.4, which said "those six commands still import sdk/mpr". That was wrong by a factor of four and pointed at the wrong files: the census tracks FullBackend METHODS with no caller through a backend value, not importers. 28 non-test files import sdk/mpr, including 7 in cmd/mxcli/docker, two mdl/executor validators, and modelsdk.go — whose Reader and Writer are type ALIASES to sdk/mpr's, so the published library's root API is the legacy serializer. 4a (port the 28 callers) is the half that pays: 23 of 28 use only mpr.Open, so the dependency is one read-only constructor rather than the serializer's bulk, and the port is the MCP move repeated — modelsdkbackend.New() + ConnectReadOnly. The 140-vs-47 reader method comparison that makes it look impossible is the same wrong comparison the MCP port already disproved: the semantic methods live on the codec backend, a layer up from modelsdk/mpr's raw reader. Its real gate is modelsdk.go's public API, which should be decided before 4a starts rather than during. 4b (convert mdl/'s mutator layer to v2) is independent: those 19 non-test v1 files are v1 because pagemutator/wfmutator/widgetobj declare their deps interfaces in v1 bson.D, not because they bridge to sdk/mpr, so they do not shrink when it goes — which is what §4 assumed. Measured: 249 v1 files, 174 v2, and only 6 importing both — all in mdl/backend/modelsdk, where the crossing is safe because the interchange is bytes and BSON bytes carry no driver version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix(layout): enforce mxbuild's placeholder rule, and add DROP LAYOUT
modelsdk.go — the published library's root API — had Reader and Writer as type ALIASES to sdk/mpr, so the documented entry point was the legacy serializer. That was the single biggest gate on deleting it. Open and OpenForWriting now return a backend.FullBackend from the codec engine. Repointed all 14 root-package importers: cmd/mxcli/marketplace (7 files), cmd/mxcli (4), and both examples. Close becomes Disconnect; the two places typing *modelsdk.Reader/Writer take backend.FullBackend. Two methods were needed to do it without a bypass: - ExportJSON on the codec backend. Pure fan-out over the backend's own List* methods, so it cannot drift from what every other reader reports. Its only caller was examples/read_project holding a concrete sdk/mpr reader, which is why it sat on the census — the probe now says LIVE, so it is struck off by closing the bypass rather than by deleting the method. Census 6 -> 5. - AddRawUnit on RawUnitBackend. The marketplace transplant copies a module's units between projects verbatim, and copying verbatim is the point: decoding and re-encoding would mint fresh identities, and an entity's GUID is what the runtime keys the database on. A behaviour difference found by RUNNING the example, not by any test: CreateEntity and CreateAssociation left the caller's element holding an EMPTY id, so the next call failed with "entity not found: " — note the blank after the colon. The legacy writer populated these. assignID only fills an empty id, so api/ was unaffected (its builders pre-assign one), which is exactly why the green suite never saw it. copyAssignedIDs writes the minted ids back, matching attributes BY NAME rather than by index: entityToGen can add, skip or reorder (an audit pseudo-type becomes a generalization, not an attribute), so an index-based copy would hand the caller another attribute's identity — which looks like it worked and is worse than the empty id. Both examples run end to end against a fixture and their writes verified in the model: 6, 3 and 2 attributes land where every one of them used to error. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (543), check-findings. The ID fix has a revert control that fails with the reported symptom, and a companion test that a caller-assigned id survives — without it the test would pass against an implementation that overwrites every id and breaks api/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…he Phase 3/4a work
Phase 4a of PROPOSAL_workflow_studio_pro_constructs.md. Three workflow
constructs only Studio Pro could author become MDL:
event subprocess <name> ['<caption>'] on [non] interrupting notification|timer … { … };
notification <name> [comment '<caption>'];
boundary event [non] interrupting notification <name> ['<caption>'] { … }
Describe used to leave sub-processes out, print a notification activity as a
comment and drop notification boundary events. The shapes come from ako/TestApp
(Studio Pro 11.14, built over MCP and saved); that document is the reader's
fixture, since gen has no type for the timer starts, the notification activity
or the notification boundary events, which are read off the raw document.
Measured on mxbuild 11.13.0: a sub-process body with no end is CE0105 (the
builder appends the End), a jump across flows CE6682 (MDL-WF05 now scopes jumps
per flow), a timer start with no expression CE0126 (MDL-WF14), two interrupting
boundary events on one activity CE6697 (MDL-WF15), a start named like an
activity CE0495 (names deduplicated across flows). Version gates were measured
by loading each construct into blank 11.10-11.13 projects: notification starts
11.8 (gen; load from 11.10), notification activity and boundary events 11.11,
timer starts 11.13.
The rewrite guard now requires sub-processes and notification activities to be
restated, walks sub-process bodies, and does not count a sub-process's implicit
End. Fixes a silent loss: a rewrite from describe output deleted every
notification activity (measured 1 -> 0 with the previous build).
MCP: sub-processes go into the constructor and a rewrite replaces the stored
ones. Verified live against Studio Pro 11.14, which also showed that the single
user task constructor drops boundary events (notification events are now
re-added after create and update) and that its interrupting constructors
rewrite a boundary path's ending (such notification paths are refused with the
ending Studio Pro needs). Timer boundary events are affected by the same
constructors and are left for a separate change.
ALTER WORKFLOW's insert boundary event refuses notification kinds, which need a
name the op does not carry; the three boundary-event writers now share one
EventType -> $Type map instead of defaulting unknown kinds to an interrupting
timer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Close Phase 3, and cut the root package off sdk/mpr (Phase 4a gate)
test(mcp): dial the fake PED at its listener, not through defaultDial
feat(workflow): multi-user task completion rules — participants, decide by, await all users
Five files, and the slice splits into two ports rather than one: the Phase 3 decision that raw tools keep raw access does NOT mean they keep sdk/mpr. Raw tools move to modelsdk/mpr, the v2 RAW reader. bson dump, bson discover and diag moved by import swap alone — ListRawUnits, GetRawUnitByName, GetRawMicroflowByName, ListAllUnitIDs and ContentsDir have identical signatures on both readers, and RawUnitInfo is field-for-field identical. So they honour Phase 3 and land on v2 at the same time. project_tree.go and debug_resolve.go go through the backend instead. The first calls 36 semantic reads, every one already on FullBackend; the second needs ParseMicroflowBSON, which is a backend method. That also corrects the Phase 3 write-up, which called every remaining bypass a raw-unit debugging tool. project_tree.go is not one, and the census could not reveal the mistake: it lists only methods with NO implementation, so a caller holding a concrete reader while calling only implemented methods never appears. The complete list of bypasses is the sdk/mpr importer list, not the census. A regression the port introduced, caught only by a baseline diff: project-tree silently lost System.VerifyPassword — 131 bytes out of 78KB of JSON, build clean and every test green. The System module's Java actions are platform built-ins with no stored unit; sdk/mpr synthesized them and the codec backend did not. The definitions move to modelsdk/meta beside the virtual System module's entities, both codec listings append them, and sdk/mpr delegates rather than keeping a second copy of a hand-maintained platform list. The technique is the transferable part: build a binary from the pre-port commit and require byte-identical output from every command whose reader changed. No test caught this and none would have. A synthesized element is what a reader swap loses, because it lives in one reader's code rather than in the data. Regression tests cover both listings, with a control that the STORED actions are still returned — without it they would pass against a backend that returns only the synthesized ones. Reverting the fix fails with the reported symptom. Importers 27 -> 22. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (544), check-findings. project-tree, bson dump and bson discover are byte-identical to the pre-port binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
feat(workflow): event sub-processes and notification events
… could not build Phase 4b of PROPOSAL_workflow_studio_pro_constructs.md. $Notified = notify workflow $Workflow target Module.Workflow.ElementName; The target names the element a notify resumes: a notification-started event sub-process's start, a notification activity, a notification boundary event or a wait for notification. Mendix stores a different NotifyTarget $Type for each (ako/TestApp ZzMxcliExample_Notify, Studio Pro 11.14, now the reader's fixture), so the executor looks the element up in the stored workflow and writes the matching type; an element no notification reaches (a timer start, a user task, a timer boundary event) is refused with the names that would work. Before Mendix 11.7 the action names its wait for notification directly (Activity), and that form is written there. Measured on the 11.6, 11.10 and 11.13 mxbuilds, a notify without a target is CE0166 — so every notify mxcli wrote failed the build, and describe dropped a stored target so a rewrite removed it. A notify with no target is now MDL-WF16, refused by check and exec. Fixes the output variable being lost on read: modelsdk/gen bound it to the key VariableName where Mendix stores OutputVariableName (11.6 metamodel and Studio Pro 11.14 agree; under VariableName the variable is undefined, CE0109). Fixed as a storage-name override on NotifyWorkflowAction only — other gen types bind VariableName correctly. MCP: the notify action is mapped (it was unsupported); the notifyTarget shape matches what Studio Pro 11.14 returns for TestApp's saved actions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(workflow): notify workflow … target, and fix notify actions that could not build
Port cmd/mxcli's readers off sdk/mpr (Phase 4a, first slice)
…ption Every ALTER WORKFLOW op failed with `ambiguous activity "X" (2 matches)` whenever the workflow contained `jump to X`. buildJumpTo defaults a jump's caption to its target's name, and both activity resolvers (MCP mcpWorkflowMutator.searchActivities, MPR wfmutator) matched name OR caption into one pool, so the jump's caption shadowed the target. Measured live over MCP against Studio Pro 11.14. Resolve in two tiers in both backends: name matches win; captions are consulted only when no activity carries that name. Ambiguity and @n are judged within the chosen tier, in the same DESCRIBE traversal order. The jump's default caption is left unchanged: existing projects already carry that shape, and changing it would rewrite every mxcli-authored jump on the next re-run. Tests in both backends fail with the reported message against the pooled resolver (control) and pass with the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
|
Opened against the wrong repository by mistake (intended for the ako/mxcli fork). Closing — sorry for the noise. |
ako
added a commit
to ako/mxcli
that referenced
this pull request
Sep 16, 2026
…ht files Three defects, one visible and two that the visible one uncovered. THE FAILURE. `gh pr diff` reads an endpoint that caps at 20,000 lines and answers HTTP 406 above it. mendixlabs#1107 is 21,366 changed lines, so the step died with "PullRequest.diff too_large" under `bash -e` and took the job with it -- the only hard-fail path in a workflow whose every other error (missing key, non-200, empty content) is a warning and exit 0. mendixlabs#1106 failed the same way hours earlier. The per-file endpoint has no such cap: measured on mendixlabs#1107 it returns all 278 files, 3 without a patch (the .bson fixtures). The diff is now rebuilt from those patches, and a fetch problem downgrades the review rather than failing the PR. WHAT THE REVIEW ACTUALLY SAW. `head -c 80000` truncates by BYTES over an alphabetically ordered diff. Measured on mendixlabs#1107, whose full diff is 1.29 MB: the window covered 3 of 275 files, and all three were .claude/skills/fix-issue/findings/*.jsonl -- append-only evidence logs. Zero Go code. mendixlabs#1095 was cut to 23% and mendixlabs#1090 to 19% the same way, so this is what every large review has been looking at. Selection is now file-aware: generated, fixture and append-only paths are skipped, whole file patches are taken most-changed-first, and no single file may exceed a tenth of the budget. That last clause is load-bearing -- ordering by size with no cap put three files in the budget and nothing else, while ordering alphabetically missed mdl/backend/mpr/backend.go, the 1,163-line change this PR is named after. With both, mendixlabs#1107 yields 11 source files including that one. A manifest of all 278 changed files goes in the prompt regardless, and the note tells the model to say when a finding needs a file it cannot see rather than assume absence. A normal PR is unaffected: mendixlabs#1098 comes through as 13 of 14 files, nothing over budget. Not fixed here, because it is not in this repo: every "successful" run since at least 2026-09-04 got HTTP 401 from OpenRouter and posted nothing, so the green checks were not reviews. The key has been rotated; this PR is the first run that can reach the model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNDe35kDNsMX5cz4Ahn4rk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every
ALTER WORKFLOWop that addresses an activity (e.g.insert boundary event on bugSplitJump …) failed withwhenever the workflow also contained
jump to bugSplitJump. Measured live over the MCP backend against Studio Pro 11.14 (TestApp,workflow.ZzMxcliBug_TimerBoundaryEvents2);bugSplitJump@1worked.Cause
buildJumpTo(mdl/executor/cmd_workflows_write.go) names every jumpJumpToand, when the MDL gives no caption, setsCaptionto the target's name. Both activity resolvers —mcpWorkflowMutator.searchActivities(MCP) andfindActivitiesRecursive/findActivityIndexRecursive(MPR,wfmutator) — matchedname == ref || caption == refinto a single pool, so the jump's caption shadowed its target's name. The MPR backend had the same defect; it just had not been reported.Fix
Both backends now resolve in two tiers: activities whose name equals the reference win; captions are consulted only when no activity carries that name. Ambiguity and
@Nare judged within the chosen tier, in the unchanged DESCRIBE traversal order.Behaviour change: a reference that was ambiguous only because of a caption now resolves without
@N.X@2, which previously reached the jump, now reports not found — the jump stays addressable by its own name.The jump's default caption is deliberately left alone: existing projects already carry that shape (so the resolver had to be fixed regardless), and changing the default would rewrite every mxcli-authored jump on the next re-run. DESCRIBE already suppresses the derived caption.
Tests
mdl/backend/mcp:TestWFResolve_NameBeatsJumpCaption(fake PED) — boundary event lands on the named activity; caption fallback still works; two caption-only matches remain ambiguous and@2selects the second.mdl/backend/wfmutator:TestWorkflowMutator_FindActivity_NameBeatsJumpCaption— covers bothfindActivityByCaptionandfindActivityIndex.Control: both tests were written first and fail against the pooled resolver with the reported
ambiguous activitymessage; they pass with the fix.mcp,wfmutatorand executor workflow/jump tests pass;gofmt,go vet,make check-findingsclean.Finding recorded in
.claude/skills/fix-issue/findings/mdl-backend.jsonl.Not yet re-verified live against Studio Pro after the fix.
🤖 Generated with Claude Code