Skip to main content

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.

PropTypeDefaultNotes
youtubeIdstringnoneLeave out until the recording exists
titlestring'Watch this lab'Shown on the poster
durationstringnoneBadge on the poster, for example '12:40'
chapters{t: string; label: string}[]noneListed under the player
Watch this labNarrated walkthrough, recorded after the pilot runRecording coming soon
<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.

PropTypeNotes
labnumberThe lab number in labs.ts. Renders nothing if it doesn't exist
Time 45 minBuilds on Lab 2, Lab 4Centers on PreToolUse

A hook runs your code on every tool call it matches. It is the difference between Droid usually respecting rounding and Droid being unable to change it through the tools the hook guards.

<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.

PropTypeNotes
titlestringRequired. Short and imperative
leadReactNodeOne sentence on what to do and what to notice
hrefstringThe full playground page. Adds an "Open the full playground" link
childrenReactNodeRequired. 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`?

Option A
rule
{ "id": "project/no-force",
  "decision": "deny",
  "match": { "prefix": ["git", "push", "--force"] },
  "tests": { "match": ["git push --force"], "noMatch": ["git push"] } }
Option B
rule
{ "id": "project/no-force",
  "decision": "block",
  "match": { "prefix": ["git", "push", "*"] },
  "tests": { "match": ["git push --force"], "noMatch": ["git status"] } }
Option C
rule
{ "id": "project/no-force",
  "decision": "block",
  "match": { "prefix": ["git", "push", ["--force", "-f"]] } }
Option D
rule
{ "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"] } }

Open the full playground →

<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.

PropTypeNotes
nnumberRequired. Numbered from 1 within each lab
childrenReactNodeRequired. The question
hintReactNodeOptional. Hidden behind a "Hint" disclosure
Q1

What did the hook in Step 2 actually guard: the outcome Ruth cares about, or one path to it?

Hint
Think about which tool actually made the change.
<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.

PropTypeNotes
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
Check 1 of 1

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.

PropTypeNotes
idstringRequired. Unique per page, for example 'lab-5'. Changing it resets everyone's ticks
itemsstring[]Required. Each item is something you could verify on the learner's machine
childrenReactNodeOptional closing question, shown under the list
Checkpoint0 / 3

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.

PropTypeNotes
cmdstringOne command
linesstring[]Several commands. Lines starting with # are comments and aren't copied
outputstringOptional, shown dimmed under the commands
titlestringOptional 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.

larkspur-payments
# 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.

PropTypeDefaultNotes
turnsTurn[]Required. See the turn types below
titlestring'Session'
modestringnoneFor example 'Spec mode · Autonomy: Low' or 'droid exec --auto medium'
illustrativebooleantrueSet false only for a recorded session
Turn typeFields
usertext
droidtext
toolname, input, optional result, optional status: ok, error, denied (by a rule) or blocked (by a hook)
hookevent, verdict (block, allow or context), text, optional source
approvaltext, optional answer: approved, rejected or pending
specitems (string array), optional title
notetext
Step 3Auto (Medium)Illustrative
  1. you

    Make the overdraft test pass.

  2. Editblocked by hooksrc/money/rounding.ts
  3. hook · PreToolUseblocked

    src/money/rounding.ts is protected.

    .factory/hooks/protect-paths.mjs · exit 2
  4. spec · revised plan
    1. Freeze the clock in the test
    2. Leave src/money/ unchanged
  5. approval requestedpending

    git push origin fix/overdraft-clock

  6. 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.

PropTypeDefaultNotes
titlestringRequired
rows[string, string | number][]Required. Label and value pairs
flagstring[][]Row labels to highlight, the number the lab is about
notestringnoneOne sentence under the title
illustrativebooleantrueSet 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.

PropTypeNotes
filestringRequired. Shown in the header
diffstringRequired. Lines starting with +, -, a space, or @@
highlightnumber[]One-based line numbers within the diff to flag
captionstringOptional, under the diff
src/money/rounding.ts+1 −1
@@ 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.

PropTypeNotes
treestringRequired. Indent with two spaces per level. End directories with /. Markers: [protected], [frozen], [new], [edit]. A # starts a comment
titlestringOptional caption
What you add in this lab
.factory/
hooks.jsonnew
settings.jsonedit# adds sandbox
src/
money/protected
legacy/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.

PropTypeNotes
who'dana' | 'theo' | 'ruth' | 'imani' | 'sam'Required. A key in people.ts. Renders nothing for an unknown key
childrenReactNodeRequired. 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.

Theo Lindqvist Senior payments engineer · eleven years at Larkspur
<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.

PropTypeNotes
tstringRequired. A glossary id. Unknown ids render the children as plain text
childrenReactNodeOptional. 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.

PropTypeDefault
labelstring'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.

PropTypeDefault
compactbooleanfalse
The Larkspur scoreboardIllustrative
TaskBaselineLab 2Lab 5Lab 6Lab 8
T1 Add a 1.5% foreign transaction fee0/33/33/33/33/3
T2 Fix the flaky overdraft test0/32/33/33/33/3
T3 Enforce the daily ACH limit for business accounts3/33/32/33/33/3
T4 Add a memo column to transfers0/31/32/32/33/3
T5 Audit-log failed transfers1/33/33/33/33/3
T6 Extract validation from ledger/post.ts2/33/33/33/33/3
T7 Document the /transfers route3/33/33/33/33/3
T8 Accept the new vendor file format1/33/33/33/33/3
Passes out of 241021222324
<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.

PropTypeNotes
labnumberRequired. Moves when scoreboard.ts has a snapshot with that afterLab

A lab that moves the number:

Scoreboard after this labIllustrative
TaskAGENTS.mdHooks
T1 Add a 1.5% foreign transaction fee3/33/3
T2 Fix the flaky overdraft test2/33/3+1
T3 Enforce the daily ACH limit for business accounts3/32/3-1
T4 Add a memo column to transfers1/32/3+1
T5 Audit-log failed transfers3/33/3
T6 Extract validation from ledger/post.ts3/33/3
T7 Document the /transfers route3/33/3
T8 Accept the new vendor file format3/33/3
Total2122 / 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:

Scoreboard

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.

<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.

PropTypeDefaultNotes
compactbooleanfalseA shorter rule editor, for embedding
Decisionask

“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.

Surface
Autonomy (Ctrl+L)
ActionTierOutcomeWhy
Read src/fees/calc.tsoffrunsWithin medium autonomy.
git statusoffrunsAllowed by project/git-inspect. Hooks and the sandbox still apply.
Edit src/fees/calc.tslowrunsWithin medium autonomy.
Create test/fees/foreign.test.tslowrunsWithin medium autonomy.
rg roundHalfEven srclowrunsWithin medium autonomy.
npm testmediumrunsAllowed by project/test. Hooks and the sandbox still apply.
npm install date-fnsmediumrunsWithin medium autonomy.
git commit -m "fix flaky test"mediumrunsWithin medium autonomy.
git push origin fix/flaky-overdrafthighasks youproject/git-push says ask: approval is required even at High.
npm run db:migratehighasks youproject/migrations-need-a-human says ask: approval is required even at High.
docker compose uphighasks youNeeds high. Droid asks you first.
psql -h ledger-prod.internalhighblockedBlocked 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.

Heuristic score
0/ 100
  • noRunnable commands
    0 found. Droid needs the exact install, test and check commands, not "run the tests".
  • noNames the test command
    The one command every session should run before it says done.
  • noHas a verification step
    A checklist Droid runs before finishing turns "done" into something checkable.
  • noNames real code
    0 identifiers like `roundHalfEven()`. Naming the helper beats describing the rule.
  • noA never-list
    0 prohibitions. The things that must not happen, said plainly.
  • noMaps the repository
    0 paths. Droid should know where things live and which folders are protected.
  • noA stop condition
    Permission to stop and ask. Without it, an agent under pressure improvises.
  • yesSensible length
    438 characters. Droid loads up to 80,000 at start, but that is a cap, not a target.
  • noAdjectives and hedges
    6 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.

PropTypeDefaultNotes
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?

Option A
src/money/rounding.ts+1 −1
@@ export function roundHalfEven(x: number)
- return roundHalfEvenImpl(x);
+ return Math.round(x);
Option B
test/fees/overdraft.test.ts+1 −1
@@ it("charges pro-rated overdraft fee")
- expect(fee.cents).toBe(Math.round(expectedRaw));
+ expect(fee.cents).toBe(Math.round(expectedRaw) - (isHalf ? 1 : 0));
Option C
test/fees/overdraft.test.ts+1 −1
@@ describe("overdraft fees")
-describe("overdraft fees", () => {
+describe("overdraft fees", { retry: 3 }, () => {
Option D
test/fees/overdraft.test.ts+3 −0
@@ 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.

Level2Documented
Level 1Functional4/4 · 100% · unlocks next
Level 2Documented2/4 · 50% · needs 80%
Level 3Standardized3/4 · 75% · needs 80%
Level 4Optimized0/3 · 0% · needs 80%
Level 5Autonomous0/1 · 0%

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.

Runs, at least15

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, :::warning and :::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​

  1. Build it in src/components/Name/index.tsx, with a comment above the export that says what it's for.
  2. Register it in src/theme/MDXComponents.tsx. check-mdx.mjs reads that file to decide which components are known.
  3. Add an entry here with its props, a live example and the source.
  4. If it holds content that changes between workshops, put that content in src/data/, not in the component.
  5. 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-skip leaves an element out: controls, decoration, a placeholder. Terminal uses it on its own copy button.
    • data-md-code on a <pre> sets the exact text of the code block, and data-md-lang its language. FileTree uses them to hand over its source tree rather than the styled lines.
    • data-md-root copies 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.