corezoid/simulator
v2.10.0MIT
Simulator.Company platform assistant. Exposes the Simulator REST API as an MCP server and provides skills for managing actors, forms, graph structures, financial accounts, charts, smart forms, and process simulation.
Changelog
[2.9.1]
Fixed
- simulationSnapshot wrote numbers in actor
data(andformId, positions,sim.source) to the graph file as YAML strings, so a run from the saved file read them as text:self.price + self.costgave"1020"instead of30andcount(price=10)found nothing, with statuscompleted. Numbers are now written as YAML numbers; strings such as"10"stay strings. A snapshot file written by 2.9.0 stays wrong: simulationCheck / simulationRun now warn about it — re-take it withsimulationSnapshot(layerId, overwrite: true). simulationRun now returns the check'swarnings. - simulationRun many-run mode counted runs stopped before the horizon (
stopped_by_time,stopped_by_limit) as successful: their partial metrics fed the statistics and goal shares, and the table showed them as ok. Metrics and goals are now over completed runs only; the table showscompleted/total (N stopped, M failed), the reason a run stopped is listed under the table, and a goal no completed run could evaluate showsno datainstead of dropping its column. - Opening the first account of a conserved type at 0 (e.g.
create … accounts: [{name: cash, value_type: USD}]when the graph has no USD account) failed the step withconserved totals changed within step:. A type with no account now counts as total 0. - The Release workflow published a tag without running the tests (only the binaries build was required); it now runs build / vet / test first and publishes only if they pass.
[2.9.0]
Added
- behaviour simulation engine (simulationRun / simulationCheck / simulationSnapshot) (#111)
Changed
- reactionOrders is optional; executors without an order still close the task (#117)
- bump github.com/mark3labs/mcp-go from 0.58.0 to 1.1.0 (#114)
- document the page-level focusVisible flag (#112)
- document the page-level browser tab title (#106)
Fixed
- read layer pages until an empty one, not a short one (#115)
- make MCP startup work in Codex (#81)
- place pushGraphFile edges without a position, send data on create (#110)
[2.8.0]
Added
- authenticate with a workspace API key via SIMULATOR_API_SECRET (#103)
- simulator-app-generator (#100)
Changed
- make design quality an explicit deliverable (#104)
- bump mcp-go from 0.57.0 to 0.58.0 (#96)
Fixed
- address bug-finder issues #87, #88, #89 (#102)
- recognize bare top-level style file as text/css in pushSmartForm (#97)
- carry the page query through appGetPage / appSendForm (#98)
- allow visibility placeholders (#95)
- catch on push what only the browser caught before (#99)
[Unreleased]
[2.10.0] - 2026-10-05
Added
- Directory-readiness for the public plugin catalogs: root
plugin.jsonin the Agent Plugins 1.0.0 format (required by Kiro's Powers registry and accepted by OpenAI's plugin portal),supportURL/privacyPolicyURL/termsOfServiceURLin the Codex interface block, brand icons underplugins/simulator/assets/, and a README "Support & Legal" section. The root manifest joins the version lockstep (now seven files;scripts/release.shbumps it automatically).
Fixed
claude plugin validate --strictpasses again: the non-standardinterfaceblock moved out of.claude-plugin/plugin.json(it lives in the Codex manifest, which is the host that reads it).
[2.9.1]
Fixed
- simulationSnapshot wrote numbers in actor
data(andformId, positions,sim.source) to the graph file as YAML strings, so a run from the saved file read them as text:self.price + self.costgave"1020"instead of30andcount(price=10)found nothing, with statuscompleted. Numbers are now written as YAML numbers; strings such as"10"stay strings. A snapshot file written by 2.9.0 stays wrong: simulationCheck / simulationRun now warn about it — re-take it withsimulationSnapshot(layerId, overwrite: true). simulationRun now returns the check'swarnings. - simulationRun many-run mode counted runs stopped before the horizon (
stopped_by_time,stopped_by_limit) as successful: their partial metrics fed the statistics and goal shares, and the table showed them as ok. Metrics and goals are now over completed runs only; the table showscompleted/total (N stopped, M failed), the reason a run stopped is listed under the table, and a goal no completed run could evaluate showsno datainstead of dropping its column. - Opening the first account of a conserved type at 0 (e.g.
create … accounts: [{name: cash, value_type: USD}]when the graph has no USD account) failed the step withconserved totals changed within step:. A type with no account now counts as total 0. - The Release workflow published a tag without running the tests (only the binaries build was required); it now runs build / vet / test first and publishes only if they pass.
[2.9.0]
Added
- behaviour simulation engine (simulationRun / simulationCheck / simulationSnapshot) (#111)
Changed
- reactionOrders is optional; executors without an order still close the task (#117)
- bump github.com/mark3labs/mcp-go from 0.58.0 to 1.1.0 (#114)
- document the page-level focusVisible flag (#112)
- document the page-level browser tab title (#106)
Fixed
- read layer pages until an empty one, not a short one (#115)
- make MCP startup work in Codex (#81)
- place pushGraphFile edges without a position, send data on create (#110)
[2.8.0]
Added
- authenticate with a workspace API key via SIMULATOR_API_SECRET (#103)
- simulator-app-generator (#100)
Changed
- make design quality an explicit deliverable (#104)
- bump mcp-go from 0.57.0 to 0.58.0 (#96)
Fixed
- address bug-finder issues #87, #88, #89 (#102)
- recognize bare top-level style file as text/css in pushSmartForm (#97)
- carry the page query through appGetPage / appSendForm (#98)
- allow visibility placeholders (#95)
- catch on push what only the browser caught before (#99)
[2.9.1]
Added
- Behaviour simulation engine (
internal/engines/sim) with thesimulationCheck,simulationRunandsimulationSnapshottools and thesimulator-simulateskill: YAML behaviour rules over any layer, exact decimal accounts with conserved types, a discrete-event queue in model time, scenario comparison, many-run medians/ranges and goal shares, and a static model check. Read-only: nothing is written to Simulator. The model format is specified inplugins/simulator/docs/simulation/model-format.md; the engine reproduces the reference engine's answers on the conformance cases (TestConformance).
Fixed
- simulationSnapshot wrote numbers in actor
data(andformId, positions,sim.source) to the graph file as YAML strings, so a run from the saved file read them as text:self.price + self.costgave"1020"instead of30andcount(price=10)found nothing, with statuscompleted. Numbers are now written as YAML numbers; strings such as"10"stay strings. A snapshot file written by 2.9.0 stays wrong: simulationCheck / simulationRun now warn about it — re-take it withsimulationSnapshot(layerId, overwrite: true). simulationRun now returns the check'swarnings. - simulationRun many-run mode counted runs stopped before the horizon (
stopped_by_time,stopped_by_limit) as successful: their partial metrics fed the statistics and goal shares, and the table showed them as ok. Metrics and goals are now over completed runs only; the table showscompleted/total (N stopped, M failed), the reason a run stopped is listed under the table, and a goal no completed run could evaluate showsno datainstead of dropping its column. - Opening the first account of a conserved type at 0 (e.g.
create … accounts: [{name: cash, value_type: USD}]when the graph has no USD account) failed the step withconserved totals changed within step:. A type with no account now counts as total 0. - The Release workflow published a tag without running the tests (only the binaries build was required); it now runs build / vet / test first and publishes only if they pass.
- [CE-15944] Layer position updates (
compactGraphLayout,updateLayerPositions, pushGraphFile) sent thePUT /graph_layers/actorsbody as{"items":[…]}, which the server rejects with 400body must be array— surfaced by compactGraphLayout asapplyPositions batch 0, and swallowed as a log-only warning on the push path so positions silently never reached the canvas. The body is now a bare JSON array. compactGraphLayout additionally: sends all placements in one request (100-item batches conflicted at batch boundaries via the occupied-cell check and left the layer half-compacted), skips placements already on their computed cell (so a repeat run no longer fails), and de-collides multiple placements of the same actor. pushGraphFile compares coordinates on the 50px grid so snap-adjusted nodes are not resent, and now reports position-update failures in its result (warnings) instead of only logging them. - pushGraphFile: edges were placed with a position
{x:0,y:0}, which the server treats as a grid cell, so every push with more than one new edge failed withOccupied cells: (A, 1). Edge placements are now sent without a position. - pushGraphFile: an actor without
datain the file failed to create (body must have required property 'data'); an emptydataobject is sent instead. - createForm / getForms / searchForms descriptions and the simulator-forms skill now say that
private/draft forms (
isTemplate=false) are not listed by getForms/searchForms; pushGraphFile's description says that a UUID id places the existing actor instead of copying it. - Codex MCP startup no longer depends on
CLAUDE_PLUGIN_ROOT: the launcher keeps the user's workspace inSIMULATOR_WORK_DIRand resolves the installed plugin root separately, so Codex completes the MCP initialize handshake after a marketplace install. - Layer reads through
/graph_layers/paginated(pullGraphFile, pushGraphFile, getAllLayerPlacements, compactGraphLayout, pruneLongEdges) stopped at the first short page. The server applies LIMIT before it drops deleted actors and edges, so one deleted element made a page short and silently cut off the rest of the layer. Pages are now read until an empty one.
Changed
- access rules / tasks:
reactionOrdersis documented as optional — an executor or signer without an order still counts toward task completion — and the completion rule is spelled out - bump github.com/mark3labs/mcp-go from 0.58.0 to 1.1.0 (replaces #108, which was based on
main) - simulator-app-generator: design quality is now an explicit deliverable — a gated design brief in Phase 3 (§5.3a), tokens seeded in Phase 4, an acceptance-criteria quality bar (§10.1) and a mandatory human visual pass (§10.2)
[2.7.0]
Changed
- [CE-15707] Sync the edge-hole link contract into the Simulator plugin
[2.6.0]
Added
- anonymous tool-call analytics + opt-in email (#86)
Changed
- bump actions/setup-go from 6 to 7 (#80)
- bump github.com/mark3labs/mcp-go from 0.55.1 to 0.57.0 in /plugins/simulator/mcp-server (#84)
- CE-15765 feat(graph): exportGraph/importGraph/uploadGraphFile/getTaskStatus (#90)
- CE-15784 docs(links): closing an actor hole keeps each link's kind (#85)
Fixed
- mime type, conflict detection, dup guard (#79)
- reconcile missing file id after create (#78)
- set customize_response:false on callback nodes (#82)
[2.5.0]
Added
- actor geolocation fields on createActor/updateActor (#74)
Changed
- CE-15667 feat(actors): filterActors linkedToActorDirection param (#77)
- add CDU UI pattern recipes & DOM/protocol notes (#70)
- document extra.reverseEdge link-direction flag
Fixed
- self-healing MCP path resolution in dev checkouts, bump to 2.4.1 (#72)
- pushSmartForm Windows path bug, bump to 2.4.1 (#71)
- getAccounts defaults to limit=100; getActor validates the UUID up front (#69)
[2.9.1]
Added
- API-key authentication (
SIMULATOR_API_SECRET). A second, non-interactive auth mode for CI, headless runs and service integrations: set a workspace access key in.envand every request is sent asAuthorization: Bearer <key>instead of the OAuthSimulator <jwt>. The key takes precedence overACCESS_TOKENand saved OAuth credentials, and the OAuth flow is disabled end to end —loginreturns an explanation instead of opening a browser (so the telemetry-email elicitation never fires),auth.Saverefuses to write a token, andset-environmentrefuses to re-point the gateway, since a key is scoped to one workspace on one gateway and switching would send it to a host it was not issued for. The plugin only ever reads the key: it is never written back to.env, logged, or included in telemetry. Because a key is long-lived, the server refuses to start when the API base URL would send it over plaintext HTTP to a non-local host, and stateless/SSE construction rejects the mode outright as incompatible with per-request multi-tenant auth. That plaintext override (SIMULATOR_ALLOW_INSECURE_API_SECRET) is parsed as a boolean rather than the repo's usual "set to anything" convention: the natural way to turn a switch off is=0, and under a non-empty test that would have DISABLED the guard protecting the one credential that never expires. Implemented as a single branch inauth.Loadplus the existing per-credentialTokenType, so no header call site changed; theapp/authpackage gets its first tests. In API-key mode the startup line now also names where each half came from (.envvs the process environment) —loadDotEnvnever overrides a value already in the environment, so a key exported in a developer's shell can outrank the project's.envwhile the base URL still comes from that file, and nothing in the log used to say so. Sources only; never the value. - A
.envline the loader could read but the writers could not find (pre-existing).loadDotEnvtrims a line before splitting it, so it has always readKEY=valueandKEY = value;updateEnvFileMulti/removeEnvKeymatched a bareKEY=prefix and did not. So a rewrite of a key already present in one of those shapes appended a second line — and the loader takes the FIRST occurrence, sologin,set-workspaceandset-environmentsilently lost their new value on the next start, whileauth.Deleteleft the line in place and the "cleared" token came straight back. This predates the API-key work and affectedACCESS_TOKEN/WORKSPACE_IDon any indented.env. Both sides now normalise through oneauth.ParseEnvLine, matching whole keys and preserving the file's BOM and the author's indentation on rewrite. A cross-package test drives loader and writers against each other so the two cannot drift apart again..envis not a shell script, soexport KEY=valueis deliberately NOT an assignment — both sides skip it alike. simulator-app-generatorskill. Generates a complete multi-page Smart Form app from a set of existing Corezoid process ids plus a product description: pulls every process, derives its real input/output contract from itsapi_rpc_replynodes (declaredparamsdrift and are only a cross-check), designs a page map that covers all of them, builds the Smart Form and the bridging Corezoid middleware process that calls them viaapi_rpc, binds both envs, then verifies end-to-end (lint-process+pushSmartForm, syntheticrun-taskpayloads, and a liveappGetPage/appSendFormdrive) with a self-repair loop. Addsdocs/user-flows/app-generation.mdwith the extraction algorithm, middleware skeleton, and test-payload catalogue.- Anonymous tool-call telemetry + opt-in email. The MCP server now sends anonymous usage
events (tool name, duration, error type, API hostname, transport, server version, a
per-installation UUID, and MCP client name/version) to the same Corezoid ingest process
corezoid-ai-plugin uses, tagged
product: "simulator"so the two stay distinguishable downstream. No tokens, workspace/actor/form identifiers, or graph content are ever sent. Opt out entirely withSIMULATOR_ANALYTICS_DISABLED=1. After the first successfullogin, clients that support MCP elicitation are offered a one-time opt-in to include an email address, stored in~/.simulator/preferences.json. Newinternal/telemetrypackage; wired viaserver.WithToolHandlerMiddlewareinapp/mcpserver.Newso it covers every registered tool without touching individual handlers. See README's Telemetry section and SECURITY.md for the full field list. - Graph import/export tools —
exportGraph,importGraph,uploadGraphFile,getTaskStatus. Wraps the pong-server async task API so a workspace graph (actors, edges, forms, and optionally attachments / transactions / processes / users / balances) can be exported to a.grapharchive or re-imported, mirroring the UI's Export/Import buttons — distinct from the existingpullGraphFile/pushGraphFiledeveloper sync tools, which edit a single layer's YAML and never touch.grapharchives.exportGraphrequires at least one ofactors/forms/allWorkspace;uploadGraphFileaccepts a.graphfile as base64 or a public URL (capped at 100 MiB either way) and returns a storagefileNameforimportGraph;getTaskStatuspolls a task by id and, for a completed export, returns a ready-to-sharedownloadUrlalongside the rawdetails.file.fileName.
Fixed
- Actionable 401/403 errors. A rejected credential used to surface as a bare
API returned 401: …with no clue whether the key,WORKSPACE_IDor the environment was wrong. Both HTTP stacks now append a mode-aware remediation hint (never containing credential material, and suppressed in stateless mode, where the credential came from the caller). .envparser silently mangled hand-written values (pre-existing). Quoted values became part of the Authorization header (Bearer "abc"), and a UTF-8 BOM (Notepad / PowerShell) left the first line's variable unset with no explanation. Machine-written keys never hit these, but a hand-pastedACCESS_TOKENalready could, andSIMULATOR_API_SECRETalways is. Inline#is still treated as part of the value.ecore.EnsureAuthnever cleared its cached Authorization header. It only overwrote the process-global cache on success, so a removed or revoked credential would keep being sent by engine tools while the curated tools reported "not authenticated". The cache is now authoritative.serverInfo.versionreported2.1.0while every manifest was at2.7.0(#89). The version returned in the MCPinitializehandshake came from two stale Go consts (cmd/server's andmcpserver.defaultVersion) thatscripts/release.shnever bumped — it only touched the six manifests. Collapsed them into one exported source of truth,mcpserver.DefaultVersion, whichcmd/servernow reads;release.shbumps it in lockstep with the manifests, and a newTestDefaultVersionMatchesManifestfails CI if it ever drifts again. Also bumped.kiro-plugin/plugin.json, which had fallen a release behind (2.5.0).loadSysFormscached transient failures and could serve valid data alongside a stale error (#87). The success path never clearedsysFormsErr, and both failure paths cached the error withsysFormsLoaded=true; in the stateless (SSE) server, a failing and a succeeding request racing on the same workspace could leave the cache permanently{validForms, staleErr}, after which every caller (if sysErr != nil …) silently stopped resolving form-name→id until restart. Now only successful loads are cached (a failure is retried, never poisoned), the success write runs under a double-check so a concurrent winner is reused rather than clobbered, and the unusedsysFormsErrfield is removed.createEdgeLinkignored the per-itemerrorflag frommass_links(#88). The response'serror boolwas parsed but never checked, so an{error:true, data:{id:…}}item would be read as a created edge and recorded as a live link — a silent ghost. The success branch is now guarded on!resp.Data[0].Error. Defensive: the current backend strips the id from a failed item, so there is no active data loss today, but the contract is now enforced.pushSmartFormcould not see cross-file token defects, so an unresolved[[key]]shipped silently.cduschema.ValidateFileis per-file by signature — it can never tell whether a page's[[key]]resolves against the locale files or whether a{{key}}has a viewModel default — and the save endpoint stores page source opaquely, so nothing reported it until a literal[[key]]appeared in the browser. Newcduschema.ValidateTreeaudits the whole env tree (not only the files being written: deleting a viewModel default breaks an untouched page) andpushSmartFormruns it alongside the per-file pass. A missing locale key is an error (locale resolves from files only; nothing at runtime can supply it); a missing viewModel default is a warning (the bound process may fill it per request). Also reports alabel/imagebound to a default of""(the renderer rejects an empty value), a default no page references, and — for a literalcontentLoop— the exact entries missing a key the template uses. Placeholders inside a templatedcontentLoopare backend-filled and never reported, so list pages stay quiet; only a section'scontentis loop-scoped, andregexp/maskare skipped entirely so a character class like^[[:alpha:]]+$is not read as a locale token. A locale miss blocks only when the page or one of the locale files feeding it is part of the push — the same miss in an untouched page is pre-existing debt and is reported as a warning, becausepushSmartFormhas no force flag and aborting on it would strand an unrelated fix. Warnings are surfaced on a successful push under a newwarningsfield.pushSmartFormaccepted item keys and nested shapes the renderer then rejected. The swagger setsadditionalProperties: falsenowhere, so the save endpoint stores a typo'dvisibiltyor ahead[].idverbatim and the defect surfaces only as a console error in the browser — the item simply never hides, or the column list renders empty.cduschemanow derives, from the bundled swagger, the property surface of every itemclassand of the nested spots where the schema is precise (extra,options[], a table'shead[]/body[]), andValidateFilerejects a key no variant declares. The allowlist is the union over every schema variant of a class, because the swagger splits one class across type variants that each redeclare only part of the surface (valueis onEdit-intbut not onEdit-default) — checking a single variant would reject valid config. The union is then widened with the fields the renderer accepts but the swagger omits — the §4 base envelope (value,required,error,errorMsg,submitOnChange,extra) plus the §5 rows the swagger under-describes (mainMenu.options,carousel.items,comments.title,timer.extra.duration,file.extra.{downloadUrl,uploadUrl,auth},upload.extra.compression,attachment.extra.downloadUrl) — because the derived union alone was NARROWER than the documented protocol and rejected those shapes outright.TestProbeDocumentedKeysPresentInUnionnow walks the whole §5 table rather than a hand-picked subset, so a swagger update that drops a real key fails the tests instead of blocking users' pushes; a class the swagger never described (row,draggable) keeps no rule and is skipped rather than rejected. Also transcribed the renderer-only rules the swagger cannot express (it carries nominLengthanywhere): alabel/imagevaluemay not be an empty string, and animagevaluemay not be adata:URI — the renderer proxies it through/api/1.0/image?src=, which rejects the scheme with400 "URL is not allowed". Documented incdu-page-protocol.md§5.1 / §10.1.- App-generator docs: the callback
apinode snippet was incomplete and failedlint-process. All three copies of it (simulator-smart-forms-logic§2.7/§2.8,simulator-app-generator§7.2,app-generation.md§4.4) omittedformat,send_sys,debug_info,cert_pemandmax_threads. Following them verbatim produced a process that fails the JSON-schema gate (missing property 'max_threads') and theUNDERSPECIFIED API CALL NODEScheck — whose real-world symptom is a server commit that hangs ~15–20 s then reportsno response from server. All three snippets are now complete, with the cost of trimming them spelled out. Also documents thatextra.codemay be templated ("{{respCode}}"withextra_type.code:"number"), so one callback node can serve 200/205/302 instead of one node per code. - App-generator docs: the
err_node_idinvariant drove authors into a lint-flagged anti-pattern. §7.7 required anobj_type: 3escalation for every fallible node; for an error path with no work to do that produces a passthrough escalation (lint-processflags it), and padding it with a throwawayset_paramearnsUNUSED SET_PARAMplusSHARED ERROR CLUSTERS. Now states both shapes — straight to anobj_type: 2final when there is no logic, escalation only when there is — plus the two facts that make "every branch still answers the runtime" reachable: an escalation'sgomay rejoin the happy path, and each branch needs its own callback node so nine branches don't share one error terminal. - App-generator docs: generated apps looked broken by default. The two platform defaults that
wreck an unstyled app —
.section__contentshipping its own grey background pluspadding: 20px 16px 0, and[data-class="grid-one-column"]being capped and centred — were documented only insimulator-styles, which the app-generator reached for in Phase 8, long after the pages were authored. New §6.1 makes the resets, the app shell and the per-pagegrid.styleClasshook part of Phase 4; Phase 8 is now explicitly about branding rather than rescue. - Contract extraction missed real inputs in two node shapes.
api_copycarries its payload indata/data_type(it has noextrafield at all) and anapinode withformat: "raw"carries it inraw_body; neither was mentioned anywhere in the plugin, andraw_bodyhad zero occurrences. A scan reading onlyextrareports such processes as input-free — and, in a real run, reported a non-existent "sends an empty email" defect in a correct process. §1.7 /simulator-app-generator§3.6 now table all three carriers, and §3.9 asks for the field you read to be named before any defect is reported about someone else's backend. - Side-effect classification scored unreachable nodes. Corezoid never prunes orphans, so a
repurposed process keeps every sender it ever had: the reference
Cashback Categorieshas 126 nodes of which 6 are reachable, and scoring the whole bag marks a safe read-only process aslikelyand excludes it from probing. New §2.1 requires a BFS from Start (overto_node_id+err_node_id+semaphors[].to_node_id) before scoring, notes that unreachable reply nodes otherwise invent outcome branches, and adds anodes: "<reachable> of <total>"manifest field. - A declared input can be dead. The mirror of the documented output-side
paramsdrift: a process may declare aninputno node ever reads, because the value actually comes from a state-diagram read ({{conv[<id>].ref[SessionData].Session}}— one workspace-wide session, not a per-user token). New §1.7a plus adeadDeclaredInputsmanifest field. - Two verified backend facts the extraction docs were missing. §1.3: a reply value may be a
literal JSON string rather than a
{{var}}— such a process is a constant source, sorun-taskreturns empty task data and the schema must be read off the literal. And Corezoid translation refs inside that literal resolve to the empty string, not to themselves, so a regex huntingt'([A-Za-z]+)in the value can never match and a label map keyed on the ref is dead code — recover from a surviving sibling field instead (the reference FAQ usedurl). §1.8:create-aliasaccepts onlya-z,0-9and-, so an underscored sub-process name (chudo_get) is rejected outright; pick the dashed form up front. - App-generator docs: "seed every
{{key}}" was unqualified and produced false positives. Placeholders inside acontentLoopsection'scontentare loop-scoped — substituted from the entries the backend returns — so they are not viewModel keys and must not be seeded or reported as undefaulted (only the array binding itself, e.g.{{promos_loop}}, needs a default). Phase 4 now states the exclusion. Also aligns §9.1 withpushSmartForm's new cross-file token audit (locale misses are errors, viewModel misses warnings) and tells the reader to actually readwarnings, since they do not block a push; and §9.3's L3 assertion now covers unresolved[[as well as{{. - App-generator docs: the canonical node table contradicted its own error-cluster rule. §4.1
listed a single Callback GET, a single Callback SEND and one Error final while the paragraph below
it required a callback node and error terminals per branch — and §7.8 told the brief to
instantiate that table verbatim, so following it earned the
SHARED ERROR CLUSTERSthe same PR documents. §4.1 is now a spine plus a GET/SEND branch template instantiated per page and per button; only the logic-free Success final is shared. - App-generator docs: the side-effect heuristic banned every reader from
/get. "apinode with a non-GET method → strong" marks any POST-to-read processlikely, which §2.2/§5.1a then forbids on a page's/get— including the referenceTransactions history, which the coverage table itself puts there. The method is now an explicitly weak signal; the strong ones are a mutating verb in the URL path or callee name, anapi_copy, and an outbound reply nothing reads. - App-generator docs: a session token in the
302 querywas only conditionally discouraged. The query is the page URL — history,Referer, proxy logs, and any link the user shares hands over the session. A domain session token is now treated as sensitive by default: park it in a state process or actor and carry an opaque id; a bearer token reaches a URL only when the backend leaves no alternative and the user has been told. - App-generator docs: node-budget guidance counted the wrong nodes, and two size levers were
undocumented. The "split above ~60 nodes / 8 pages" threshold gave no hint that error clusters
scale mechanically with the number of fallible nodes —
layout-processmeasures 32% of both reference handlers as error nodes (77 → ~52 business, 74 → ~50) — so following it splits graphs that are comfortably inside it. §7.1 / §4.1a now say to budget business nodes only. §4.1a also gains a fourth pattern, mapping an outcome in one Code node instead of ago_if_consttree (7 mappers instead of 7 conditions plus ~20 builders and their clusters on the reference/sendhandler), stated with both sides of the trade: it removes the unbracketed-nested-paramsilent fallthrough entirely, but hides that branching from the Corezoid canvas — so thepath/page/buttonIddispatches stay real condition nodes. §7.5 adds the two structural defences that stopsubmitOnChangecorrectness depending on a hand-maintained id list: prefer zero such fields when the page has no cascade, and make the dispatch default a no-op ack rather than a submit. simulator-styles: silent Less compile failures had no documented check. A Less error is emitted as a/* Less Error … */comment at serve time and the page renders unstyled;pushSmartFormdoes not compile CSS andappGetPagenever returns it, so nothing reported it. Adds a verified local recipe, including the three traps that break the obvious command: Smart Form partials have no.lessextension,lessccannot read a<(…)process-substitution path (EBADF), and the npm package isless— a package literally namedlesscexists and is not the compiler.- Smart Form visibility placeholders rejected by
pushSmartForm. Page configs may use a pure{{viewModelKey}}placeholder for form, section, and rendered-itemvisibility; validation now accepts that server-resolved form while still rejecting malformed or embedded placeholders. appGetPage/appSendFormcould not carry a pagequery, making the platform's own session pattern untestable. A Smart Form is stateless: a302answers{nextPage, query}and the next page reads it asbody.query.*, which is how apps carry a session token across navigation. Neither runtime tool accepted it, so a logged-in page could not be rendered at all — driving one produced a cold page that looked like a backend bug. BothappGetPageandappSendFormgainquery, flattened into the URL query string exactly as the renderer sends it — including on theappSendFormPOST, whose handler reads the query off the URL and forwards it to the process asbody.query(a body-bornequeryis overwritten there and never arrives). NewInQueryMapparam kind ininternal/tools/op.godoes the flattening — plainInQuerywould have sent the whole object as one opaque value, silently dropping the session; it rejects a non-object and skips blank keys / nil values (a nil would otherwise render as the literal"null").- Telemetry: unsynchronized
telemetryEmailread/write. The opt-in email was stored in a plainvar string, written byAskForEmailOnce(afterlogin) and read byMiddlewareon every tool call — safe under the current single-threaded stdio transport, but a data race undergo test -raceif a concurrent transport (HTTP/SSE) were ever added. Now usesatomic.Pointer[string], matching theatomic.Booldiscipline already used for the telemetryenabledflag.
[2.5.0] - 2026-07-14
Added
- Actor geolocation —
geoPosition/geoNameoncreateActor/updateActor(#74). An actor now carries an optional real-world position independent of its formdata:geoPosition—{"lat": <number>, "lon": <number>}in WGS84 decimal degrees, ornullto clear — andgeoName— a location-name string (max 255 chars), ornull. Coordinates are validated backend-side: latitude is hard-bounded to -90..90 (out of range rejected), longitude outside ±180 wraps cyclically, and values round to 6 decimals;lat/lonare set as a pair. Documented in/simulator-actorsanddocs/entities/actors.md; drift snapshot and an eval scenario updated.
[2.4.2]
Fixed
- Kiro MCP server path resolution in a dev checkout.
.mcp.kiro.json's${KIRO_PLUGIN_ROOT:-$PWD/.kiro/..}fallback resolved to the repo root (notplugins/simulator/) when a developer opened the repo directly in Kiro without runninginstall-kiro.sh, so the server tried (and failed) to run<repo>/mcp-server/run.sh. It now probes formcp-server/run.shat that path and falls back toplugins/simulator/when missing, with a final guard that prints a clear error and exits if neither candidate has it.install-kiro.shalso nowsed-resolves the same fallback to the absolute plugin path (escaping\,&, and the#delimiter) when generating a workspace'smcp.json, instead of a plain copy, so an external workspace's config no longer depends onKIRO_PLUGIN_ROOTat all. README's Kiro install instructions updated to match.
[2.4.1]
Fixed
pushSmartFormfailed on Windows when creating files in a new subfolder (e.g. a new Smart Form pagepages/<id>/config). Phase 2 mapped the server's create response back to local paths usingfilepath.Dir, which yields backslash-separated paths on Windows and misses the slash-keyed folder map — so the push aborted with "server did not return id for created file …" even though the server had already created the folder and files (a subsequentpullSmartFormshowed them). The response mapping now reusesresolveParentID(the sameToSlash-normalized lookup used when POSTing), keeping the key consistent across OSes. macOS/Linux behaviour is unchanged (ToSlashis a no-op there).
[2.4.0]
Added
- extend agents to any actor, not just user twins
- add /simulator-agents digital-twin agent skill + findAgent/getAgent
- add /simulator-agents digital-twin agent skill + findAgent/getAgent
- expose hole field on createLink; document edge-hole
- add simulator-styles skill (#61)
- resolve target entity before creating; offer Total for debit/credit pairs (#60)
- add AWS Kiro support (#42)
Changed
- fix Codex test step — no Plugin Directory GUI in CLI
- fix Codex plugin commands (install→add, update flow)
- record CDU Smart Forms doc changes under 2.4.0 (#68)
- record CDU Smart Forms doc changes under 2.4.0
- detect submitOnChange by buttonId, not buttonData.action (#67)
- document CDU rendering gotchas (#62)
- getForm filter guidance — request
form, notsections(#66) - document CDU form links & button.extra spec (#64)
- note #60 skill behaviour under 2.3.0
- release v2.1.0
[2.4.0]
Added
/simulator-styles— Smart Form (CDU) styling skill (#61). A dedicated specialist for thestyle/styles/(Less/CSS) layer of Smart Forms: theme tokens, page/form/section layout, component re-skinning, reusable patterns, and design-system approaches. It reuses the existingpullSmartForm/pushSmartForm/deploySmartFormcycle and consumes thestyleClasshooks that/simulator-smart-formsattaches — the two skills hand off explicitly (structure vs. styling). Ships with a new rendered-DOM reference (docs/user-flows/cdu-dom-tree-reference.md) mapping each page-config element to the tag tree and stable class hooks the CSS must target. Also corrects the CDU section/layout model incdu-page-protocol.mdand/simulator-smart-forms: a section has nofooterslot, the wizard header class issteps(notstepper), androwis authored via the baserow/wfields (the renderer synthesizes the component) withwas a relative weight.- CDU Smart Form rendering gotchas (#62). New §12 in
cdu-page-protocol.md(with pointers from/simulator-smart-formsand/simulator-smart-forms-logic) documenting non-obvious renderer behaviour verified againstcontrol-cdu: arowrenders as a CSStable, so its items do not wrap on desktop (usedisplay:inline-blockitems incontentfor a wrapping grid) — thoughrowdoes collapse to one column at the mobile breakpoint;styleClasson arowis dropped from the DOM (style the leaf components); aradio/selectoption "dot" is a JSS-hashed<i>(not the<input>) — restyle via[class*="icon"]/[class*="content"]and drive the selected state off the.checkedclass (onebuttonper option is the version-proof card-picker alternative); and avisibility:"visible"field hidden via CSS still submits (the idiomatic hidden value carrier). - CDU form links & full
button.extraspec (#64).cdu-page-protocol.mdand/simulator-smart-formsnow document that[url=https://…]text[/url]bbcode renders a clickable<a target="_blank">(raw<a>HTML is escaped — use[url];[iurl]gives a same-tab link), and complete thebutton.extrareference:url(opens the URL instead of submitting) withtarget(_self/_blank, honoured by newer renderers — older ones open the same tab),action:'logout',request(a barefetchbefore submit, which proceeds only if it resolves),autoSubmit {interval,maxCount}(interval clamped 5–60s),options[](a click menu that bypassesurl/request/action/submit),icon,rounded,mobileVisible.
Fixed
getFormfilter guidance (#66). The tool Summary told callers to keepsections, butfilterprojects top-level keys only and a form's fields live underform.sections, sofilter=…,sections(and the dottedform.sections) returned nothing. It now says to requestform; the top-leveldescription(the form's purpose) is unchanged.submitOnChangedetection in/simulator-smart-forms-logic(#67). §2.3a/§2.9.3 taught detecting a field change viabody.buttonData.action != ""and listedcheck/radioas emitting a non-emptyaction— but onlyselectsendsbuttonData(verified againstcontrol-cdu);radio,check,toggle,edit, … send{}, so that guard silently routed those changes to the real-submit path (the wizard "jumping a step" / a half-filled form persisted). Detection now dispatches onbody.buttonId(which §2.3 already recommends);actionstays only as aselectselect-vs-filtersignal.
[2.3.0]
Added
pictureObjectoncreateActor/updateActor— custom-image ("napkin") nodes (#57). An actor can now render a custom image AS the node body instead of a standard form node: passpictureObject{"img": "data:image/png;base64,…" (PNG/SVG data URI), "width": 800, "height": 8, "type": "napkin"}. The image is anchored at its centre and keeps the source aspect ratio (setwidth;heightfollows) — e.g. a wide-and-short source PNG for a thin divider line. Uses: dividers/separators, a custom shape or icon the form catalogue does not cover, or an embedded picture/logo on a graph. Documented indocs/entities/actors.mdand/simulator-graph.refonupdateActor— re-key an actor in place (#59).updateActornow accepts an optionalrefbody param, reaching parity withcreateActor: an existing actor can be given or reassigned its external business key (1–255 chars, unique performId) without recreating it (which would mint a new UUID and break its links/placements). Resolve it afterwards withgetActorByRef(formId, ref); omitrefto leave it unchanged. (The backendPUT /actors/actor/{formId}/{actorId}acceptsrefin the body even though it is absent from that endpoint's request schema in the drift spec — verified live; the drift gate only checks method/path/operationId, so it stays green.)getLayerActorsPaginated— paginated layer reads for large graphs (#58).getLayerActorsloads a whole layer in one call and the backend rejects it with a400(Layer is too large (… nodes, … edges, total: …). Maximum allowed: N.) once nodes+edges exceed the layer-size cap (~300), which previously left the assistant unable to read big layers. The new curated tool maps toGET /graph_layers/paginated/{actorId}and returns one page of eithernodesoredges(type,limit≤ 50,offset,filter); walkoffsetpertypeto traverse a layer of any size.getLayerActorsnow documents the size cap and points at the paginated tool, and/simulator-graphdefaults to the paginated read (calllayerStats, then page) rather than the whole-layer call. The graph skill also now teaches how to extract the layeractorIdfrom a pasted graph URL (.../graph/<graphActorId>/layers/<layerActorId>→ the segment after/layers/) and is explicit that "read the nodes on a graph/layer" means reading that layer's placements — never a workspace-widesearchActors/filterActors, which previously pulled in unrelated chats, daily reports and other forms.buildLink'slayerhelp/params were also reworded to use the same graph-URL vocabulary (/graph/<graphActorId>/layers/<layerActorId>) so the two docs agree on which segment is the graph folder vs. the layer.- Edge styling surfaced on
manageLayerActors(#55). The edgelayerSettingsdescription now documents the full set of pong-server edge-styling keys that already ride through the passthroughitemsarray —lineStyle(solid/dashed/dotted),curveStyle(curved/rounded/roundedDownward/straight),color(6-digit#RRGGBBhex, no shorthand/alpha),width(integer ≥ 1) androutingPoints({w,d}manual-routing waypoints) — so an edge's colour, thickness and curve are now discoverable and settable. A new "Styling edges" section in/simulator-graphshows the pattern (to restyle an edge already on the layer, delete its placement and re-create it). Documentation only — the fields already reached the wire. - Skill registry — data-driven, user-authored playbooks (the platform analogue of these built-in skills). A skill is an actor of the new
Skillssystem form: itstitle+ref(slug) are cheap discovery metadata and itsdescriptionholds the full procedure (which MCP tools to call, with concrete entity ids) for a workspace-specific task like "create a smart contract". Workspace members can teach the assistant new procedures without a plugin release.- New MCP tools
findSkill(discover by intent; empty query lists all published skills) andgetSkill(load one in full byref/slug orid). Both are local composite tools (resolve theSkillssystem form + compose existing PAPI reads), so they are outside the OpenAPI drift gate. - New
/simulator-skillsskill (discover/run + author) and a Step-0 "check the skill registry" hook in/simulator. Skills are discovered by intent or invoked explicitly by slug (/skill <slug>). - Reference:
docs/entities/ai-skills.md. Skill bodies are treated as author-proposed plans, not system instructions; destructive/outward steps still require confirmation, and onlyverifiedskills are dispatched. - Requires the paired pong-server change (the
Skillssystem form + seeding migration, and a reaction-agent system-prompt protocol — running saved skills, gated to workspaces with ≥1 published skill, and authoring new ones, always available so the first skill can be created from the platform).
- New MCP tools
/simulator-agents— digital-twin agents: talk to a person as an agent and delegate work (the people-analog of the skill registry above). Every workspace user has a 1:1 twin actor (systemObjType="user"on theSystemform) whosedescriptionholds an "# Agent" competency profile (System instructions + Knowledge — what they do, what they know, whether they fit a task); the registry is the workspace's user twins instead of theSkillsform. The delegation procedure — discover the right person, load and adopt their profile, then either do the task autonomously (under the caller's access), find a better-suited person, or hand the decision to the user (create a task / send a p2p message) — is layered on top and reuses the/simulator-tasksand/simulator-chatflows.- New MCP tools
findAgent(discover people by competency over their twin profiles viasearchAll; empty query lists members viagetUsers) andgetAgent(load one person's "# Agent" profile in full byuserId— get-or-creates the twin — oractorId). Both are local composite tools (agents.go, resolve theSystemsystem form + compose existing PAPI reads), so — likefindSkill/getSkill— they are outside the OpenAPI drift gate. - New
/simulator-agentsskill and reference docdocs/entities/digital-twin-agents.md(the twin-as-agent model, the "# Agent" profile format,findAgent/getAgent, inject-as-instruction, the caller-access boundary), plus cross-references from/simulator,/simulator-chat, and/simulator-tasks. - A twin profile is treated as data, not system instructions (prompt-injection guard, mirroring the skill registry): it cannot escalate privileges or skip confirmations, and everything runs under the caller's PAPI access — the twin never grants the target person's privileges. Outward/destructive actions still require confirmation. Populating the "# Agent"
descriptionis done by an external factory from git activity and is out of scope here. - Any actor can be an agent, not just user twins. An agent is now defined as any actor whose
descriptionholds an "# Agent" profile — the common case is a person's user twin, but a non-user system actor (a service/bot/device twin) or a plain business actor (a team, department, organization, process, service) can be one too.findAgentgains an optionalformId(a numeric form id, tolerant of a JSON-number value): it still defaults to the user-twin registry (Systemform), but a form id targets another agent-registry form; an empty query enumerates the chosen registry (getUsersfor the default,filterActorsfor aformId). The search is always form-scoped — findAgent never runs an unscoped workspace-wide actor search (that would rank arbitrary actors as agents, since/search'sfilteris field projection, not an "# Agent" predicate); use the generalsearchActors/searchAlltools for cross-form discovery.getAgentdocuments thatactorIdloads any agent actor (not only a person twin). The/simulator-agentsskill anddocs/entities/digital-twin-agents.mdare reframed around agent-as-actor: discovery branches on the result'ssystemObjType(a"user"twin vs a candidate non-user agent, whose "# Agent" profile is confirmed viagetAgentbefore use), and delegation offers the choices that fit the agent — task / p2p message for a person, or routing to the members behind a non-user agent / triggering a runnable actor. Person twins remain the default and behave exactly as before (backward compatible).
- New MCP tools
Fixed
buildLinkopen-layer URL. An id-lessentity=layerlink now builds the canonical/graph/<graphActorId>/layers/<layerActorId>shape: the open graph (folder, from the UI context'sactiveGraph) fills the/graph/slot and the open layer (activeLayer) is the focused element. Previously it droppedactiveLayerinto the/graph/slot and never usedactiveGraphat all, yielding/graph/<layerId>/layers. Platforms that send onlyactiveLayer(noactiveGraph), or a degenerateactiveGraph == activeLayer, keep the old fallback (/graph/<layerId>/layers) so existing links still resolve; in that fallback any caller-suppliedfocusIdis dropped rather than appended, since the layer already occupies the path slot (avoids a malformed/graph/<layer>/layers/<focusId>). Requires the paired pong-server change: the public/graph_layers/paginated/{actorId}route now declaresoperationId: 'getLayerActorsPaginated'. The committed drift spec (internal/tools/testdata/papi-openapi.json) is updated to match; re-dump it from pong-server (yarn dump-openapi) on the next refresh.- AI behaviour, from QA feedback.
- Form knowledge is read, not guessed.
getForm's field-filter example now includesdescription, and its Summary tells the assistant to keepdescription/sectionswhenever it needs to understand a form — so following the tool's guidance no longer projects the form's purpose text away./simulator-formsinstructs the assistant to includedescription/sectionsin thegetFormfilter and to read and interpret both the form-leveldescriptionand each field'ssections[].content[].description(re-reading the form rather than answering from memory). Docs (forms.md) mark these as the authoritative "knowledge" fields. Fixes the assistant inventing a field's meaning (e.g. confusing the escalationcooldownwithdelay). - Response & output conventions added to
/simulator(applied to every answer): reply in the user's language without switching mid-answer; never HTML-escape prose (>stays>, not>); platform timestamps are unixtime UTC (seconds = 10-digit, or milliseconds = 13-digit, e.g. transactioncreated_at÷1000) — convert to the user's time zone and label the offset (e.g.18:30 (UTC+3)) usingtimeZoneOffsetfrom the UI-context, falling back to labelled UTC when it is absent. The web client (pong-front-end) forwardstimeZoneOffsetin the AI-agentcontrol-events-context(pong-server is pass-through). Seedocs/entities/ui-context.mdanddocs/INTEGRATION.md§9a. - Resolve which entity to create before touching domain data (#60). A new "Step 0.5" in
/simulatortells the assistant to resolve a named business entity ("smart contract", "supplier", …) before calling any entity-specific tool: search template forms first (searchForms/getFormsontitle/description), then check for prior actors of the matched form (searchActors/searchAll), and only create after the user confirms. A workspace-specific business form takes priority over any platform system mechanism that merely sounds related — incidentally stumbling ontoAccountTriggers/Tagswhile inspecting data is a data point to mention, not a substitute for the form search, so the task no longer silently pivots into configuring a similarly-named mechanism. An already-resolved term is reused within the same conversation instead of re-searching/re-asking. Fixes the assistant defaulting toAccountTriggersfor "smart contract". /simulator-financeoffers Total for directional account pairs (#60). WhengetAccountsreturns both adebitand acreditaccount for the same(nameId, currencyId)pair, the assistant now offers Total (the net balance,credit.amount − debit.amount) as a third choice alongside Debit/Credit, and asks which the user wants before proceeding. Total is always computed, never a stored row — matching pong-server's ownincomeType: 'total'semantics (credit − debitover the two directional rows).
- Form knowledge is read, not guessed.
[2.2.0]
Added
- add AWS Kiro support (#42)
[2.2.0]
Added
- AWS Kiro support. The same plugin payload now installs on Kiro alongside the existing Claude Code and Codex hosts via a symmetric overlay:
plugins/simulator/.kiro-plugin/plugin.json,plugins/simulator/.mcp.kiro.json,plugins/simulator/steering/simulator.md, and a root-levelPOWER.mddistribution manifest for kiro.dev/powers. plugins/simulator/scripts/install-kiro.shsets up an existing Kiro workspace from a cloned repo: copies the MCP entry, symlinks the steering file, hard-copies each skill into.kiro/skills/<name>/, andsed-substitutes$CLAUDE_PLUGIN_ROOTin everySKILL.mdwith the absolute plugin path (Kiro does not substitute the token on its own, unlike Claude Code and Codex). Idempotent — re-run after agit pullto refresh the workspace overlay.
Notes
- The canonical
SKILL.mdfiles keep$CLAUDE_PLUGIN_ROOT— Claude Code and Codex both resolve that exact token via host-side text substitution (anthropics/claude-code#48230, #47789, #44057) and renaming it would break doc loading on both. The install-timesedsubstitution ininstall-kiro.shis the only host-specific bit. - There is no release-zip Kiro overlay artifact in this version. A pre-built zip would still need a post-extract substitution step (the token can only be resolved to an absolute path that the user actually checked out), so the clone +
install-kiro.shpath is currently the only correct install flow for Kiro.
[2.1.0]
Added
updateLayerPositionsMCP tool — reposition actors already present on a layer within the same layer (usemoveActorsto move actors between layers).updateAccountNamegains atransferOnlyboolean parameter; transfer-only behaviour documented in the finance skill and the accounts entity doc (mirrors pong-server CE-15565).
Changed
- Bump
github.com/mark3labs/mcp-gofrom 0.54.1 to 0.55.0. - CI and release workflows: bump
actions/checkoutv4→v7,actions/setup-gov5→v6,actions/upload-artifactv4→v7,actions/attest-build-provenancev2→v4,softprops/action-gh-releasev2→v3.
Fixed
buildLinkchat deep-links now point at a conversation correctly:/chats/<acc>/list/chats/<chatActorId>?tab=chat(the stream segment defaults to the standardchatsstream and the required?tab=chatquery is included).idis the chat-actor UUID; omit it to open the chat list.- AI agent now finds files the user attached to their triggering message. The message is a reaction under the root actor, so its files live on the reaction, not the actor — the agent was calling
getActorAttachmentson the root actor and reporting "no attachments". Documented that an actor's own attachments and the triggering message's attachments are two distinct sets, both read viagetActorAttachments(<id>)→readAttachment(the actor for "files on this actor", the triggering reaction for "the file I sent"). Added anactiveReactionfield to the UI context (control-events-context) so the platform can hand the trigger id directly, with agetReactions(... orderValue=DESC)fallback when it's absent. (PopulatingactiveReactionrequires a matching pong-server change.) - UI-context guidance now states that
activeActor(the actor the user is viewing) outranks the root actor the agent was triggered on: "this / the current / the open actor" resolves toactiveActor. Fixes the AI agent answering about the root/console actor instead of the on-screen one. (Paired with a pong-serverbuildPromptchange that asserts the same priority in the agent's prompt.)
[2.0.0]
First public release of the Simulator.Company plugin for Claude Code / Codex.
Added
- MCP server (Go) wrapping the Simulator.Company REST API and exposing ~100 curated tools across actors, forms, graph, finance, access, reactions, attachments, charts, users and Smart Forms.
- Authentication: API-key flow plus OAuth2 PKCE with MCP Elicitation; TLS verification on by default.
- Curated tool surface declared as typed operations and validated against the backend OpenAPI spec by a drift gate.
- Skills covering the master router plus actors, forms, graph, finance, access, reactions, attachments, charts, chat, meetings, tasks, init, and Smart Forms (author, logic, runtime).
- Smart Form runtime —
appGetPage/appSendFormdrive any CDU / Script mini-app conversationally; convention-free discovery via the form's own title / description / tags. - Local helper tools:
buildLink,getBbcodeTags,readAttachment(text / image / binary-aware). - Plugin manifests for Claude Code and Codex, plus the local agents marketplace.
- Architecture and per-entity documentation, contributor guides, and MIT license.