references/node-catalog.md
# 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 (`SequenceNode`, `SelectorNode`, `ParallelNode`) **or custom** — `.mlua` declares `script X extends CompositeNode`. 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 `decoChildNodes` at 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 `childNodes` array. RootNode must only point to one `startNodeId`.
- ❌ Two or more root-level nodes with `nodeParentId: ""`. The format only executes one `startNodeId`; 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 `childNodes` populated. 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 singular `decoChildNodes` (a single `nodeId` string), and that id must match its single wrapped child.
- ❌ Any node other than the `startNodeId` node with `nodeParentId: ""`. 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`), `script X extends CompositeNode` -> custom composite (`btNodeType: 1`, may have `childNodes`).
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.7.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.
references/skeleton-full.json
{
"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
}
]
}
]
}
}
}
references/skeleton-minimal.json
{
"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 }
}
]
}
}
}
scripts/build-spec.cjs
// Project-agnostic BehaviourTree spec generator.
// Scans a project for BT codeblocks (.codeblock with paired .mlua extending
// ActionNode/DecoratorNode/CompositeNode), 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;
const scriptCompositeRe = /^\s*script\s+\w+\s+extends\s+CompositeNode\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';
else if (scriptCompositeRe.test(mluaText)) kind = 'Composite';
}
if (!kind) {
const tgt = cp.Target;
if (tgt === 'MOD.Core.BTNodes.ActionNode') kind = 'Action';
else if (tgt === 'MOD.Core.BTNodes.DecoratorNode') kind = 'Decorator';
else if (tgt === 'MOD.Core.BTNodes.CompositeNode') kind = 'Composite';
}
if (!kind) { nonBtCount++; continue; }
const props = getMluaProperties(mluaPath);
btNodes.push({
Name: cp.Name,
Id: cp.Id,
Kind: kind,
BtNodeType: kind === 'Action' ? 0 : kind === 'Composite' ? 1 : 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);
const composites = btNodes.filter(n => n.Kind === 'Composite').sort(byName);
console.log(`Found ${actions.length} action nodes, ${decorators.length} decorator nodes, ${composites.length} custom composite nodes`);
console.log(`Non-BT codeblocks: ${nonBtCount}, parse failures: ${failCount}`);
if (actions.length === 0 && decorators.length === 0 && composites.length === 0) {
console.warn('');
console.warn('WARNING: zero Action, zero Decorator, and zero custom Composite 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`/`extends CompositeNode` 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, ${composites.length} custom composite 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. Composites (built-in + discovered custom `extends CompositeNode`)
push('## 1. Composite nodes');
push();
push('| nodeName | definitionId | btNodeType |');
push('|---|---|---|');
push('| `SequenceNode` | `SequenceNode` | 1 |');
push('| `SelectorNode` | `SelectorNode` | 1 |');
push('| `ParallelNode` | `ParallelNode` | 1 |');
push();
if (composites.length > 0) {
push('### Custom composite nodes (`extends CompositeNode`)');
push();
push('| Name | definitionId | btNodeType | Properties |');
push('|---|---|---|---|');
for (const n of composites) {
push(`| \`${n.Name}\` | \`codeblock://${n.Id}\` | ${n.BtNodeType} | ${formatPropertyList(n)} |`);
}
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)`);
SKILL.md
---
name: msw-behaviourtree
description: "Authors MSW `.behaviourtree` files end-to-end and maintains the project-specific authoring spec (`.behaviourDocs/bt-spec.md`). Scans every `.codeblock` whose paired `.mlua` extends `ActionNode`/`DecoratorNode`/`CompositeNode` to build a compact catalog of custom action/decorator/composite UUIDs, propertyKey names, and version-stamped MODNativeType strings. Then generates the full tree: RootNode → Nodes graph, Blackboard variables, nodeProperties wiring, and self-validates parent/child consistency. Triggers: 'create behaviourtree', 'new BT', 'add a behaviour tree', 'BT node graph', '비헤이비어 트리 만들어', '.behaviourtree 생성', 'SequenceNode SelectorNode', 'Blackboard variable', 'definitionId codeblock', 'startNodeId', 'build BT spec', 'refresh bt-spec', 'generate behaviourtree catalog', 'BT 스펙 생성', 'bt-spec.md 만들어', 'rescan BT nodes'."
---
# 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/composite 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.md` yet).
- After **any** change that affects BT node surface area:
- new / renamed / removed `.codeblock` whose paired `.mlua` extends `ActionNode` / `DecoratorNode` / `CompositeNode`
- added / removed / renamed `property` lines in such a `.mlua`
- `Environment/config` `CoreVersion` bumped (the serialized type strings are version-tagged).
- The user says they recently added/changed a BT codeblock or a `.mlua` property — 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:
```bash
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:
```bash
node "scripts/build-spec.cjs" --projectRoot "C:/path/to/project" --coreVersion 26.7.0.0
```
The 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. Composite nodes — built-in names with fixed `definitionId` / `btNodeType`, plus discovered custom composites (`.mlua` declares `extends CompositeNode`).
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`](references/skeleton-minimal.json) for the smallest valid tree, [`references/skeleton-full.json`](references/skeleton-full.json) for a Composite+Decorator+Action+Blackboard example with all optional fields populated, [`references/node-catalog.md`](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/composite 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/composite name the user mentions must appear in `bt-spec.md` §1 / §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 `EntryKey` and `ContentProto.Json.id` (both identical, both prefixed `behaviourtree://`).
- One UUID for **each** node in `Nodes` (`nodeId`).
```bash
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 composite node (`extends CompositeNode`) | value from `bt-spec.md` §1 | `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 `ObjectValue`s use float literal form (`3.0`, not `3`).
> **Runtime caveat:** the Blackboard lives on the entry's tree. If any script later calls `AIComponent:SetRootNode(...)` on that component, the entry tree and this Blackboard are discarded — `BlackBoard` reads `nil` and `*Key` properties stop resolving. Do not mix runtime root swaps with Blackboard-dependent trees.
### 4.5 Resolve node property values
For each custom node that needs `nodeProperties`:
1. Confirm the `propertyKey` exists in `bt-spec.md` §1 / §2 / §3 for that node.
2. Find the paired `.mlua` by searching for `script <NodeName> extends ActionNode`, `extends DecoratorNode`, or `extends CompositeNode` 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:
```json
{
"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 stores `startNodeId`, and `startNodeId` points to exactly **one** node in `Nodes`.
- If the tree needs several top-level behaviors, use either one Composite as the single `startNodeId`, or one Decorator as the single `startNodeId` whose `decoChildNodes` wraps a Composite or another Decorator chain that eventually wraps a Composite. Put the multiple behaviors under that Composite's `childNodes`.
- Exactly one node in `Nodes` may have `nodeParentId: ""`: the node referenced by `RootNode.startNodeId`. Do not create multiple root-level Action/Composite/Decorator nodes.
- **Composite** (`btNodeType: 1`) is the only node category that can own multiple children through `childNodes`.
- **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 singular `decoChildNodes` (a single `nodeId` string) for that one child, not `childNodes`; the wrapped child must also record the Decorator's id in its `nodeParentId`.
- **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 → Action` where each decorator's `decoChildNodes` points 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 `nodeId` is unique within the file.
- `nodeParentId` of every non-root node points to a real `nodeId` that is a Composite or Decorator. It must never point to `RootNode`, because `RootNode` is not represented as a node in `Nodes`.
- 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 `decoChildNodes` must equal that node's `nodeId`. This is valid even when both parent and child are Decorators.
- Composite `childNodes` ↔ child `nodeParentId` is **bidirectionally consistent**.
- Action nodes omit `childNodes`. Decorator nodes omit `childNodes` and use exactly one `decoChildNodes` (single string `nodeId`) 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.0` on 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 `nodePosition` field for it on save. Only Composites and Actions carry `nodePosition`. The `RootNode` block also carries its own `nodePosition` (separate from the start node).
- **Empty collection fields are omitted, not serialized as `[]`.** A Composite with no children yet should omit `childNodes` entirely; a node with no overrides should omit `nodeProperties` entirely. 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; `ChildNodeId` is silently stripped on round-trip). It is a single string holding the wrapped child's `nodeId` (not an array). When *reading* legacy files you may still encounter `ChildNodeId` on hand-authored decorators; treat it as the same field. When *writing*, always emit `decoChildNodes`.
- `RootNode.startNodeId` references one of the `nodeId`s — an Action, Composite, or Decorator — and that node is the only node with `nodeParentId: ""`.
`*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`:
```json
"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:
- [ ] `EntryKey` is `behaviourtree://{uuid}` and matches `ContentProto.Json.id` exactly.
- [ ] Top-level `Id`, `GameId`, `Content` are `""`. `Usage`, `UseService`, `DynamicLoading` are `0`. `UsePublish` is `1`. `CoreVersion` matches the project (`Environment/config`). `StudioVersion` is `0.1.0.0`. `ContentType` is `x-mod/behaviourtree`. `ContentProto.Use` is `Json`.
- [ ] `RootNode` has no `childNodes`; `RootNode.startNodeId` matches exactly one `nodeId` in `Nodes`; that start node has `nodeParentId: ""`; and no other node has `nodeParentId: ""`.
- [ ] Every `nodeParentId` is `""` or an existing `nodeId`.
- [ ] All `nodeId` values are unique.
- [ ] For every Composite, the set of `childNodes` IDs equals the set of nodes whose `nodeParentId` is this Composite.
- [ ] Every Action has no `childNodes`. Every Decorator has no `childNodes`, has exactly one `decoChildNodes` (single `nodeId` string — `ChildNodeId` is the legacy variant; the editor strips it on round-trip), and that id points to exactly one Action, Composite, or Decorator child whose `nodeParentId` points back to the Decorator. Decorator-to-Decorator parent/child chains are valid and must be checked with the same `decoChildNodes` ↔ `nodeParentId` rule.
- [ ] **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 *unique* `nodeParentId` values (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 a `nodeParentId` is invalid. (Sibling decorators under one Composite that wrap *different* downstream subtrees are fine — uniqueness is per-chain, not global.)
- [ ] No node serializes `"probability"`. (Legacy `1.0` values may appear on read but are never authored.)
- [ ] Every Composite and Action carries `nodePosition` in 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.nodePosition` is `{ "x": 0.0, "y": 0.0 }` and the node referenced by `startNodeId` has `y ≤ -200.0` (typically `{ "x": 0.0, "y": -200.0 }`). If the start node is a Decorator (no `nodePosition`), 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 omits `nodeProperties`. Do not write `"childNodes": []` or `"nodeProperties": []`.
- [ ] Every custom node's `definitionId` is copied from `bt-spec.md` (never invented).
- [ ] Every `nodeProperties[].propertyKey` matches a property in `bt-spec.md` for that node.
- [ ] Every `*Key` property's `propertyValue` matches a `Blackboard.Variables[].Name` of 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's `Version=X.Y.Z.Z` substring (in `Blackboard.Variables[].Type.type` and `Nodes[].nodeProperties[].propertyType.type`) equals the file's top-level `CoreVersion`. Mismatch silently breaks deserialization — common when `bt-spec.md` is stale relative to the project's current `CoreVersion`. If they differ, **re-run Step 0** before writing. (`System.*` types use the immutable `Version=4.0.0.0` and are exempt.)
- [ ] JSON parses:
```bash
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`](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`](references/skeleton-minimal.json) — smallest valid tree (empty Blackboard, single Composite root with no children).
- [`references/skeleton-full.json`](references/skeleton-full.json) — Composite → Decorator → Action with `nodeProperties` (literal + `Key`-suffix) and a populated `Blackboard`. Use this as the shape reference whenever the tree is non-trivial.
- [`references/node-catalog.md`](references/node-catalog.md) — narrative explanation of `btNodeType` values, valid graph shapes, the `Key`-suffix convention, and how the spec builder discovers nodes (kept for reference; the runtime catalog itself lives in `bt-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.