Chapter 1 — A Line Cook with a Work Permit
# Chapter 1 — A Line Cook with a Work Permit
You want help in your kitchen. Two candidates walk in.
The first one is brilliant, tireless, and wants root on your laptop. You hand over the keys, turn your back, and hope. When the sauce catches fire, you will not know who turned up the flame, when, or why.
The second one is equally brilliant, but arrives with a work permit. The permit says which knives they may touch, which burners they may light, and that every order they fill gets written down and signed. You watch the permit, not the cook. When something goes wrong, the paperwork tells you what happened — and you can revoke the permit at any time.
Relurpify is the second candidate. This book teaches you to be the kind of kitchen owner who hires them.
# Where you are
Nothing to recall — this is the first page. You start with a terminal, a machine, and about twenty minutes. By the end of the chapter you will have a small, deliberately broken Python project, a materialized agent workspace, and a doctor’s report you can read line by line.
# Technical requirements
| Requirement | Version / notes |
|---|---|
| OS | Linux x86-64 or arm64 (this book verified on Linux; macOS readers: see the box in Gotchas) |
| Go | 1.26 or newer to build — the repository pins toolchain 1.27.2 |
| Git | Any recent version |
| Python | 3.10+ with pytest 9.x, for the demo project |
| Ollama | 0.40.x, serving gemma4:e4b |
| Sandbox | Docker, plus gVisor runsc on Linux for the default backend |
| Disk | ~3 GB free, plus room for the model (gemma4:e4b is a few GB) |
You need no GPU — gemma4:e4b is a small model, and a CPU is fine for this
book’s tasks. Expect slower turns on an old laptop; the runtime will tell
you what it is doing the whole time.
Note The
offlinebackend — a deterministic scripted model that needs no setup — exists for CI and plumbing checks. This book runs against a real local model, because that is the job you are training for. We touch the offline backend once, in Chapter 9, where its determinism becomes a feature.
Everything in this chapter was captured on a real machine: Linux 6.18, Go
1.27.3, Ollama 0.40.8 with gemma4:e4b served, Docker with runsc
installed, Python 3.14.8, pytest 9.1.1. Where your output will differ, the
text says so.
# What you are hiring, exactly
Four names, one kitchen. Keep them straight and the rest of the book reads easily:
- Relurpify is the framework: the governed agent runtime this whole repository builds. It is the permit system — policy, approvals, sandbox, audit, receipts.
- Euclo is the built-in coding agent, a worker who runs on that framework. When you type a task, Euclo is who thinks about it.
- relurpish is the program you will actually run: a terminal UI
(the “TUI” — a full-screen program inside your terminal). The same binary
also carries
doctorandstatus, two non-interactive commands you will use constantly. - relurplint is the workspace linter, and relurpify-runner is a background helper the app spawns when it needs one. They wait in the wings for now; Chapters 3 and 4 give them their entrances.
And one word you will see everywhere: workspace — the project directory
the agent works in. The agent acts only inside it, and every command it
runs executes inside a sandbox: container-isolated command execution,
through gVisor (runsc) or Docker. The sandbox is a locked door between
Euclo and the rest of your machine. We will lean on that door all book.
# The two-directory rule
After this chapter, your project will contain two new directories, and the difference between them is the most important rule in the book:
| Directory | What it is | Who writes it |
|---|---|---|
relurpify_cfg/ |
The agent’s configuration: which model, which tools, which recipes, which policies | The doctor command creates it once, on first run. After that, you own it. Your edits win. |
.relurpify_state/ |
Everything the runtime produces: logs, telemetry, the audit trail, memory, session records | The runtime. Always output, never source. You read it; you never feed it. |
A kitchen analogy, and then we drop it for a while: relurpify_cfg/ is the
rulebook on the wall — the recipes, the standards, the allergy list.
.relurpify_state/ is the tape of last night’s service. You rewrite the
rulebook whenever you like. You do not cook from the tape.
Why the hard split? Because a system you are learning to trust owes you a clean answer to one question: what did you do, and why? If configuration and output mix, that question loses its answer. Governed behavior starts with this directory line.
# Hands-on
# 1. Get the code and build the binaries
Clone the repository and check out the edition this book is pinned to:
git clone https://github.com/lexbity/relurpify.git
cd relurpify
git checkout BETA-RC1-FALL2026Build the two binaries this book uses (run from the repository root):
go build -o relurpish ./app/relurpish
go build -o relurplint ./app/relurplintThe build is offline-friendly. First build takes a minute or two.
Check what you built:
./relurpish --versionrelurpish dev (commit none, built unknown)A source build reports dev for the version and none/unknown for the
commit and build date. Release binaries report real values, like
relurpish 0.1.0 (commit 594da458..., built 2026-10-08T...) — the
difference is cosmetic, and it tells you which kind of binary you are
holding.
Tip If you work in a sandboxed or disk-quotad machine, the repository’s own contributing guide suggests keeping the Go caches inside the checkout:
export GOMODCACHE=$PWD/.gomodcache GOCACHE=$PWD/.gocache.
# 2. Seed the stringkit project
Every chapter from here works on stringkit, a tiny Python string library that starts life with one bug on purpose. The bug is the book’s pedal: the agent will spend Chapter 2 fixing it while you watch every step.
Create the project, initialize git, and add two files:
mkdir -p ~/code/stringkit && cd ~/code/stringkit
git initstringkit.py:
"""stringkit — small string utilities, kept tiny on purpose."""
import re
def slugify(text: str) -> str:
"""Turn a title into a URL-safe slug."""
slug = text.strip().lower()
slug = re.sub(r"\s+", "-", slug)
return slugtest_stringkit.py:
"""Tests for stringkit."""
from stringkit import slugify
def test_slugify_strips_punctuation():
assert slugify("Hello, World!") == "hello-world"
def test_slugify_collapses_whitespace():
assert slugify(" many spaces ") == "many-spaces"Read slugify the way a reviewer would. It lowercases, it turns runs of
whitespace into single hyphens — and it never touches punctuation. The
first test describes behavior the code does not have. Commit the broken
state (a clean git baseline makes the agent’s changes visible as a diff,
which you will want in Chapter 2):
git add -A
git commit -m "seed stringkit"
python3 -m pytest test_stringkit.pycollected 2 items
test_stringkit.py .F [100%]
=================================== FAILURES ===================================
_______________________ test_slugify_strips_punctuation ________________________
def test_slugify_strips_punctuation():
> assert slugify("Hello, World!") == "hello-world"
E AssertionError: assert 'hello,-world!' == 'hello-world'
E
E - hello-world
E + hello,-world!
E ? + +
test_stringkit.py:11: AssertionError
=========================== short test summary info ============================
FAILED test_stringkit.py::test_slugify_strips_punctuation - AssertionError: a...
========================= 1 failed, 1 passed in 0.03s ==========================One red, one green, and pytest’s assert diff tells the whole story:
hello,-world! — the comma and the bang rode along. That red test is
stringkit’s state at the end of this chapter. Leave it broken. Fixing it is
the agent’s job, not yours.
Note The exact pytest banner above your failure list varies with your pytest version and installed plugins. What matters is the bottom:
1 failed, 1 passed, and the failure namedtest_slugify_strips_punctuation.
# 3. Run the doctor
doctor is relurpish’s readiness check. It inspects your machine and workspace, tells you what is missing, and — on a workspace that has none — creates the starter configuration. Run it from inside stringkit, pointing at the binary you built:
~/relurpify/relurpish doctor --workspace .Substitute your own path to the repository. Here is the captured output on the verification machine, with the log lines at the top trimmed and one long section elided where marked:
Workspace: ~/code/stringkit
Config root: ~/code/stringkit/relurpify_cfg
Workspace present: yes
Config file: yes
Manifest fingerprint: e3985117c09367091d0a4cc6f7a5b28f11519a3a9f411114410f75552a30badc
Inference backend:
provider: ollama
endpoint: http://localhost:11434
state: ready
tool_calling_mode: fallback
models: parable/qwen3-fable:8b, rafw007/qwen35-claude-coder:9b, gemma4:12b, gemma4:e4b
selected: parable/qwen3-fable:8b
error: -
profile: gemma4.llm.yaml
Providers:
lmstudio [unhealthy]
endpoint: http://localhost:1234
hint: Start LM Studio, enable the local HTTP server in Settings, then verify the port
* ollama [ready]
endpoint: http://localhost:11434
models: parable/qwen3-fable:8b, rafw007/qwen35-claude-coder:9b, gemma4:12b, gemma4:e4b
hint: Start it: `ollama serve` then `ollama pull gemma4:e4b`
... [offline and openai_compatible rows elided]
Dependencies:
- workspace_directory: ok [required] (workspace directory exists and is readable)
- disk_space: ok [warning] (sufficient disk space available (317725 MB))
- runner: missing [warning] (no runner status (not spawned yet))
- hitl_mode: ok [warning] (governed: human-approval requests block on a decision)
- selection_posture: ok [warning] (deterministic: ambiguous intents always ask a human via a clarification frame (workspace selection.posture: model_assisted to opt into model disambiguation))
- starter-templates: ok [required] (workspace template archive available)
- model-profile: ok [required] (gemma4.llm.yaml)
- sandbox_backend: ok [required] (runsc version release-20260406.0
spec: 1.1.0-rc.1)
- runsc: ok [warning] (runsc version release-20260406.0
spec: 1.1.0-rc.1)
- docker: degraded [warning] (degraded: runsc runtime not registered)
- audit_chain: ok [required] (no audit chain yet (created on first agent run))
- runtime_image: degraded [required] (unpinned (tag-based; resolved from the local daemon at boot — set security.sandbox.image_digest to pin))
Result: readyResult: ready is the verdict, and this report deserves a slow read,
because it is your kitchen inventory:
- The config tree now exists.
Config root: .../relurpify_cfg— doctor materialized it on this first run, silently, from templates shipped inside the binary. That is materialization: the framework writes its starter configuration into your project once, and from that moment the copy in your workspace outranks the templates. Step 5 walks the tree. - The model is real.
provider: ollama ... state: ready, with every model your Ollama serves listed. The*marks the provider the doctor would use. - Two informational warnings are normal.
runner: missing(the background helper has not been spawned yet — it appears on first boot) andruntime_image: degraded(the sandbox’s container image is not pinned to an immutable digest — Chapter 3 shows how, and why you would). Both saywarning, neither saysblocking, and the verdict is still ready. - Each row names its severity class.
requiredrows must pass for a green verdict;warningrows inform. Learning to sort those visually is most of what “reading the doctor” means. - The two posture rows are the work permit’s headline.
hitl_mode: governed— approval requests block on a human, which is you.selection_posture: deterministic— when your instruction is ambiguous, a clarifying question comes to you, not to a model making a guess. Chapter 3 makes both dials adjustable; for now, read them as the default contract.
Warning Two lines in that transcript deserve a second look. First,
selected: parable/qwen3-fable:8b— that is the probe’s pick from the backend’s model list, not the model your agent turns will use. The workspace configuration (model.nameinrelurpify_cfg/workspace.yaml) governs turns; Chapter 3 untangles this properly. Second,docker: degraded (runsc runtime not registered)means your Docker daemon does not know about gVisor as a runtime. The defaultgvisorbackend works throughrunscdirectly (sandbox_backend: ok), so this degrades only the docker backend choice — you can registerrunscwith Docker later if you want that option.
# 4. Walk the tree the doctor left
find relurpify_cfg -maxdepth 1 | sort
ls relurpify_cfg/euclo/
ls -a .relurpify_state/relurpify_cfg
relurpify_cfg/euclo
relurpify_cfg/model
relurpify_cfg/prompts
relurpify_cfg/security
relurpify_cfg/tools
relurpify_cfg/workspace.yaml
code_review.erpe
debug_tdd_repair.erpe
default.erpe
dep_upgrade.erpe
explain.erpe
extract_func.erpe
families.yaml
investigation.erpe
plan.erpe
security_review.erpe
test_synthesis.erpe
audit artifacts ast jobs logs memory sessions telemetry test_runThree readings, three chapters:
euclo/holds thoughtrecipes (.erpefiles) — the workflows Euclo can run, plusfamilies.yaml, the routing table that maps kinds of intent to recipes. Chapter 4 teaches the language; these shipped files are your reference corpus.model/,security/,tools/,prompts/are exactly what their names say: the provider catalog and per-model behavior profiles, the security policies, the ~99 tool manifests, and the prompt corpus. Chapter 3 tours them..relurpify_state/already has subdirectories (audit,telemetry,logs, …), and nothing in it is yours to edit. In Chapter 2 you will read two entries from it — the audit chain and the telemetry line — and they will become your receipts.
Look inside the file that steers it all:
cat relurpify_cfg/workspace.yamlschema: relurpify/workspace/v1
model:
provider: ollama
name: gemma4:e4b
sandbox:
backend: gvisor
selection:
posture: deterministic(Trimmed to the lines that matter this chapter — the shipped file also
carries logging, audit-retention, telemetry, and runner settings, each with
a comment explaining itself.) Notice schema: on the first line: every
config file kind carries a schema tag, the versioning mechanism. And
notice what you did not have to do: pick defaults. The materialized tree
is a working kitchen the moment it lands.
# 5. Read what the doctor tells you to fix
On the verification machine, one more row appeared the moment a real model was reachable — this is the report earning its keep:
model_tool_template MISSING served template for gemma4:e4b has no tool support
(no .Tools block); profile intent is native, so tool decisions may arrive as
text instead of structured tool calls. Use a model artifact with a
tool-capable template, or set tool_calling.intent: prompt_based in the model
profilePlain-language translation: Euclo drives models through tool calls —
structured requests, not prose. This particular gemma4:e4b artifact’s
prompt template has no tool section, so with the profile’s native intent,
the model’s decisions may arrive as text the runtime cannot parse. The row
tells you the fix, and the fix is one line in a file you own:
sed -i 's/ intent: native/ intent: prompt_based/' \
relurpify_cfg/model/profiles/gemma4.llm.yamlWhether you need this depends on your model artifact — a tool-capable
build of gemma4:e4b will not show the row at all. The lesson is not the
specific edit; it is the shape of the exchange: the doctor names the
problem, the fix lives in relurpify_cfg/, and your edit wins. You will
run that loop for the rest of the book.
# 6. Boot the TUI
~/relurpify/relurpish chat --workspace .What you should see, per the project’s own guide: a Postures banner
(Postures — approvals: governed; intent selection: deterministic), three
tabs ([1] chat [2] diff [3] recipe), and an input bar at the bottom. You
are not chatting yet — Chapter 2’s first turn is this book’s real opening
night — but you can look around: the input bar accepts / for the command
registry (try /help), and ctrl+c quits.
Here is what the verification machine’s first boot rendered, captured from
a live run (the relurpify_cfg panels redraw at your terminal’s width; the
transcript is trimmed to one panel):
╭─────────────────────────────────────────────────────────╮
│ Doctor / Checks │
│ workspace ~/code/stringkit │
│ config ~/code/stringkit/relurpify_cfg │
│ manifest missing │
│ fingerprint e3985117c09367091d0a4cc6f7a5b28f11519a3a9f411114410f75552a30badc
│ workspace_directory OK workspace directory exists and is readable
│ disk_space OK sufficient disk space available (317725 MB)
│ runner MISSING no runner status (not spawned yet)
│ hitl_mode OK governed: human-approval requests block on a decision
│ selection_posture OK deterministic: ambiguous intents always ask a human
│ via a clarification frame
│ starter-templates OK workspace template archive available
│ model-profile OK gemma4.llm.yaml
│ sandbox_backend OK runsc version release-20260406.0
│ runsc OK runsc version release-20260406.0
│ docker MISSING degraded: runsc runtime not registered
│ audit_chain OK no audit chain yet (created on first agent run)
│ runtime_image OK unpinned (tag-based; resolved from the local daemon at
│ boot — set security.sandbox.image_digest to pin)
╰─────────────────────────────────────────────────────────╯
startup checks failed; resolve Doctor issues to unlock guest chat
tab checks/templates/logs r refresh h heal p modelsThe TUI’s first screen is the same report the doctor prints, rendered live
— the same checks, the same severities, now with r (refresh), h (heal),
and p (models) in the footer.
Note — a capture-harness postscript. An earlier draft of this chapter carried a warning here: on the author’s capture harness, this gate appeared to hold chat locked even with the CLI doctor reporting ready. Root cause found: the harness drove relurpish through a terminal pseudo- device with a broken window size — every locked capture also showed
Terminal too narrow. Given a real terminal (or a correctly sized pseudo-terminal), the gate opens onto the Postures banner immediately, and Chapter 2’s turns were captured that way. One quirk in the pane above is real but cosmetic: themanifest missingline reads from a status field that the doctor report never fills in, so it displays “missing” regardless of workspace health — it does not gate readiness. Worth a bug report when you feel at home; the door itself works.
Meanwhile you have lost nothing: the CLI doctor is this chapter’s oracle, it says ready, and everything Chapter 2 needs — a materialized, governed, model-backed workspace — is standing.
# Gotchas
doctor --fixis not the same asdoctor. On a fresh workspace, plaindoctormaterializes quietly.--fixre-materializes and asksOverwrite current starter config and manifest from templates? [y/N]:— answer carefully, becauseyoverwrites your edited copies with shipped templates. Theyour-edits-winrule protects you from future materializations, not from a confirmed overwrite.selected:is the probe’s pick, not your model. Turns are governed bymodel.nameinrelurpify_cfg/workspace.yaml. Chapter 3 separates the two for good.- A MISSING row is not always a blocker. Read the severity word and
the verdict line, in that order.
runner: missingandruntime_image: degradedare informational;Result: readyis the verdict. - The tool-template row is model-artifact-specific. If
model_tool_templateappears, your model build’s template lacks a tool section — switch to an artifact with tool support, or settool_calling.intent: prompt_basedin the model profile, exactly as the row’s hint says. - Terminal too narrow. The TUI needs at least 60 columns and wants more; on a cramped terminal it says so and renders badly. Widen the window and continue.
# Summary
- You hired the second candidate: Relurpify is the framework, Euclo the agent, relurpish the program you drive, and the workspace is the only room they work in — through a sandbox.
doctormaterializesrelurpify_cfg/once and then reports readiness forever after;Result: readyplus two informational warnings is a healthy machine.- The two-directory rule is the book’s constitution in miniature: you edit
relurpify_cfg/, you read.relurpify_state/. - Postures are the permit’s headline: governed approvals, deterministic selection. You will meet the approval banner in person next chapter.
- When the doctor names a problem, the fix lives in a file you own, and your edit wins.
# Check yourself
- What does
doctordo on a workspace that has norelurpify_cfg/directory, and what does it do on one that does? - Which of the two directories would you edit to change which model the agent uses — and which one holds the audit trail?
- The doctor’s output says
runner: missing [warning]andResult: ready. Is your workspace ready to boot? Why does the warning not block? - Your teammate runs
doctor --fixon their edited workspace and answersyat the overwrite prompt. What happens to their edits, and what does that teach about the difference between--fixand plaindoctor? - The probe’s output says
selected: parable/qwen3-fable:8bbutworkspace.yamlsaysname: gemma4:e4b. Which model runs your next turn, and why does the probe’s line exist at all?
Answers. 1. On a bare workspace it materializes the starter config
silently; on an initialized one it re-checks and reports. 2. Edit
model.name in relurpify_cfg/workspace.yaml; the audit trail lives in
.relurpify_state/audit/. 3. Yes — the row is severity warning, not
blocking, and the verdict line is the authority; the runner spawns on
first boot. 4. Their edits are overwritten by the shipped templates; --fix
is a deliberate re-materialization with a confirmation gate, while plain
doctor never overwrites. 5. gemma4:e4b — the workspace configuration
governs turns; the selected: line shows what the probe picked while
listing backend health.
# Try it yourself
- Fingerprint stability. Note the
Manifest fingerprint:line from your doctor run. Rundoctor --fix, answery, and run plaindoctoragain. Is the fingerprint the same? (It should be —--fixrestores the shipped templates; it does not invent new ones. If it differs, stop and read the diff before proceeding.) - Model swap, observed. Change
model.nameinrelurpify_cfg/workspace.yamlto another model your Ollama serves (ollama listshows them), re-run plaindoctor, and find where the new name shows up in the report. Swap it back when you are done.
# Where next
You have a kitchen, a work permit, and a report that says ready. Next
chapter you hand Euclo its first order — the broken slugify — and watch
one complete governed turn: classification, selection, an approval banner
with your name on it, a sandboxed edit, a test run, and the two receipts
the run leaves behind. Bring the red test; it finally turns green.
The lab for this chapter lives in
devdocs/cooking-agentic-thoughts/labs/ch01-stringkit/ — the exact
stringkit files, the captured failure, and verify.sh ch01 to check your
seed matches the book’s.