Open Claude Code in your juniper/ folder and say:
Use the mantle-new-project skill to scaffold a plant-shop helper called Juniper. Two skills: recommend a plant for a room, and check stock. Text only, no voice.
Claude Code reads
mantle-new-project/SKILL.md —
a playbook that contains the full, correct template for every file — and
builds the project. Then it runs the lint, because the skill tells it to
finish that way.
One request, working project. But you should never ship a file you couldn’t explain, so let’s walk through what appeared and why each file has the shape it has. Each shape is a decision, and most of the decisions were paid for.
pyproject.toml — three lines that are all trap-avoidance
requires-python = ">=3.11,<3.13"
dependencies = [
"rasa-pro==3.20.0.dev6",
]
[tool.uv]
prerelease = "allow"
- The pin says
devand that is correct. The Mantle engine ships only on the3.20.0.devpre-release line; the newest stable rasa-pro contains no engine package at all. This is the least intuitive fact in the whole ecosystem — Chapter 3 makes you feel it. prerelease = "allow"— without it, uv refuses to resolve a dev pin.- The
>=3.11floor — Mantle 3.20 requires it, and the error you get with a lower floor doesn’t mention Python. (Also a Chapter 3 exhibit.)
lib/engine.py — one import to rule them all
try: # 3.20.0.dev1 and later
from rasa.mantle.tools.decorator import ToolContext, tool
from rasa.mantle.tools.result import ToolResult
except ImportError: # 3.19.x and earlier
from rasa.calm_v2.tools.decorator import ToolContext, tool
from rasa.calm_v2.tools.result import ToolResult
Rasa renamed the engine package (rasa.calm_v2 → rasa.mantle) in
3.20.0.dev1 — old name gone, not aliased. Every tool in your project
imports from lib.engine, so the day the name changes again, you edit one
file. This works because tool discovery keys off an attribute the @tool
decorator sets, not the import path.
agent.yml — read the comment, it’s earning its keep
agent:
id: juniper
language: en
persona: |
You are Juniper, a friendly plant-shop helper.
Ask one question at a time and keep answers short.
Never invent stock numbers — always use tools.
# TOP-LEVEL keys, siblings of `agent:`. Nested inside they parse without
# error and are silently discarded.
name: Juniper
description: Plant-shop helper for recommendations and stock checks
rules:
- 'Be warm, clear, and brief.'
- 'Never state a stock count that did not come from a tool.'
The comment marks the site of the 39-ignored-rules failure from the series
intro. persona lives inside agent:; name, description, and rules
live outside it. Get that backwards and everything still parses, trains,
and runs — minus its guardrails. This is silent trap #1, and the reason the
scaffold writes the warning into the file itself.
integrations.yml — the LLM is a reference, not a config
llm:
model_group: orchestrator
model_groups:
- id: orchestrator
models:
- provider: openai
model: gpt-4.1-mini
api_key: ${OPENAI_API_KEY}
temperature: 0.0
Older tutorials (most of the internet, honestly) put provider: and model:
directly under llm:. Since 3.20.0.dev6 that form is rejected — llm:
names a model group, and the provider details live on the group. Two
practical notes:
${OPENAI_API_KEY}is filled from your environment or.envat runtime — the name of the key is config, the value never is.temperature: 0.0because this model routes your conversation; routing wants determinism, not creativity.
memory.yml vs skills/*/memory.yml — who is allowed to write
Root memory.yml is project memory: facts every skill shares, written
by tools only.
preferred_room:
type: text
description: The room the customer is shopping for.
Skill folders get their own memory.yml — and that’s where fields the LLM
may set live, marked llm_settable: true. Putting that flag in the root
file is rejected by the engine. The mental model: project memory is the
database, skill memory is the scratchpad. The scaffold’s shapes come from
the catalog’s tools-and-memory tutorial project,
where a bank agent uses exactly this split to make you verify once, not
three times.
skills/recommend_plant/skill.md — three languages, one file
---
name: Recommend Plant
description: >
Suggest a plant for the customer's room. Activate for plant advice,
what should I get, or what survives in a given room.
---
Help the customer pick a plant for their room.
if: not session.recommend_plant.room_type
Ask what kind of room the plant is for — light level matters most.
if: session.recommend_plant.room_type
Call `suggest_plants` for @memory.recommend_plant.room_type and present
the top two options briefly. Never invent a plant name.
Three languages stacked: YAML frontmatter (config), Markdown prose (what the
LLM reads), and if: lines at column zero (branching logic). The
description doubles as the router’s activation hint — write your trigger
phrases into it. And note the two reference styles: session.x.y in the
if: conditions, @memory.x.y in the prose. They are not interchangeable —
that’s silent traps #4 and #5, both waiting for you in Chapter 3.
The rest
.env.example (committed, names your two keys, values empty), .gitignore
(with .env in it — that one line is what makes “paste your key in .env”
safe advice), a Makefile (Chapter 4), and tools/ for shared tools.
Now prove the whole thing hangs together:
python3 scripts/lint_mantle.py
ok — 0 finding(s) across 9 check(s)
Commit it — your first commit passes through the hook you installed:
git add -A && git commit -m "Scaffold Juniper with the Mantle starter pack"
Green, saved, and you can explain every file. Next chapter we take a hammer to it.
