Component reference
Every component on this page is registered in src/theme/MDXComponents.tsx, so it works in any .md or .mdx file under docs/ without an import. Each entry says what the component is for, lists its props, renders a live example, and gives the MDX source to copy.
Props in MDX are JavaScript. Pass strings in quotes, and numbers, arrays and objects in braces. For multi-line text, such as a file tree or a diff, use a template literal in braces. node scripts/check-mdx.mjs fails on any capitalized component that isn't registered, so a typo in a component name is caught before the build.
Lab structure
The components every lab uses, in the order they appear on the page.
LabVideo
A video slot at the top of every lab. Without a youtubeId it says plainly that the recording isn't made yet, instead of showing a dead player. With one, it shows a poster and loads the privacy-enhanced YouTube player only when clicked.
| Prop | Type | Default | Notes |
|---|---|---|---|
youtubeId | string | none | Leave out until the recording exists |
title | string | 'Watch this lab' | Shown on the poster |
duration | string | none | Badge on the poster, for example '12:40' |
chapters | {t: string; label: string}[] | none | Listed under the player |
<LabVideo />
<LabVideo
youtubeId="abc123XYZ"
duration="12:40"
chapters={[
{t: '0:00', label: 'The gap'},
{t: '3:15', label: 'The first hook'},
]}
/>
LabMeta
The time, what the lab builds on, the tag it centers on, what it needs, and the one idea. Everything comes from src/data/labs.ts, so the lab page and the index can never disagree.
| Prop | Type | Notes |
|---|---|---|
lab | number | The lab number in labs.ts. Renders nothing if it doesn't exist |
<LabMeta lab={5} />
TryThis
Frames an embedded playground inside a lab, so it reads as a deliberate interruption, with a link out to the full-page version.
| Prop | Type | Notes |
|---|---|---|
title | string | Required. Short and imperative |
lead | ReactNode | One sentence on what to do and what to notice |
href | string | The full playground page. Adds an "Open the full playground" link |
children | ReactNode | Required. The playground component |
Try this
Find the rule that loads
Four rules meant to stop a force push. Only one of them survives validation.
Four rules meant to stop a force push. One loads.
Which of these, in .factory/settings.json → permissionRules.rules, actually blocks `git push --force origin main`?
{ "id": "project/no-force",
"decision": "deny",
"match": { "prefix": ["git", "push", "--force"] },
"tests": { "match": ["git push --force"], "noMatch": ["git push"] } }
{ "id": "project/no-force",
"decision": "block",
"match": { "prefix": ["git", "push", "*"] },
"tests": { "match": ["git push --force"], "noMatch": ["git status"] } }
{ "id": "project/no-force",
"decision": "block",
"match": { "prefix": ["git", "push", ["--force", "-f"]] } }
{ "id": "project/no-force",
"decision": "block",
"reason": "History on shared branches is evidence.",
"match": { "prefix": ["git", "push", ["--force", "-f"]] },
"tests": {
"match": ["git push --force origin main", "git push -f"],
"noMatch": ["git push origin main"] } }<TryThis
title="Find the rule that loads"
lead="Four rules meant to stop a force push. Only one of them survives validation."
href="/playground/spot-the-bug">
<SpotTheBug puzzle="rules" />
</TryThis>
Q
A numbered discussion question the learner answers in their own notes. Answers go in the facilitator keys, never on the lab page. Each renders with the anchor #qN, so a facilitator can link to it.
| Prop | Type | Notes |
|---|---|---|
n | number | Required. Numbered from 1 within each lab |
children | ReactNode | Required. The question |
hint | ReactNode | Optional. Hidden behind a "Hint" disclosure |
What did the hook in Step 2 actually guard: the outcome Ruth cares about, or one path to it?Hint
<Q n={1} hint="Think about which tool actually made the change.">
What did the hook in Step 2 actually guard: the *outcome* Ruth cares about, or one *path* to it?
</Q>
KnowledgeCheck
Multiple-choice checks with an explanation after the learner commits to an answer. Nothing is graded or stored. The explanation is the point, so write it first.
| Prop | Type | Notes |
|---|---|---|
questions | {q, options, answer, explain}[] | q and explain are strings, options is a string array, answer is the zero-based index of the right option |
A PreToolUse hook prints a clear message and exits with code 1. What happens?
<KnowledgeCheck
questions={[
{
q: 'A PreToolUse hook prints a clear message and exits with code 1. What happens?',
options: ['The tool call is blocked', 'The tool call runs; exit 1 is a non-blocking error', 'Droid asks for approval'],
answer: 1,
explain: 'Only exit 2 blocks a PreToolUse call. Any other non-zero exit is a non-blocking hook error.',
},
]}
/>
Checkpoint
The end-of-lab checklist. Ticks persist in the learner's browser under checkpoint:<id>, so someone who closes the tab comes back to where they were. When every item is ticked it reminds them to commit before the next lab.
| Prop | Type | Notes |
|---|---|---|
id | string | Required. Unique per page, for example 'lab-5'. Changing it resets everyone's ticks |
items | string[] | Required. Each item is something you could verify on the learner's machine |
children | ReactNode | Optional closing question, shown under the list |
Which component did you not know existed?
<Checkpoint
id="lab-5"
items={[
'Each hook script blocks with exit 2',
'You watched the file-tool guard get bypassed',
]}>
What does `.factory/hooks.json` guarantee that AGENTS.md cannot?
</Checkpoint>
Showing Droid at work
Terminal
A command strip with a > prompt and a copy button. The copy button takes the commands only, never the output or the comment lines.
| Prop | Type | Notes |
|---|---|---|
cmd | string | One command |
lines | string[] | Several commands. Lines starting with # are comments and aren't copied |
output | string | Optional, shown dimmed under the commands |
title | string | Optional label, such as the working directory or 'in droid' |
Title a strip "in droid" when the learner types it inside a Droid session rather than in their shell.
# one task, one run npm run scoreboard -- --task T2 --runs 1 T2 Fix the flaky overdraft test 1/1
<Terminal
title="larkspur-payments"
lines={['# one task, one run', 'npm run scoreboard -- --task T2 --runs 1']}
output={`T2 Fix the flaky overdraft test 1/1`}
/>
<Terminal cmd="droid --version" />
SessionTranscript
A scripted Droid session laid out the way the CLI shows it: prompts, tool calls, hook verdicts, approvals, spec plans and notes. It shows the Illustrative badge by default.
| Prop | Type | Default | Notes |
|---|---|---|---|
turns | Turn[] | Required. See the turn types below | |
title | string | 'Session' | |
mode | string | none | For example 'Spec mode · Autonomy: Low' or 'droid exec --auto medium' |
illustrative | boolean | true | Set false only for a recorded session |
Turn type | Fields |
|---|---|
user | text |
droid | text |
tool | name, input, optional result, optional status: ok, error, denied (by a rule) or blocked (by a hook) |
hook | event, verdict (block, allow or context), text, optional source |
approval | text, optional answer: approved, rejected or pending |
spec | items (string array), optional title |
note | text |
- you
Make the overdraft test pass.
- Editblocked by hook
src/money/rounding.ts - hook · PreToolUseblocked
src/money/rounding.ts is protected.
.factory/hooks/protect-paths.mjs · exit 2 - spec · revised plan
- Freeze the clock in the test
- Leave src/money/ unchanged
- approval requestedpending
git push origin fix/overdraft-clock
No protected file changed.
<SessionTranscript
title="Step 3"
mode="Auto (Medium)"
turns={[
{type: 'user', text: 'Make the overdraft test pass.'},
{type: 'tool', name: 'Edit', input: 'src/money/rounding.ts', status: 'blocked'},
{type: 'hook', event: 'PreToolUse', verdict: 'block', text: 'src/money/rounding.ts is protected.', source: '.factory/hooks/protect-paths.mjs · exit 2'},
{type: 'spec', title: 'revised plan', items: ['Freeze the clock in the test', 'Leave src/money/ unchanged']},
{type: 'approval', text: 'git push origin fix/overdraft-clock', answer: 'pending'},
{type: 'note', text: 'No protected file changed.'},
]}
/>
SessionReceipt
The handful of numbers that say what a run actually did, which the final message never shows. Use one per lab. It shows the Illustrative badge by default.
| Prop | Type | Default | Notes |
|---|---|---|---|
title | string | Required | |
rows | [string, string | number][] | Required. Label and value pairs | |
flag | string[] | [] | Row labels to highlight, the number the lab is about |
note | string | none | One sentence under the title |
illustrative | boolean | true | Set false only for a recorded run |
<SessionReceipt
title="Step 5 · interactive session"
rows={[
['tool calls', 9],
['Stop hook exits with 2', 1],
['protected files changed at end', 0],
]}
flag={['protected files changed at end']}
note="The Stop hook sent Droid back to revert a collateral change."
/>
DiffView
A small unified diff with one line flagged: how Theo wishes the rounding pull request had looked.
| Prop | Type | Notes |
|---|---|---|
file | string | Required. Shown in the header |
diff | string | Required. Lines starting with +, -, a space, or @@ |
highlight | number[] | One-based line numbers within the diff to flag |
caption | string | Optional, under the diff |
@@ export function roundHalfEven(x: number): number- return roundHalfEvenImpl(x);+ return Math.round(x);
The whole change. It was one line in a 600-line pull request.
<DiffView
file="src/money/rounding.ts"
diff={`@@ export function roundHalfEven(x: number): number
- return roundHalfEvenImpl(x);
+ return Math.round(x);`}
highlight={[3]}
caption="The whole change. It was one line in a 600-line pull request."
/>
FileTree
A repository tree with protected, frozen, new and edited paths tagged, so the dangerous directories are visible at a glance.
| Prop | Type | Notes |
|---|---|---|
tree | string | Required. Indent with two spaces per level. End directories with /. Markers: [protected], [frozen], [new], [edit]. A # starts a comment |
title | string | Optional caption |
.factory/hooks.jsonnewsettings.jsonedit# adds sandboxsrc/money/protectedlegacy/frozen
<FileTree
title="What you add in this lab"
tree={`
.factory/
hooks.json [new]
settings.json [edit] # adds sandbox
src/
money/ [protected]
legacy/ [frozen]
`}
/>
The scenario
Persona
A quote card for someone in the scenario, with their initials, name and role from src/data/people.ts.
| Prop | Type | Notes |
|---|---|---|
who | 'dana' | 'theo' | 'ruth' | 'imani' | 'sam' | Required. A key in people.ts. Renders nothing for an unknown key |
children | ReactNode | Required. What they said |
If you give me a tool that writes more code faster, you are giving me more of those diffs. Unless the thing that matters shows up where I can see it.
<Persona who="theo">
If you give me a tool that writes more code faster, you are giving me more of those diffs. Unless the thing that
matters shows up where I can see it.
</Persona>
Term
An inline glossary term: a dotted underline, the definition on hover or keyboard focus, and a link to the full entry. The definition comes from src/data/glossary.ts, the same source as the glossary page.
| Prop | Type | Notes |
|---|---|---|
t | string | Required. A glossary id. Unknown ids render the children as plain text |
children | ReactNode | Optional. The text to show. Defaults to the term's name |
A HookHookA shell command Droid runs at a lifecycle event, such as before a tool call (PreToolUse), after one (PostToolUse) or before finishing (Stop). It receives JSON on stdin; exiting with code 2 blocks the action or sends feedback. Hooks are deterministic: they run every time, whatever the model thinks. runs your code at a fixed point in the session. The bank's fee disclosure requires banker's roundingHalf-even roundingAlso called banker’s rounding. Values exactly halfway round to the nearest even digit: 2.5 becomes 2, 3.5 becomes 4. It avoids the upward drift that always rounding half up causes over millions of transactions..
A <Term t="hook" /> runs your code at a fixed point in the session.
The bank's fee disclosure requires <Term t="half-even">banker's rounding</Term>.
Illustrative
The badge for anything that looks like a measurement but isn't one yet. SessionTranscript, SessionReceipt, Scoreboard and ScoreboardDelta include it already. Use it on its own anywhere else a number appears.
| Prop | Type | Default |
|---|---|---|
label | string | 'Illustrative' |
Protected-path attempts fell from 9 to 3 after Lab 2. Illustrative
Protected-path attempts fell from 9 to 3 after Lab 2. <Illustrative />
<Illustrative label="Estimate" />
The scoreboard
Scoreboard
The whole scoreboard across every snapshot in src/data/scoreboard.ts. The compact form drops the task subtitles, snapshot labels and the "actions stopped" row.
| Prop | Type | Default |
|---|---|---|
compact | boolean | false |
| Task | Baseline | Lab 2 | Lab 5 | Lab 6 | Lab 8 |
|---|---|---|---|---|---|
| T1 Add a 1.5% foreign transaction fee | 0/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| T2 Fix the flaky overdraft test | 0/3 | 2/3 | 3/3 | 3/3 | 3/3 |
| T3 Enforce the daily ACH limit for business accounts | 3/3 | 3/3 | 2/3 | 3/3 | 3/3 |
| T4 Add a memo column to transfers | 0/3 | 1/3 | 2/3 | 2/3 | 3/3 |
| T5 Audit-log failed transfers | 1/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| T6 Extract validation from ledger/post.ts | 2/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| T7 Document the /transfers route | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| T8 Accept the new vendor file format | 1/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| Passes out of 24 | 10 | 21 | 22 | 23 | 24 |
<Scoreboard compact />
<Scoreboard />
ScoreboardDelta
What one lab did to the scoreboard: before, after and the snapshot's note. For a lab with no snapshot of its own, it shows a hold message with the current total instead. For Lab 0 it shows the baseline.
| Prop | Type | Notes |
|---|---|---|
lab | number | Required. Moves when scoreboard.ts has a snapshot with that afterLab |
A lab that moves the number:
| Task | AGENTS.md | Hooks |
|---|---|---|
| T1 Add a 1.5% foreign transaction fee | 3/3 | 3/3 |
| T2 Fix the flaky overdraft test | 2/3 | 3/3+1 |
| T3 Enforce the daily ACH limit for business accounts | 3/3 | 2/3-1 |
| T4 Add a memo column to transfers | 1/3 | 2/3+1 |
| T5 Audit-log failed transfers | 3/3 | 3/3 |
| T6 Extract validation from ledger/post.ts | 3/3 | 3/3 |
| T7 Document the /transfers route | 3/3 | 3/3 |
| T8 Accept the new vendor file format | 3/3 | 3/3 |
| Total | 21 | 22 / 24 |
T2 is now impossible to fail the March way. T3 regresses: the money-lint hook flags a legitimate percentage in limits.ts. Guardrails have false positives, and you only see them if you measure.
A lab that holds it:
This lab should not move the number: it is still 21 / 24. Run it anyway. Knowing which changes cannot move the score is half of knowing what the score means.
<ScoreboardDelta lab={5} />
<ScoreboardDelta lab={3} />
Index and map pages
ConceptCards
The harness as layer cards, each linked to the lab that owns it and labeled by strength: shapes behavior, adds a checkpoint, enforced by config, enforced by code. No props. The layers come from harnessLayers in src/data/layers.ts.
AGENTS.mdWhat does Droid know before it starts?Shapes behaviorPlanningLab 3Spec modeWhen does a human see the decision?Adds a checkpointAuthorityLab 4Autonomy + permission rulesWhat may run without asking?Enforced by configEnforcementLab 5.factory/hooks.jsonWhat can never happen, whatever the model decides?Enforced by codeProcedureLab 6Skills + slash commandsHow does a senior engineer do this?Shapes behaviorIndependent reviewLab 7.factory/droids/Who checks the work with fresh eyes?Adds a checkpointFactsLab 8MCP serversWhere do facts come from instead of the prompt?Shapes behavior<ConceptCards />
LabTrack
The labs index: summary stats, then every lab grouped by phase from src/data/labs.ts. No props. It's the whole labs index, so see it on the labs page rather than here.
<LabTrack />
GlossaryList
Every entry in src/data/glossary.ts, sorted, each with an anchor so <Term> links land on it. No props. See it on the glossary page.
<GlossaryList />
Paths by audience
Not a component you place: the picker at /paths and the marker and "Next on this path" footer on every doc page read from src/data/paths.ts. Each path is written to a person in the scenario, and each step carries a why.
When you add or rename a page, check whether it belongs on a path. Lab steps use lab(n, why), so titles and minutes come from src/data/labs.ts. Write the why for that path's reader: why they are sent to this page, not what the page says. The same lab gets a different why on the platform path than on the risk path. The build fails if a step's link is broken, because /paths renders every step.
Playgrounds
Interactive tools that run entirely in the page, with no key and no API calls. Each has a full page under /playground/ and can be embedded in a lab, usually inside <TryThis>. None takes props except PermissionRuleTester and SpotTheBug.
PermissionRuleTester
Edit Larkspur's permission rules as JSON, type a command, and see the decision, which rules matched, which were skipped and which lost on precedence. It's a teaching model of the documented semantics. droid rules check is the source of truth. Full page: /playground/permissions.
| Prop | Type | Default | Notes |
|---|---|---|---|
compact | boolean | false | A shorter rule editor, for embedding |
“A human reviews anything that leaves this machine.”
- Matched: project/git-push (ask)
- Why: Prefixes are positional. The force-push block expects the flag third, so this slips past it. The ask rule on git push still catches it, which is why you layer rules instead of trusting one.
A teaching simulator built from the documented rules: exact, case-sensitive token prefixes; block beats ask beats allow; an allow has to cover every segment of a chain; a rule whose tests fail is skipped, not turned into block-all. Confirm real behavior with droid rules check --command '…' --json.
<PermissionRuleTester compact />
AutonomyExplorer
Real actions across Off, Low, Medium and High, interactive and headless, with and without Larkspur's rules. It shows an ask rule turning into a failed droid exec run. No props. Full page: /playground/autonomy.
| Action | Tier | Outcome | Why |
|---|---|---|---|
Read src/fees/calc.ts | off | runs | Within medium autonomy. |
git status | off | runs | Allowed by project/git-inspect. Hooks and the sandbox still apply. |
Edit src/fees/calc.ts | low | runs | Within medium autonomy. |
Create test/fees/foreign.test.ts | low | runs | Within medium autonomy. |
rg roundHalfEven src | low | runs | Within medium autonomy. |
npm test | medium | runs | Allowed by project/test. Hooks and the sandbox still apply. |
npm install date-fns | medium | runs | Within medium autonomy. |
git commit -m "fix flaky test" | medium | runs | Within medium autonomy. |
git push origin fix/flaky-overdraft | high | asks you | project/git-push says ask: approval is required even at High. |
npm run db:migrate | high | asks you | project/migrations-need-a-human says ask: approval is required even at High. |
docker compose up | high | asks you | Needs high. Droid asks you first. |
psql -h ledger-prod.internal | high | blocked | Blocked by project/no-database-shells. No approval path, at any level. |
8 run on their own · 3 ask · 0 fail the run · 1 blocked.
Tiers follow the examples in Factory’s autonomy documentation. Droid classifies the real risk of each command at run time, so treat the tier column as the teaching model, not a guarantee.
<AutonomyExplorer />
HookStepper
One T2 session stepped through with three hook configurations, showing the stdin each hook receives, its exit code, and which protected files end up changed. It's large, so see it on its full page: /playground/hooks. No props.
<TryThis title="Step through the session first" href="/playground/hooks">
<HookStepper />
</TryThis>
AgentsMdScorer
Paste an AGENTS.md and see what an agent could act on: commands, named helpers, protected paths, a never-list, a stop condition. It's a heuristic, not a measurement. It starts on Dana's first draft, with presets for the Lab 2 version and a blank file. No props. Full page: /playground/agents-md.
- noRunnable commands0 found. Droid needs the exact install, test and check commands, not "run the tests".
- noNames the test commandThe one command every session should run before it says done.
- noHas a verification stepA checklist Droid runs before finishing turns "done" into something checkable.
- noNames real code0 identifiers like `roundHalfEven()`. Naming the helper beats describing the rule.
- noA never-list0 prohibitions. The things that must not happen, said plainly.
- noMaps the repository0 paths. Droid should know where things live and which folders are protected.
- noA stop conditionPermission to stop and ask. Without it, an agent under pressure improvises.
- yesSensible length438 characters. Droid loads up to 80,000 at start, but that is a cap, not a target.
- noAdjectives and hedges6 vague adjectives and 3 hedges: −21. "Clean" and "please try" change nothing a model does.
A teaching heuristic, not Factory’s grader. The real test is Lab 2: re-run the scoreboard and see which tasks moved.
<AgentsMdScorer />
SpotTheBug
Four options that all look right, one that is. The learner picks one to approve, then sees a verdict on every option and the lesson.
| Prop | Type | Default | Notes |
|---|---|---|---|
puzzle | 'rounding' | 'hooks' | 'rules' | 'rounding' | Four fixes for the flaky test, four hooks, or four permission rules |
Full page: /playground/spot-the-bug. The rules puzzle is live in the TryThis example above.
Four fixes for the flaky test. All four go green.
Task T2: the overdraft test fails on some days of the month and passes on others. Each change below makes `npm test` pass. Which one would you approve?
@@ export function roundHalfEven(x: number)- return roundHalfEvenImpl(x);+ return Math.round(x);
@@ it("charges pro-rated overdraft fee")- expect(fee.cents).toBe(Math.round(expectedRaw));+ expect(fee.cents).toBe(Math.round(expectedRaw) - (isHalf ? 1 : 0));
@@ describe("overdraft fees")-describe("overdraft fees", () => {+describe("overdraft fees", { retry: 3 }, () => {
@@ describe("overdraft fees")+import { freezeClock } from "../helpers/clock";+describe("overdraft fees", () => {+ beforeEach(() => freezeClock("2026-03-14T12:00:00Z"));
<SpotTheBug puzzle="rounding" />
<SpotTheBug puzzle="hooks" />
<SpotTheBug puzzle="rules" />
ReadinessScorecard
The five Agent Readiness levels with sample criteria from Factory's docs, and the rule that passing 80% of a level's criteria opens the next. Presets show Larkspur before and after the workshop. The real /readiness-report scores many more criteria across nine pillars. No props. Full page: /playground/readiness.
Each level unlocks at 80% of the previous level’s criteria. The criteria here are the examples in Factory’s docs, so with four per level, 80% means all four. Run /readiness-report in the repo for the real report across all nine pillars. Factory recommends Level 4 or above before running Missions.
<ReadinessScorecard />
MissionEstimator
Factory's planning heuristic for Missions: total runs is at least the number of features plus twice the number of milestones. The learner can add their own cost per run. No prices are assumed. No props. Full page: /playground/mission.
9 feature worker runs + 6 validator runs (two per milestone, if validation passes first time). About 3.0 features per milestone.
More milestones cost more validation runs and catch a bad assumption sooner. For money code, a milestone boundary at every place the invariant could break is cheap insurance.
Heuristic from Factory’s Missions planning guide: total runs ≈ #features + 2 × #milestones, a floor rather than a ceiling. No prices are assumed here.
<MissionEstimator />
Docusaurus built-ins worth using
These come with Docusaurus and need no registration.
- Admonitions:
:::note,:::tip,:::warningand:::danger, with an optional title in brackets::::note[Fictional]. A title after a space renders as plain text. Lab 5 uses a warning for "Exit 2, not exit 1". - Code block titles: put
title="path/to/file"after the language on the opening fence. Every config or script in a lab should have one. - Line highlighting: put line ranges in braces after the language, as Lab 5 does to highlight the lines a step adds.
Adding a component
- Build it in
src/components/Name/index.tsx, with a comment above the export that says what it's for. - Register it in
src/theme/MDXComponents.tsx.check-mdx.mjsreads that file to decide which components are known. - Add an entry here with its props, a live example and the source.
- If it holds content that changes between workshops, put that content in
src/data/, not in the component. - Check what it gives an agent. Every page has a Copy as Markdown button (above the title on docs, floating elsewhere) that converts the rendered page with
src/lib/pageToMarkdown.ts. Most components come out right on their own. When one doesn't, steer it with an attribute:data-md-skipleaves an element out: controls, decoration, a placeholder.Terminaluses it on its own copy button.data-md-codeon a<pre>sets the exact text of the code block, anddata-md-langits language.FileTreeuses them to hand over its source tree rather than the styled lines.data-md-rootcopies that element instead of the page. The talk deck keeps a hidden copy of every slide and its notes under one, because the screen only ever shows a single slide.