---
name: space
description: Space mental model (the infinite work surface), tool boundaries, and command reference. The single entry point for any agent operating on a Huabu Space.
appliesTo: [ask, operate, external]
version: 1
---

# Space

The canonical reference for working with a Huabu Space. This file covers the **read** side: the on-disk layout and the tools that turn it into structured data. Mutation is gated behind a separate tool (`space_commands`) and a separate reference file — see the **Mutating a Space** section below.

> **Schemas are the source of truth.** This skill explains _semantics_ and _idioms_. Field names, types, and required parameters live on the corresponding tool / command schema. When in doubt about a field, trust the schema.

---

## A Space has two surfaces

A Space exposes two complementary surfaces, each owned by its own family of tools. Pick the family by what you want to know, not by which tool feels familiar.

### Surface A — A folder of text files

Node text, frontmatter, skill files, memory, history. Read with `read`, `find`, `ls`, `grep`.

```
<Space>/
  nodes/
    <safeLabel>.md         # one file per node: YAML frontmatter + Markdown body
  memory/*.md              # long-form, agent-curated memory
  skills/<id>/SKILL.md     # per-Space skill overrides (optional)
  .artifacts/<id><ext>     # raw bytes (image / pdf / video / cover); `read` returns images inline, rejects pdf / video
  .history/                # saved threads, event log (rarely needed)
```

A node's filename is deterministically derived from its `label` and kept in 1:1 sync, so when you have the label you can build the path yourself:

```
nodes/<safeLabel>.md
```

`safeLabel` = the label with characters `\ / : * ? " < > |` (and ASCII control chars) replaced by `_`. **Spaces, hyphens, parentheses, dots, and any other character are kept verbatim** — do not substitute them. Only leading/trailing dots and spaces are stripped, and the result is truncated to 120 chars. Empty / missing labels fall back to the stable `nodeId`.

Examples:

| label               | safeLabel           | path                         |
| ------------------- | ------------------- | ---------------------------- |
| `Dolphin Migration` | `Dolphin Migration` | `nodes/Dolphin Migration.md` |
| `Notes (draft)`     | `Notes (draft)`     | `nodes/Notes (draft).md`     |
| `Foo: Bar / Baz`    | `Foo_ Bar _ Baz`    | `nodes/Foo_ Bar _ Baz.md`    |
| `   trailing.`      | `trailing`          | `nodes/trailing.md`          |

- Have the label → `read("nodes/<safeLabel>.md")` directly. **Don't** `find` first when you already have the label — build the path.
- Have only the nodeId → `find("nodes/*.md")` or `grep` (results carry `nodeId`, `label`, `nodeType`); the stable id also lives in each file's `id:` frontmatter.
- A direct read returns ENOENT (rare — usually means the label was just edited mid-flight) → fall back to `find` / `grep`.

### Surface B — Layout & connectivity data (`space.json`)

Where each node sits, how big it is, which frame it belongs to, what colour it's painted; every edge's endpoints and style. Stored as one `space.json` per Space, served through `get_space_outline`, `inspect_nodes`, `inspect_edges`. These tools also expose **derived fields you can't read off disk** — `distance`, `direction`, `hops`, `clusterId`, `arrangement` — computed on the fly from the same data, which is why you should query through them rather than `read("space.json")` and parse it yourself.

### Boundary rule of thumb

| Property                                  | Tool                |
| ----------------------------------------- | ------------------- |
| label, content, summary, keywords, src    | `read` / `grep`     |
| position, size, parentFrame, visual style | `inspect_nodes`     |
| edge direction / lineStyle / stroke       | `inspect_edges`     |
| whole-Space overview, spatial clusters    | `get_space_outline` |

## Tool decision matrix

| Question                                       | Tool                                                     |
| ---------------------------------------------- | -------------------------------------------------------- |
| "Give me the lay of the land"                  | `get_space_outline()`                                    |
| "What does this node say?"                     | `read("nodes/<safeLabel>.md")`                           |
| "Where is this node? How big? In which frame?" | `inspect_nodes({ ids: ["<id>"] })`                       |
| "What's near this node?"                       | `inspect_nodes({ nearNode: { id } })`                    |
| "What connects to this node?"                  | `inspect_nodes({ connectedTo: { id } })`                 |
| "What does that edge look like?"               | `inspect_edges({ ids: ["<edgeId>"] })`                   |
| "Which nodes mention 'gradient descent'?"      | `grep`                                                   |
| "List all PDFs"                                | `find("**/*.pdf")` or `inspect_nodes({ byType: "pdf" })` |
| "Load a deeper Space reference"                | `read("skills/space/references/<name>.md")`              |

## Gotchas

Behaviour the schema can't convey:

- ** Selection & Position Geometry (CRITICAL):** The selected-node / anchor context **DOES NOT** directly contain position, size, parent frame, or geometry coordinates. If you need to place a new node near, relative to, or offset from a selected or anchor node, **you MUST first call `inspect_nodes({ ids: ["<anchorId>"] })`** (or `get_space_outline`) to query its geometry.
- **Coordinates — idioms.** Each node carries two coordinate fields (defined on the `inspect_nodes` / `space_commands` schemas): writable **parent-local** `position` and read-only **world** `absolutePosition`. Practical placement: to put a child next to a sibling in the same frame, add an offset to the sibling's `position`; to place a root node near some reference, use the reference's `absolutePosition` (root-local == world); to align a framed child to something outside its frame, submit `target.absolutePosition − thisFrame.absolutePosition`. Into a `column`/`row`/`grid` (structured) frame, `position` is only a sort hint — the frame owns the final slot. In `grid` mode `position` does **not** decide rows either: pass `cells` on `SET_FRAME_LAYOUT` to place children, giving paired children the same `row`.
- **Nodes in context are metadata only.** Pass a node's supplied `file` path straight to `read` for the body. Only when a node is mentioned outside your context (e.g. it appears in a Space snapshot but wasn't shown as a `<node>`) do you build the path yourself via the safeLabel rule above. For spatial / structural info (including position), call `inspect_nodes({ ids: ["<id>"] })`.
- **Cross-Space reads are World-only.** Tools default to the conversation's Space. In a World conversation, first read the World outline, take `targetCanvasId` from a canonical `spacePreview`, then pass it to `get_space_outline`, `inspect_nodes`, `inspect_edges`, `read`, `grep`, `find`, or `ls`. The target must be a live ordinary Space addressed by exactly one canonical preview. `snapshot_nodes` remains owner-scoped because it materializes cache artifacts; use `read` with `targetCanvasId` to view source images inline. This never grants cross-Space writes; `space_commands` remains World-scoped.
- **`read` returns image artifacts inline** as vision content: pass an image node's frontmatter `src` straight to `read`, OR — for an inline `![](<key>)` image embedded in a note body — call `read(".artifacts/<key>")` to see it. PDF / video bytes still live under `.artifacts/` but are not readable — their `src` URL is the only handle.
- Before placing new nodes, anchor on the selection / a referenced node / a focal cluster — never pick coordinates from the global bbox alone, or new nodes land outside the user's viewport. If setting `size`, inspect and match comparable nearby nodes; with no comparable peer, omit it and use the canonical default. See `layout-recipes.md` for long-Note and per-type sizing rules.

---

## Mutating a Space

The filesystem tools (`read`, `find`, `ls`, `grep`) are read-only — there is no `write` / `edit_file` / `rm`. **Every mutation flows through one tool: `space_commands`.**

- If `space_commands` is **not** in your available tool list → you are in read-only mode. Do not attempt mutations and do not claim in your reply that you performed any.
- If `space_commands` **is** available → the tool's own schema and description carry the full command set, the id / dependency-ordering rule, and per-field behaviour. For composed multi-command recipes see `command-cookbook.md`; for diagram geometry see `layout-recipes.md`.
- **Commands are not guaranteed to succeed.** Each command in a call reports its own outcome in `results[]`; on failure it carries a `reason` (e.g. `invalid-target` when a CONNECT / SET_NODE_PARENT endpoint doesn't exist). Read it and adjust — don't assume a write landed.

---

## Deeper dives

Load on demand when the situation calls for it:

- `read("skills/space/references/command-cookbook.md")` — composed batch patterns: brainstorm, merge / synthesize, group into a frame, restyle a cluster, tidy a row, …
- `read("skills/space/references/layout-recipes.md")` — coordinate system, hierarchical / left-to-right / grid layouts, frames (including when to pick the row-aligned `grid` frame mode over `column`), and the row-track flowchart / roadmap recipe.
