
Msw Behaviourtree
- 4.2k installs
- 33 repo stars
- Updated July 29, 2026
- msw-git/msw-ai-coding-plugins-official
msw-behaviourtree is an agent skill that generates MSW .behaviourtree files and refreshes bt-spec.md from scanned ActionNode and DecoratorNode codeblocks.
About
msw-behaviourtree is an MSW agent skill for creating and maintaining .behaviourtree files plus the project-specific .behaviourDocs/bt-spec.md catalog. Step zero runs scripts/build-spec.cjs to scan every .codeblock whose paired .mlua extends ActionNode or DecoratorNode, capturing definitionId UUIDs, propertyKey names, and CoreVersion-stamped MODNativeType strings from Environment/config. The skill mints file and node UUIDs, resolves built-in composite versus custom action and decorator definitionIds, builds Blackboard variables from the spec type map, wires nodeProperties to paired .mlua defaults, and self-validates parent-child graph consistency before save. Fixed skeletons skeleton-minimal.json and skeleton-full.json illustrate RootNode graphs with optional Culler-related fields replaced by the live CoreVersion. Triggers include create behaviourtree, refresh bt-spec, Blackboard variables, and Korean phrases for BT authoring. Custom nodes must exist in bt-spec sections two and three; missing nodes stop generation instead of inventing UUIDs. Validation step seven catches definitionId, propertyKey, or version mismatches after assembly.
- Build-spec script scans ActionNode and DecoratorNode .mlua pairs into bt-spec.md with real UUIDs.
- End-to-end flow covers UUID minting, Blackboard variables, nodeProperties, and graph validation.
- Built-in SequenceNode SelectorNode ParallelNode use name-as-definitionId with btNodeType rules.
- Custom node existence check blocks invented definitionIds when names are missing from bt-spec.
- References skeleton-minimal.json, skeleton-full.json, and node-catalog.md for authoring conventions.
Msw Behaviourtree by the numbers
- 4,164 all-time installs (skills.sh)
- +540 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #11 of 247 Game Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
msw-behaviourtree capabilities & compatibility
- Capabilities
- bt spec.md generation via build spec.cjs scan · uuid minting for file entrykey and nodeids · blackboard variable typing from modnativetype ma · nodeproperties resolution from paired .mlua file · built in versus custom definitionid assignment r · post assembly parent child validation checklist
- Use cases
- orchestration · debugging · frontend
- Platforms
- Windows · macOS · Linux
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-behaviourtreeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.2k |
|---|---|
| repo stars | ★ 33 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 29, 2026 |
| Repository | msw-git/msw-ai-coding-plugins-official ↗ |
How do I author a valid MSW behaviour tree without stale definitionIds, wrong CoreVersion types, or missing custom node properties?
Author MSW .behaviourtree files end-to-end by scanning custom nodes, building bt-spec.md, and validating Blackboard wiring.
Who is it for?
MSW project developers adding patrol, chase, or combat AI trees with custom action and decorator codeblocks.
Skip if: Non-MSW engines, pure Lua gameplay scripts without .behaviourtree assets, or projects lacking Environment/config CoreVersion.
When should I use this skill?
User asks to create behaviourtree, refresh bt-spec, wire Blackboard variables, or add Sequence Selector graphs in MSW.
What you get
A validated .behaviourtree graph with accurate Blackboard variables, nodeProperties, and parent-child links tied to bt-spec.md.
- .behaviourtree file
- .behaviourDocs/bt-spec.md
- node catalog
Files
MSW BehaviourTree
End-to-end authoring skill for MSW .behaviourtree files. Owns both the project-specific authoring spec (<ProjectRoot>/.behaviourDocs/bt-spec.md) and the tree generation itself. Fixed graph rules and skeletons live in this skill's references/; the per-project spec is (re)built by this skill's local scripts/build-spec.cjs.
---
🚦 Execution order (follow this sequence)
0. Build / refresh the project spec (bt-spec.md)
The spec is the source of truth for every project-specific data point: each custom action/decorator node's definitionId, btNodeType, visible propertyKey names, and the serialized Type.type strings stamped to this project's CoreVersion.
When to (re)build:
- First time working on BT in a project (no
.behaviourDocs/bt-spec.mdyet). - After any change that affects BT node surface area:
- new / renamed / removed
.codeblockwhose paired.mluaextendsActionNode/DecoratorNode - added / removed / renamed
propertylines in such a.mlua Environment/configCoreVersionbumped (the serialized type strings are version-tagged).- The user says they recently added/changed a BT codeblock or a
.mluaproperty — stale UUIDs / missing properties silently produce broken trees. - The downstream validation (Step 7) flags a
definitionId,propertyKey, or version mismatch.
How to run — invoke this skill's local script:
node "scripts/build-spec.cjs" --projectRoot "<MSW project root>"If the current working directory is already the MSW project root, --projectRoot can be omitted. Requires Node.js on PATH (no other dependencies — pure stdlib fs/path).
Optional overrides (long flags, case-insensitive):
| Flag | Default | Notes |
|---|---|---|
--projectRoot | current working directory | MSW project root to scan |
--outputPath | <ProjectRoot>/.behaviourDocs/bt-spec.md | folder is created if missing |
--coreVersion | read from <ProjectRoot>/Environment/config (CoreVersion field) | required if the config is missing |
Example with overrides:
node "scripts/build-spec.cjs" --projectRoot "C:/path/to/project" --coreVersion 26.5.0.0The script throws if Environment/config is absent and --coreVersion is not passed — there is no fallback default.
What the spec contains:
1. Project metadata — project root, CoreVersion, generated time, discovered node counts. 2. Built-in composite node names and their fixed definitionId / btNodeType. 3. Custom action nodes — Name, definitionId, btNodeType, visible property names. 4. Custom decorator nodes — same shape as action nodes. 5. Type map — mlua type to serialized MODNativeType.type plus Blackboard ObjectValue shape.
UUIDs come from real .codeblock files in the project — the spec never invents them. @HideFromInspector properties are filtered out automatically. Fixed authoring rules, file skeletons, and validation checklists live in this skill's references/ rather than in the generated spec.
After (re)building, read the freshly written <ProjectRoot>/.behaviourDocs/bt-spec.md and continue with the steps below. The compact spec intentionally lists only property names; when constructing nodeProperties, resolve each property's mlua type/default from the paired .mlua file, then use the type map in bt-spec.md §4 for propertyType.type.
Also read `references/skeleton-minimal.json` for the smallest valid tree, `references/skeleton-full.json` for a Composite+Decorator+Action+Blackboard example with all optional fields populated, `references/node-catalog.md` for fixed graph rules, and any existing .behaviourtree in the project (**/*.behaviourtree) to mirror conventions. Replace {CORE_VERSION} in the skeletons with the CoreVersion from bt-spec.md — both at the top level and inside every MOD.Core.* type string in Blackboard variables and nodeProperties.
1. Collect input from the user
Confirm via context, or ask via AskUserQuestion if anything is ambiguous:
| Item | Description | Example |
|---|---|---|
name | Display name for the tree | "PatrolAndChase" |
| Save path | .behaviourtree location (relative to project root) | RootDesk/MyDesk/PatrolAndChase.behaviourtree |
| Tree shape | Intended node graph (root composite + children) | Sequence → [Chase, MoveTo] |
| Custom nodes | Action/decorator codeblocks the tree references | Chase, MoveTo, Jump |
| Blackboard variables | Variable name + type + initial value | TargetEntity: Entity, MoveSpeed: number = 10.0 |
| Node properties | For each custom node, which property maps to which Blackboard variable | Chase.TargetEntityKey = "TargetEntity" |
Custom-node existence check (mandatory): every custom action/decorator name the user mentions must appear in bt-spec.md §2 / §3. If a referenced node is not in the spec, stop and ask the user — do not invent a UUID, do not assume a node exists by name, and do not skip rerunning Step 0.
2. Mint UUIDs
You need:
- One UUID for the file → goes into
EntryKeyandContentProto.Json.id(both identical, both prefixedbehaviourtree://). - One UUID for each node in
Nodes(nodeId).
node -e "console.log(require('node:crypto').randomUUID())"Mint up front, write into a scratch table, then assemble. Don't reuse the file UUID as a nodeId.
3. Resolve every definitionId
| Node category | definitionId value | btNodeType |
|---|---|---|
Built-in composite (SequenceNode, SelectorNode, ParallelNode) | Same string as nodeName | 1 |
| Custom action node | value from bt-spec.md §2 | 0 |
| Custom decorator node | value from bt-spec.md §3 | 2 |
Custom-node UUIDs come from bt-spec.md (which read them from real .codeblock files) — never any other source.
4. Build the Blackboard
For each variable, copy the Type.type string and ObjectValue shape verbatim from bt-spec.md §4. The version-tagged substring (Version=<CoreVersion>) must match exactly — a typo silently breaks deserialization.
Variables is an ordered array; each entry: { Name, Type: { "$type": "MODNativeType", type: "<from spec>" }, ObjectValue: <from spec> }. The ObjectValue does not include a $type discriminator (unlike Value in .model files).
For Component / ComponentRef, ComponentId is <entity-uuid>:<ComponentName> (engine component) or <entity-uuid>:<scriptCodeblockUuid>:<ScriptComponentName> (script component). Mirror an existing serialized example in the project.
Numeric ObjectValues use float literal form (3.0, not 3).
4.5 Resolve node property values
For each custom node that needs nodeProperties:
1. Confirm the propertyKey exists in bt-spec.md §2 / §3 for that node. 2. Find the paired .mlua by searching for script <NodeName> extends ActionNode or script <NodeName> extends DecoratorNode under the project. If multiple files match, prefer the one whose sibling .codeblock has the exact definitionId UUID from bt-spec.md; if still ambiguous, ask the user. 3. Read the visible property declarations in that .mlua, ignoring @HideFromInspector properties. This gives the mlua type and default value. 4. Include a nodeProperties entry only when the user provided a value, the behavior requires a non-default value, or a *Key property must point at a Blackboard variable. Omit optional properties that can safely use the .mlua default. 5. For *Key string properties, set propertyValue to the Blackboard variable name. Infer the variable by name and getter usage when obvious (MoveSpeedKey -> MoveSpeed, TargetEntityKey -> TargetEntity). If more than one Blackboard variable could match, ask. 6. For literal properties, use the user-provided value. If no value is provided and the .mlua default is meaningful, omit the property instead of serializing a guessed value. 7. If OnBehave checks a property for nil, empty string, or invalid enum and no value can be inferred, ask the user before writing the tree.
nodeProperties entry shape:
{
"propertyKey": "<property name>",
"propertyType": { "$type": "MODNativeType", "type": "<type from bt-spec.md §4>" },
"propertyValue": <value>
}5. Assemble Nodes
Hard graph constraints (validate before writing):
- RootNode is not a parent node. It must not have
childNodes. It only storesstartNodeId, andstartNodeIdpoints to exactly one node inNodes. - If the tree needs several top-level behaviors, use either one Composite as the single
startNodeId, or one Decorator as the singlestartNodeIdwhosedecoChildNodeswraps a Composite or another Decorator chain that eventually wraps a Composite. Put the multiple behaviors under that Composite'schildNodes. - Exactly one node in
Nodesmay havenodeParentId: "": the node referenced byRootNode.startNodeId. Do not create multiple root-level Action/Composite/Decorator nodes. - Composite (
btNodeType: 1) is the only node category that can own multiple children throughchildNodes. - Decorator (
btNodeType: 2) is only a wrapper/parent for exactly one Action, Composite, or Decorator node. It can also be the child of another Decorator, so Decorator-to-Decorator chains are valid. It must use singulardecoChildNodes(a singlenodeIdstring) for that one child, notchildNodes; the wrapped child must also record the Decorator's id in itsnodeParentId. - Decorators applying to the same Action MUST be chained — never flattened as siblings. Each decorator owns exactly one downstream subtree. If two or more decorators are meant to gate/modify the same Action, build a single chain
Composite → ADeco → BDeco → CDeco → Actionwhere each decorator'sdecoChildNodespoints to the next decorator (and finally the Action). Concretely: within one chain leading to a single Action, no two decorators may share the same `nodeParentId` — each decorator's parent is the previous decorator, and only the topmost decorator's parent is the Composite. Sibling decorators under one Composite are still valid when each wraps a different downstream subtree. ✅Composite → ADeco → BDeco → CDeco → Action(chain — every decorator has a unique parent within the chain). ❌Composite → [ADeco→Action, BDeco→Action, CDeco→Action](Action duplicated to bypass chaining). ❌Composite → [ADeco, BDeco, CDeco, Action](decorators flattened — they don't wrap the Action and are effectively orphaned). - Action (
btNodeType: 0) is a leaf — never has children.
Node-write invariants:
- Every
nodeIdis unique within the file. nodeParentIdof every non-root node points to a realnodeIdthat is a Composite or Decorator. It must never point toRootNode, becauseRootNodeis not represented as a node inNodes.- If a node's parent is a Composite, that Composite must include the node id in
childNodes. - If a node's parent is a Decorator, that Decorator's
decoChildNodesmust equal that node'snodeId. This is valid even when both parent and child are Decorators. - Composite
childNodes↔ childnodeParentIdis bidirectionally consistent. - Action nodes omit
childNodes. Decorator nodes omitchildNodesand use exactly onedecoChildNodes(single stringnodeId) instead. - Never write `probability`. The editor strips this field on round-trip, and the supported composites (
SequenceNode,SelectorNode,ParallelNode) do not consume per-child weights. Older generated trees in the project may still carry"probability": 1.0on every node; treat that as legacy on read but do not write it on new nodes. - Decorator nodes (`btNodeType: 2`) omit `nodePosition`. The editor positions a Decorator automatically relative to the child it wraps, and writes no
nodePositionfield for it on save. Only Composites and Actions carrynodePosition. TheRootNodeblock also carries its ownnodePosition(separate from the start node). - Empty collection fields are omitted, not serialized as `[]`. A Composite with no children yet should omit
childNodesentirely; a node with no overrides should omitnodePropertiesentirely. Empty arrays are an editor-draft artifact — do not author them. - Decorator child field is `decoChildNodes` (canonical — this is what the editor preserves on save;
ChildNodeIdis silently stripped on round-trip). It is a single string holding the wrapped child'snodeId(not an array). When reading legacy files you may still encounterChildNodeIdon hand-authored decorators; treat it as the same field. When writing, always emitdecoChildNodes. RootNode.startNodeIdreferences one of thenodeIds — an Action, Composite, or Decorator — and that node is the only node withnodeParentId: "".
*Key-suffix String properties carry the name of a Blackboard variable (resolved at runtime via BlackBoard:GetXxx). Non-Key properties carry the literal value.
6. nodePosition format
nodePosition is a JSON object with numeric x / y:
"nodePosition": { "x": 0.0, "y": 0.0 }Use float literals (0.0, not 0). The legacy string form "(0.000, 0.000)" may still appear in older hand-authored trees — read it as equivalent, but always write the object form (the BT editor canonicalizes to this shape on save, so the string form re-serializes to a noisy diff the first time the file is opened).
Editor axes: the BT editor uses a math-convention canvas — +x is right, +y is up (upper-right quadrant is positive). So a child placed at a higher y than its parent appears above the parent on screen.
Layout rule — draw the tree downward: depth grows along −y (children sit below their parent), and siblings spread along ±x around the parent's x. Typical spacing: 200 units between depth levels and 200 units between siblings.
`RootNode` block vs the start node — do not stack them at the same position. RootNode.nodePosition is the canvas anchor and stays at { "x": 0.0, "y": 0.0 }. The start node (the node referenced by startNodeId) must sit one level below that anchor — putting it at (0, 0) makes it visually overlap the RootNode marker on the editor canvas. Treat the RootNode anchor as depth 0 and the start node as depth 1.
RootNode.nodePosition:{ "x": 0.0, "y": 0.0 }(fixed anchor — never moves)- Start node (depth 1, referenced by
startNodeId):{ "x": 0.0, "y": -200.0 } - Single child of the start node (depth 2):
{ "x": 0.0, "y": -400.0 } - Two children of the start node (depth 2):
{ "x": -100.0, "y": -400.0 }and{ "x": 100.0, "y": -400.0 } - Each additional level: parent.y − 200
Never place a child at a y greater than or equal to its parent's y — that draws upward and overlaps the parent visually. The same rule applies between RootNode and the start node: the start node must be at y ≤ -200 (strictly below the anchor).
Decorator nodes do not carry `nodePosition`. The editor lays them out automatically relative to the wrapped child. Omit the field on every btNodeType: 2 node; it appears only on RootNode, Composites (btNodeType: 1), and Actions (btNodeType: 0).
7. Write and validate
Write the JSON file, then run this checklist. In particular:
- [ ]
EntryKeyisbehaviourtree://{uuid}and matchesContentProto.Json.idexactly. - [ ] Top-level
Id,GameId,Contentare"".Usage,UseService,DynamicLoadingare0.UsePublishis1.CoreVersionmatches the project (Environment/config).StudioVersionis0.1.0.0.ContentTypeisx-mod/behaviourtree.ContentProto.UseisJson. - [ ]
RootNodehas nochildNodes;RootNode.startNodeIdmatches exactly onenodeIdinNodes; that start node hasnodeParentId: ""; and no other node hasnodeParentId: "". - [ ] Every
nodeParentIdis""or an existingnodeId. - [ ] All
nodeIdvalues are unique. - [ ] For every Composite, the set of
childNodesIDs equals the set of nodes whosenodeParentIdis this Composite. - [ ] Every Action has no
childNodes. Every Decorator has nochildNodes, has exactly onedecoChildNodes(singlenodeIdstring —ChildNodeIdis the legacy variant; the editor strips it on round-trip), and that id points to exactly one Action, Composite, or Decorator child whosenodeParentIdpoints back to the Decorator. Decorator-to-Decorator parent/child chains are valid and must be checked with the samedecoChildNodes↔nodeParentIdrule. - [ ] Decorator chain rule: when multiple decorators apply to the same Action, they form a single chain (
Composite → ADeco → BDeco → … → Action). Verify by walking each Action upward to its enclosing Composite: the decorators encountered along that one path must all have uniquenodeParentIdvalues (i.e. each decorator's parent is the previous decorator, never another decorator that already appeared in the chain). Two decorators in the same chain sharing anodeParentIdis invalid. (Sibling decorators under one Composite that wrap different downstream subtrees are fine — uniqueness is per-chain, not global.) - [ ] No node serializes
"probability". (Legacy1.0values may appear on read but are never authored.) - [ ] Every Composite and Action carries
nodePositionin object form{ "x": <num>, "y": <num> }with float literals — no legacy"(x.xxx, y.yyy)"strings on write. Decorator nodes carry no `nodePosition` at all. - [ ] Start node is not stacked on the RootNode anchor.
RootNode.nodePositionis{ "x": 0.0, "y": 0.0 }and the node referenced bystartNodeIdhasy ≤ -200.0(typically{ "x": 0.0, "y": -200.0 }). If the start node is a Decorator (nonodePosition), the first wrapped Composite/Action down the chain must satisfy this offset instead. - [ ] No node serializes empty arrays — a Composite with no children omits
childNodes; a node with no overrides omitsnodeProperties. Do not write"childNodes": []or"nodeProperties": []. - [ ] Every custom node's
definitionIdis copied frombt-spec.md(never invented). - [ ] Every
nodeProperties[].propertyKeymatches a property inbt-spec.mdfor that node. - [ ] Every
*Keyproperty'spropertyValuematches aBlackboard.Variables[].Nameof the right type. - [ ] Every type string is copied verbatim from
bt-spec.md§4 — version-tagged, typo-fragile. - [ ] Version cross-check: every
MOD.Core.*type string'sVersion=X.Y.Z.Zsubstring (inBlackboard.Variables[].Type.typeandNodes[].nodeProperties[].propertyType.type) equals the file's top-levelCoreVersion. Mismatch silently breaks deserialization — common whenbt-spec.mdis stale relative to the project's currentCoreVersion. If they differ, re-run Step 0 before writing. (System.*types use the immutableVersion=4.0.0.0and are exempt.) - [ ] JSON parses:
node -e "JSON.parse(require('node:fs').readFileSync(process.argv[1],'utf8'))" "<path>"If any check fails, fix it before reporting done.
---
📂 Files in / consumed by this skill
- `scripts/build-spec.cjs` — Node.js script that scans the project and emits
<ProjectRoot>/.behaviourDocs/bt-spec.md. Invoked in Step 0. <ProjectRoot>/.behaviourDocs/bt-spec.md— compact generated catalog. Source of truth for node names,definitionId,btNodeType, property names, and type strings. Written by the script above; consumed by Steps 1–7.- `references/skeleton-minimal.json` — smallest valid tree (empty Blackboard, single Composite root with no children).
- `references/skeleton-full.json` — Composite → Decorator → Action with
nodeProperties(literal +Key-suffix) and a populatedBlackboard. Use this as the shape reference whenever the tree is non-trivial. - `references/node-catalog.md` — narrative explanation of
btNodeTypevalues, valid graph shapes, theKey-suffix convention, and how the spec builder discovers nodes (kept for reference; the runtime catalog itself lives inbt-spec.md).
---
🔁 Edit workflow (existing file)
1. Read the entire file — never Edit blind. UUIDs and the parent/child graph must stay consistent. 2. Never change the file's wrapper UUID (EntryKey / ContentProto.Json.id) — external references break. 3. Adding a node: mint a fresh nodeId, append to Nodes, update the parent Composite's childNodes, set the new node's nodeParentId. 4. Removing a node: remove from Nodes, remove its ID from any Composite's childNodes. If it was a Composite, decide whether to re-parent or remove its children — never leave dangling nodeParentId references. 5. If the edit involves a custom node name, property, or type that may have changed in the project since the spec was last built, re-run Step 0 first. 6. Re-run the Step 7 validation checklist after every edit.
BT Node Catalog
Reference for filling nodeName, definitionId, and btNodeType correctly. The catalog is project-agnostic — it describes the format and how to discover what nodes are available in any project, not a fixed list of node names.
btNodeType values
| Value | Category | Notes |
|---|---|---|
0 | Action (leaf) | .mlua declares script X extends ActionNode. No childNodes. |
1 | Composite | Built-in (e.g. SequenceNode, SelectorNode, ParallelNode). Has childNodes. |
2 | Decorator | .mlua declares script X extends DecoratorNode. Wraps a single child (or a single sub-tree). Inferred value — confirm against an existing decorator-using BT in the project before relying on it. |
Valid graph shapes (parent ↔ child rules)
These are hard constraints — violating them produces a tree that loads but does not behave correctly. Validate before writing the file.
| Parent | Allowed children | Notes |
|---|---|---|
RootNode (startNodeId) | Exactly one node — Composite, Decorator, or Action | RootNode is not a parent node and must not have childNodes. startNodeId is a single id, not a list. Exactly one node may have nodeParentId: "", and it must be the startNodeId node. To run several actions in sequence/parallel/etc., the start node must be one Composite or one Decorator that wraps a Composite, directly or through a Decorator chain. Never model multiple root children. |
Composite (btNodeType: 1) | One or more children of any kind (Action, Composite, Decorator) | This is the only node category that holds multiple children. childNodes is the ordered list. |
Decorator (btNodeType: 2) | Exactly one child — Action, Composite, or another Decorator | Decorators can be attached as the parent/wrapper of one Action, Composite, or Decorator node, and can themselves be attached under another Decorator. Use singular decoChildNodes (a single nodeId string, not an array) for that one child, not childNodes; the wrapped child's nodeParentId must point back to the Decorator. The legacy field name ChildNodeId may appear in older hand-authored files — read it as equivalent, but the editor strips it on round-trip, so always write decoChildNodes. Decorator nodes carry no `nodePosition` — the editor lays them out automatically relative to their wrapped child. |
Action (btNodeType: 0) | None (leaf) | Action nodes never have children. Omit childNodes entirely. If you need a sequence of actions, put them under a Composite, not chained under each other. |
Inverse formulation (top-down):
- Want multiple actions to run? → put a Composite above them.
- Want to gate / loop / cooldown a sub-tree? → put a Decorator above it (above a Composite, an Action, or another Decorator).
- Want multiple gates / loops / cooldowns on the same sub-tree? → chain Decorator → Decorator → Action/Composite/Decorator with one
decoChildNodesat each Decorator layer. - Want a single action at the root? → allowed, but rare; usually wrap with a Composite or Decorator anyway.
Common mistakes to reject during planning:
- ❌ RootNode with a
childNodesarray. RootNode must only point to onestartNodeId. - ❌ Two or more root-level nodes with
nodeParentId: "". The format only executes onestartNodeId; multiple root-level actions/composites/decorators are invalid for generated trees. - ❌ Two or more actions directly under the root (no parent Composite). Put them under a single Composite start node, or under a Composite wrapped by one or more Decorators.
- ❌ An Action with
childNodespopulated. Actions are leaves. - ❌ A Decorator with zero children or more than one child. Decorators can only parent one Action, Composite, or Decorator.
- ❌ A Decorator with
childNodes. Decorators use singulardecoChildNodes(a singlenodeIdstring), and that id must match its single wrapped child. - ❌ Any node other than the
startNodeIdnode withnodeParentId: "". Re-parent it under the start Composite or remove it. - ❌ *Multiple decorators meant to apply to the same Action laid out as siblings of a Composite instead of chained. When two or more decorators must wrap one Action, they form a single chain — each decorator's child is the next decorator (or finally the Action), so each decorator in the chain has a unique `nodeParentId`. ✅ `Composite → ADeco → BDeco → CDeco → Action` (chain). ❌ `Composite → [ADeco, BDeco, CDeco, Action]` (decorators flattened as siblings; the decorators are orphaned with no Action to wrap, and the Action is unguarded). ❌ `Composite → [ADeco→Action, BDeco→Action, CDeco→Action]` (Action duplicated to bypass chaining). The shared-`nodeParentId` red flag: if two decorators share the same `nodeParentId` value yet the user described them as gating/modifying the same Action, the structure is wrong — chain them. Sibling decorators under one Composite are only valid when each wraps a distinct* downstream Action/subtree.
Discovering available nodes in any MSW project
The creator should normally consume <ProjectRoot>/.behaviourDocs/bt-spec.md, generated by msw-behaviourtree-spec-builder. That compact spec is the source of truth for node names, definitionId, btNodeType, and valid property names.
If the spec is missing or stale, regenerate it first. If you are debugging discovery manually, use the same logic as the builder:
1. Glob the project for **/*.codeblock. 2. For each .codeblock, read ContentProto.Json.Name and ContentProto.Json.Id. 3. Find the sibling .mlua with the same base name. 4. Classify the node from the .mlua declaration: script X extends ActionNode -> custom action (btNodeType: 0), script X extends DecoratorNode -> custom decorator (btNodeType: 2). 5. Use the codeblock Id as definitionId: codeblock://{Id}.
The ContentProto.Json.Target field can be useful as a fallback, but do not rely on it as the primary classifier; generated BT nodes may have a missing or unreliable target.
Finding property metadata
The compact spec lists only property names. For each property you need to serialize, find the node's paired .mlua. The .mlua's visible property declarations enumerate exactly which propertyKey strings are valid and what mlua type/default each one has.
property string TargetPositionKey = "" → propertyKey: "TargetPositionKey", propertyType: System.String
property number MoveSpeed = 10.0 → propertyKey: "MoveSpeed", propertyType: System.Double
property bool IsActive = false → propertyKey: "IsActive", propertyType: System.Boolean@HideFromInspector property … declarations are runtime-only state — never include them in nodeProperties.
Confirm built-in composite names
Built-in composites use the node name itself as `definitionId` (no codeblock:// prefix). To find which built-in names this version of the engine accepts, prefer one of:
1. Read an existing .behaviourtree file in the project and harvest the nodeName / definitionId strings used on btNodeType: 1 nodes. 2. Check engine documentation / Environment/NativeScripts/ if available.
Common names that BT engines typically expose — verify before use:
SequenceNode— runs children left-to-right; fails fast on first failure; succeeds when all succeed.SelectorNode— runs children left-to-right; succeeds fast on first success.ParallelNode— runs children concurrently.
If a name isn't confirmed by either source, ask the user instead of guessing.
nodeProperties ↔ .mlua property mapping
Each entry in nodeProperties must correspond to a property name listed for that node in bt-spec.md and declared in the node's .mlua. The propertyType.type and propertyValue shape MUST match the declared type — use the type map in bt-spec.md §4.
{V} is the engine version stamped into the file (e.g. 26.5.0.0). Match the project's existing files.
*Primitives (System.)**
.mlua declaration | propertyType.type | propertyValue |
|---|---|---|
bool | System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 | true / false |
string | System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 | "<string>" |
integer | System.Int64, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 | <int> (also used for enum-like operator properties — e.g. BlackboardCondition_* nodes serialize Operator: 2 as System.Int64) |
number | System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 | <num> (use 3.0 not 3) |
MOD.Core types
.mlua declaration | propertyType.type | propertyValue shape |
|---|---|---|
Vector2 | MOD.Core.MODVector2, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "x": <num>, "y": <num> } |
Vector3 | MOD.Core.MODVector3, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "x": <num>, "y": <num>, "z": <num> } |
Vector4 | MOD.Core.MODVector4, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "x": <num>, "y": <num>, "z": <num>, "w": <num> } |
Color | MOD.Core.MODColor, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "r": <0..1>, "g": <0..1>, "b": <0..1>, "a": <0..1> } |
Entity | MOD.Core.MODEntity, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "tempEntityId": null, "IsRelative": false, "EntityId": "<entity-uuid>", "Version2": false } |
Component | MOD.Core.Component.MODComponent, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "IsRelative": false, "ComponentId": "<entity-uuid>:<ComponentName>", "UseNested": false } |
ComponentRef | MOD.Core.MODComponentRef, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null | { "IsRelative": false, "ComponentId": "<entity-uuid>:<ComponentName>", "UseNested": false } |
EntityRef | MOD.Core.MODEntityRef, MOD.Core, Version={V}, Culture=neutral, PublicKeyToken=null ⚠ inferred | uncertain — Grep the project for a real example before using |
Component (live binding) and ComponentRef (reference) share the same ComponentId payload shape — <entity-uuid>:<ComponentName> where <ComponentName> is the engine component (TransformComponent, AttackComponent, …) or, for script components, the form is <scriptCodeblockUuid>:<ScriptComponentName>. Mirror an existing serialized example.
For any type not in the table, do not guess — Grep the project for a serialized example and copy the type string verbatim.
`Key`-suffix convention: properties whose name ends with Key (e.g. TargetEntityKey, MoveSpeedKey) are String properties whose propertyValue is the name of a Blackboard variable — the script resolves the actual value at runtime via BlackBoard:GetNumber(self.MoveSpeedKey) etc. Properties without the Key suffix carry the literal value directly.
{
"Id": "",
"GameId": "",
"EntryKey": "behaviourtree://{UUID_FILE}",
"ContentType": "x-mod/behaviourtree",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "{CORE_VERSION}",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"id": "behaviourtree://{UUID_FILE}",
"name": "{NAME}",
"RootNode": {
"startNodeId": "{UUID_ROOT_COMPOSITE}",
"nodePosition": { "x": 0.0, "y": 0.0 }
},
"Blackboard": {
"Variables": [
{
"Name": "bb_Bool",
"Type": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"ObjectValue": true
},
{
"Name": "bb_TargetPosition",
"Type": {
"$type": "MODNativeType",
"type": "MOD.Core.MODVector3, MOD.Core, Version={CORE_VERSION}, Culture=neutral, PublicKeyToken=null"
},
"ObjectValue": { "x": 0.0, "y": 0.0, "z": 0.0 }
}
]
},
"Nodes": [
{
"nodeId": "{UUID_ROOT_COMPOSITE}",
"nodeName": "SequenceNode",
"definitionId": "SequenceNode",
"btNodeType": 1,
"nodeParentId": "",
"nodePosition": { "x": 0.0, "y": -200.0 },
"childNodes": [
"{UUID_DECORATOR}"
]
},
{
"nodeId": "{UUID_DECORATOR}",
"nodeName": "BlackboardCondition_Bool",
"definitionId": "codeblock://{DECORATOR_DEFINITION_ID}",
"btNodeType": 2,
"nodeParentId": "{UUID_ROOT_COMPOSITE}",
"nodeProperties": [
{
"propertyKey": "Key",
"propertyType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"propertyValue": "bb_Bool"
},
{
"propertyKey": "Operator",
"propertyType": {
"$type": "MODNativeType",
"type": "System.Int64, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"propertyValue": 2
},
{
"propertyKey": "CompareValue",
"propertyType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"propertyValue": true
}
],
"decoChildNodes": "{UUID_ACTION}"
},
{
"nodeId": "{UUID_ACTION}",
"nodeName": "SetBlackboardValue_Bool",
"definitionId": "codeblock://{ACTION_DEFINITION_ID}",
"btNodeType": 0,
"nodeParentId": "{UUID_DECORATOR}",
"nodePosition": { "x": 0.0, "y": -600.0 },
"nodeProperties": [
{
"propertyKey": "Key",
"propertyType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"propertyValue": "bb_Bool"
},
{
"propertyKey": "Value",
"propertyType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"propertyValue": true
}
]
}
]
}
}
}
{
"Id": "",
"GameId": "",
"EntryKey": "behaviourtree://{UUID_FILE}",
"ContentType": "x-mod/behaviourtree",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "{CORE_VERSION}",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"id": "behaviourtree://{UUID_FILE}",
"name": "{NAME}",
"RootNode": {
"startNodeId": "{UUID_ROOT}",
"nodePosition": { "x": 0.0, "y": 0.0 }
},
"Blackboard": {
"Variables": []
},
"Nodes": [
{
"nodeId": "{UUID_ROOT}",
"nodeName": "SequenceNode",
"definitionId": "SequenceNode",
"btNodeType": 1,
"nodeParentId": "",
"nodePosition": { "x": 0.0, "y": -200.0 }
}
]
}
}
}
// Project-agnostic BehaviourTree spec generator.
// Scans a project for BT codeblocks (.codeblock with paired .mlua extending
// ActionNode/DecoratorNode), parses property declarations, and emits a Markdown
// spec consumed by the msw-behaviourtree-creator skill.
//
// Output: <ProjectRoot>/.behaviourDocs/bt-spec.md (created if missing).
//
// Usage:
// node build-spec.cjs
// node build-spec.cjs --projectRoot "C:/path/to/project" --outputPath "./bt-spec.md"
// node build-spec.cjs --coreVersion 1.2.3.4
'use strict';
const fs = require('fs');
const path = require('path');
// --- Arg parsing -------------------------------------------------------------
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith('-')) continue;
const key = a.replace(/^-+/, '');
const next = argv[i + 1];
if (next !== undefined && !next.startsWith('-')) {
out[key.toLowerCase()] = next;
i++;
} else {
out[key.toLowerCase()] = true;
}
}
return out;
}
const args = parseArgs(process.argv.slice(2));
let projectRoot = args.projectroot;
let outputPath = args.outputpath;
let coreVersion = args.coreversion;
if (!projectRoot) {
projectRoot = process.cwd();
}
projectRoot = fs.realpathSync(projectRoot).replace(/[\\/]+$/, '');
if (!outputPath) {
const docsDir = path.join(projectRoot, '.behaviourDocs');
if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
outputPath = path.join(docsDir, 'bt-spec.md');
}
// --- Read CoreVersion from project config -----------------------------------
if (!coreVersion) {
const configPath = path.join(projectRoot, 'Environment', 'config');
if (!fs.existsSync(configPath)) {
throw new Error(`CoreVersion config not found at ${configPath}. Pass --coreVersion explicitly.`);
}
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (!cfg.CoreVersion) {
throw new Error(`CoreVersion field missing in ${configPath}.`);
}
coreVersion = cfg.CoreVersion;
}
// --- mlua type -> propertyType.type / Blackboard Type.type / value-shape map -
// Order is preserved (Map keeps insertion order) so the output table matches the ps1 script.
const mluaTypeMap = new Map([
['bool', { TypeStr: 'System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089', ValueShape: 'true / false' }],
['boolean', { TypeStr: 'System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089', ValueShape: 'true / false' }],
['string', { TypeStr: 'System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089', ValueShape: '"<string>"' }],
['integer', { TypeStr: 'System.Int64, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089', ValueShape: '<int>' }],
['number', { TypeStr: 'System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089', ValueShape: '<num> (use 3.0 not 3)' }],
['Vector2', { TypeStr: `MOD.Core.MODVector2, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "x": <num>, "y": <num> }' }],
['Vector3', { TypeStr: `MOD.Core.MODVector3, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "x": <num>, "y": <num>, "z": <num> }' }],
['Vector4', { TypeStr: `MOD.Core.MODVector4, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "x": <num>, "y": <num>, "z": <num>, "w": <num> }' }],
['Color', { TypeStr: `MOD.Core.MODColor, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "r": <0..1>, "g": <0..1>, "b": <0..1>, "a": <0..1> }' }],
['Entity', { TypeStr: `MOD.Core.MODEntity, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "tempEntityId": null, "IsRelative": false, "EntityId": "<entity-uuid>", "Version2": false }' }],
['Component', { TypeStr: `MOD.Core.Component.MODComponent, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "IsRelative": false, "ComponentId": "<entity-uuid>:<ComponentName>", "UseNested": false }' }],
['ComponentRef', { TypeStr: `MOD.Core.MODComponentRef, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '{ "IsRelative": false, "ComponentId": "<entity-uuid>:<ComponentName>", "UseNested": false }' }],
['EntityRef', { TypeStr: `MOD.Core.MODEntityRef, MOD.Core, Version=${coreVersion}, Culture=neutral, PublicKeyToken=null`, ValueShape: '(verify against an existing serialized example before use)' }],
]);
// --- mlua property parser -----------------------------------------------------
// Returns array of { Name, Type, Default }. Skips properties annotated with
// @HideFromInspector on a preceding line.
function getMluaProperties(mluaPath) {
const result = [];
if (!fs.existsSync(mluaPath)) return result;
const text = fs.readFileSync(mluaPath, 'utf8');
const lines = text.split(/\r?\n/);
let pendingAnnotations = [];
const propRe = /^property\s+(\w+)\s+(\w+)\s*(?:=\s*(.+?))?\s*$/;
for (const raw of lines) {
const line = raw.trim();
if (line === '') continue;
if (line.startsWith('@')) {
pendingAnnotations.push(line);
continue;
}
const m = line.match(propRe);
if (m) {
const hidden = pendingAnnotations.includes('@HideFromInspector');
if (!hidden) {
result.push({
Name: m[2],
Type: m[1],
Default: m[3] !== undefined ? m[3] : '',
});
}
pendingAnnotations = [];
continue;
}
if (!line.startsWith('--')) {
pendingAnnotations = [];
}
}
return result;
}
// --- Recursive file walker (avoids node_modules / .git churn) ----------------
function* walkCodeblocks(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) {
if (e.name === 'node_modules' || e.name === '.git') continue;
yield* walkCodeblocks(full);
} else if (e.isFile() && e.name.toLowerCase().endsWith('.codeblock')) {
yield full;
}
}
}
// --- Discover BT codeblocks ---------------------------------------------------
console.log(`Scanning ${projectRoot} for *.codeblock ...`);
const btNodes = [];
let nonBtCount = 0;
let failCount = 0;
const scriptActionRe = /^\s*script\s+\w+\s+extends\s+ActionNode\b/m;
const scriptDecoratorRe = /^\s*script\s+\w+\s+extends\s+DecoratorNode\b/m;
for (const fullPath of walkCodeblocks(projectRoot)) {
try {
const j = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
const cp = j && j.ContentProto && j.ContentProto.Json;
if (!cp) { nonBtCount++; continue; }
const mluaPath = fullPath.replace(/\.codeblock$/i, '.mlua');
const mluaExists = fs.existsSync(mluaPath);
let kind = null;
if (mluaExists) {
const mluaText = fs.readFileSync(mluaPath, 'utf8');
if (scriptActionRe.test(mluaText)) kind = 'Action';
else if (scriptDecoratorRe.test(mluaText)) kind = 'Decorator';
}
if (!kind) {
const tgt = cp.Target;
if (tgt === 'MOD.Core.BTNodes.ActionNode') kind = 'Action';
else if (tgt === 'MOD.Core.BTNodes.DecoratorNode') kind = 'Decorator';
}
if (!kind) { nonBtCount++; continue; }
const props = getMluaProperties(mluaPath);
btNodes.push({
Name: cp.Name,
Id: cp.Id,
Kind: kind,
BtNodeType: kind === 'Action' ? 0 : 2,
RelPath: fullPath.substring(projectRoot.length).replace(/^[\\/]+/, ''),
MluaExists: mluaExists,
Properties: props,
});
} catch (err) {
failCount++;
console.warn(`Failed to parse ${fullPath}: ${err.message}`);
}
}
const byName = (a, b) => (a.Name || '').localeCompare(b.Name || '');
const actions = btNodes.filter(n => n.Kind === 'Action').sort(byName);
const decorators = btNodes.filter(n => n.Kind === 'Decorator').sort(byName);
console.log(`Found ${actions.length} action nodes, ${decorators.length} decorator nodes`);
console.log(`Non-BT codeblocks: ${nonBtCount}, parse failures: ${failCount}`);
if (actions.length === 0 && decorators.length === 0) {
console.warn('');
console.warn('WARNING: zero Action and zero Decorator nodes discovered.');
console.warn(' - If the project genuinely has no BT codeblocks yet, create them in the Maker BehaviourTree editor first, then re-run this script.');
console.warn(' - If you expected nodes here, the mlua-lsp environment may be rejecting `script <Name> extends ActionNode`/`extends DecoratorNode` declarations. In that case, the .codeblock + .mlua pairs must be created through the Maker BT editor GUI (the agent cannot author them directly). Falling back to a code-driven AI pattern (@BTNode extends BTNode + AIComponent:CreateNode) is an alternative.');
console.warn('');
}
// --- Emit markdown ------------------------------------------------------------
function pad2(n) { return String(n).padStart(2, '0'); }
function nowStamp() {
const d = new Date();
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
}
const lines = [];
const push = (s = '') => lines.push(s);
push('# BehaviourTree Authoring Spec');
push();
push(`- **Project root**: \`${projectRoot}\``);
push(`- **Engine CoreVersion**: \`${coreVersion}\``);
push(`- **Generated**: ${nowStamp()}`);
push(`- **Discovered**: ${actions.length} action nodes, ${decorators.length} decorator nodes`);
push();
push('> Compact catalog for tree construction. Custom-node UUIDs were read from real `.codeblock` files in this project -- never invent them.');
push();
push('---');
push();
// 1. Built-in composites
push('## 1. Built-in composite nodes');
push();
push('| nodeName | definitionId | btNodeType |');
push('|---|---|---|');
push('| `SequenceNode` | `SequenceNode` | 1 |');
push('| `SelectorNode` | `SelectorNode` | 1 |');
push('| `ParallelNode` | `ParallelNode` | 1 |');
push();
function formatPropertyList(node) {
if (!node.MluaExists) return '(!) paired .mlua not found';
if (node.Properties.length === 0) return '(none)';
return node.Properties.map(p => `\`${p.Name}\``).join('<br>');
}
function emitNodeSection(title, list) {
push(`## ${title}`);
push();
if (list.length === 0) { push('_(none discovered)_'); push(); return; }
push('| Name | definitionId | btNodeType | Properties |');
push('|---|---|---|---|');
for (const n of list) {
push(`| \`${n.Name}\` | \`codeblock://${n.Id}\` | ${n.BtNodeType} | ${formatPropertyList(n)} |`);
}
push();
}
emitNodeSection('2. Custom action nodes', actions);
emitNodeSection('3. Custom decorator nodes', decorators);
// 4. Type map
push('## 4. Type map');
push();
push('Use this for `nodeProperties[].propertyType.type` and `Blackboard.Variables[].Type.type`. `ObjectValue shape` applies to Blackboard variables.');
push();
push('| mlua type | serialized type | ObjectValue shape |');
push('|---|---|---|');
for (const [k, info] of mluaTypeMap) {
push(`| \`${k}\` | \`${info.TypeStr}\` | \`${info.ValueShape}\` |`);
}
push();
// Write file (BOM-less UTF-8)
fs.writeFileSync(outputPath, lines.join('\r\n') + '\r\n', { encoding: 'utf8' });
const sizeKb = (fs.statSync(outputPath).size / 1024).toFixed(1);
console.log(`Wrote ${outputPath} (${sizeKb} KB)`);
Related skills
How it compares
Pick msw-behaviourtree over generic AI prompting when MSW projects require spec-compliant .behaviourtree assets tied to registered custom nodes.
FAQ
When must bt-spec.md be rebuilt?
On first BT work, after codeblock or .mlua property changes, or when CoreVersion bumps alter serialized types.
Where do custom node UUIDs come from?
Only from real .codeblock files scanned into bt-spec.md; the skill never invents definitionIds.
What script generates the spec?
Run node scripts/build-spec.cjs with --projectRoot pointing at the MSW project root.
Is Msw Behaviourtree safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.