Course: Master Course — Harness Engineering Module / Section: 0 / 0.1 Duration: 45–60 minutes Environment: GitHub Codespace (recommended) or local Docker. No paid accounts required; the optional Pi-source step uses Node.js. Visual stack: This lab follows the course's visual order — n8n first, then code reading. You see the loop run before you read it.
By the end of this lab you will have:
Choose one of two environments.
Create a Codespace from any repo (or a blank one).
In the Codespace terminal:
# n8n in a container, data persisted to a local volume
docker run -d --name n8n -p 5678:5678 \
-v n8n_data:/home/node/.n8n \
n8nio/n8n:latest
# Codespace will forward port 5678. Open it in the browser.
Open the forwarded port 5678. Complete the one-time n8n owner setup.
docker run -d --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n n8nio/n8n:latest
open http://localhost:5678 # macOS; xdg-open on Linux
Expected output: n8n UI loads at http://localhost:5678 (or the Codespace-forwarded URL). You see an empty Workflows canvas.
Troubleshooting: If port 5678 is in use, change the left side of -p 5678:5678 (e.g. -p 5680:5678) and open the new port.
02-diagrams.md → Diagram 6 (the n8n workflow JSON block).Expected output: Seven nodes appear on the canvas, connected as in the ASCII node graph:
Manual Trigger → Assemble Context → Loop → Call Model → Stop or Route?
▲ │
│ ┌───────┴───────┐
│ done tool
│ │ │
│ ▼ ▼
│ Output Dispatch Tool
│ │
│ ┌───────────┼───────────┐
│ ▼ ▼ ▼
│ read_file write_file bash
│ └───────────┴───────────┘
└──────── Append Tool Result to History
The Call Model node needs an OpenAI API key.
gpt-4o-mini key is sufficient; the lab costs < $0.05.)No API key? You can still complete Phases 1 (steps 1.1, 1.4) and 2 and 3 fully without spending tokens — those phases use n8n's execution viewer and a mock. The model-call step requires the key only for the live run in step 1.3.
Double-click Assemble Context. Edit the task assignment to point at a real file you create in the n8n container:
# in a second terminal
docker exec -it n8n sh -c 'echo "Meeting notes: ship Module 0.1 by Friday. Owner: Brandon." > /tmp/NOTES.txt'
Then set task in the node to: Read /tmp/NOTES.txt and summarize it in one sentence.
Click Execute Workflow (top right).
Watch execution: the highlight travels Trigger → Context → Loop → Model → Route → read_file → Append → back to Loop → Model → Route (done) → Output.
Expected output: The Output node's final field contains a one-sentence summary of NOTES.txt. The execution took 2 turns of the loop: turn 1 read the file; turn 2 produced the summary.
The deliverable observation: the loop's turn boundary is the edge from Append Tool Result to History back into Loop. That edge is the single most important concept in the course. You have now watched it.
You will add a logging node that writes one record per turn. This is the lab analog of Module 10 (observability), introduced early so the rest of the module's failure modes are observable.
Add a new node after Append Tool Result to History. Type: Write Binary File or HTTP Request to a local webhook. Simplest: Code node.
Configure it to append a JSONL line:
// Code node — "log turn"
const ts = new Date().toISOString();
const turn = {
timestamp: ts,
tool: $json.name, // read_file / write_file / bash
tool_input: $json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments,
result_preview: ($json.stdout || '').slice(0, 200),
history_length_after: JSON.parse($('Assemble Context').item.json.history).length
};
// append to a file inside the container
const fs = require('fs');
fs.appendFileSync('/tmp/turns.jsonl', JSON.stringify(turn) + '\n');
return { logged: true, ...turn };
Wire its output back into the Loop node (replacing the direct Append → Loop edge).
docker exec -it n8n sh -c 'cat /tmp/turns.jsonl'
Expected output: one JSONL line per turn. For the NOTES.txt task, two lines — one for the read_file turn, one (empty tool) for the summarizing turn.
Quality check: The history_length_after field should grow by 2 each turn (assistant message + tool result). If it does not, your wiring is wrong — the tool result is not being appended correctly.
The rest of the course is about these. Here you provoke them on purpose, in a controlled environment, so you recognize them later.
Loop node, set maxIterations to 1.Expected output: Output.final is empty or contains only the model's first-turn reasoning. The task is incomplete. This is graceful degradation — the stop condition fired; nothing crashed.
maxIterations to 8.read_file on a path that does not exist: set task to Read /tmp/DOES_NOT_EXIST.txt and summarize it..read_file each turn until the iteration cap stops it.Expected output: 8 turns, all read_file failures, then a forced stop. This is the stuck-loop pattern. Module 7.2 builds a detector for exactly this — same tool + same input + same error = stuck.
Append Tool Result to History node, deliberately do NOT append — wire it to pass through without updating history.Expected output: 8 turns of read_file, all reading the same file, because the model never sees the prior result. This is what broken context management looks like. Module 3 is the systematic study of how not to do this.
You have now seen a harness loop run. Read one in code and confirm the same structure. Pi is the reference thin harness — ~1,200 lines of TypeScript.
Note: Pi is referenced in the teaching document as the minimal baseline. The exact public location of "Pi" by Mario Zechner has shifted over time; the canonical 80-line loop referenced in the teaching doc is the lab target. If the named repo is unavailable in your region, fall back to the Aider alternative in Step 4.5 — the 6-step method is repo-independent.
# In your Codespace terminal (separate from the n8n container)
git clone https://github.com/anthropics/anthropic-quickstarts
# OR, if available:
# git clone <pi repo URL from the course ecosystem map>
Fill in a 6-phase analysis card for Pi. For each step, record the file path and line numbers.
| Step | Find | Search command | Record |
|---|---|---|---|
| 1 | Entry point | cat package.json | grep -A3 scripts |
main file: ____ |
| 2 | The loop | grep -rn "while|messages.create|complete(" src/ |
loop file:line, stop conditions: ___ |
| 3 | Tool registry | grep -rn "input_schema|name:" src/ | grep -i tool |
tools (name + 1-line desc): ____ |
| 4 | Safety layer | grep -rn "permission|approve|sandbox|max_iter|deny" src/ |
safety mechanisms: ___ |
| 5 | Context manager | grep -rn "token|truncat|compact|summariz" src/ |
present? yes/no, what it does: ___ |
| 6 | Verdict | (write) | one paragraph: see Step 4.3 |
One paragraph. Required elements: loop architecture · tool count · permission model · position on the thickness spectrum · why the designers made those choices. Use this template:
Pi uses a [loop architecture] with [N] tools ([list]) and a [permission model]
permission model. It sits at the [thin/medium/thick] end of the thickness spectrum.
The designers chose this position because [use case], trading off [what they gave up]
for [what they gained]. This is [correct/incorrect] for that use case because [reason].
Answer in two sentences: Does Pi have the same three-layer structure (loop + tool registry + safety layer) as the n8n workflow? Cite one file:line for each of the three layers. If yes, you have confirmed the module's central claim — structure is the invariant, thickness is the variable.
git clone https://github.com/Aider-AI/aider
cloc aider # ~25k LOC, much larger than Pi
Apply the same 6 steps. Aider sits thin-medium on the spectrum (~8 tools, ~2k-token prompt, git-gated permission). Comparing Pi and Aider makes the thickness spectrum concrete: same three-layer structure, different thickness, both correct.
Submit the following as a single markdown file (lab-0.1-report.md):
/tmp/turns.jsonlInstructors / self-graders: the canonical solutions live in _solutions/lab-0.1-solution.md (kept separate from the lab so learners do not accidentally read it). Key checks:
finish_reason: stop on turn 2 — check the Stop-or-Route node's comparison value.history_length_after grows by exactly 2 per turn. If it grows by 1, the assistant message is missing; if it does not grow, the wiring is broken.maxIterations=1, exactly 1 turn runs and the summary is incomplete. Failure 2: 8 turns, all failures, then stop. Failure 3: 8 turns, all reads of the same file.list_dir) with a schema and a dispatch branch. Re-run. Did agent behavior improve? Did it get worse? (This is a teaser for Module 2.1 — the Vercel finding that cutting tools improves results.)Dispatch Tool and the tool nodes, blocking any write_file call. Re-run a task that requires writing. Observe how the model responds to permission denial. (Teaser for Module 6.)usage.total_tokens across turns and stop when it exceeds 5,000. (Teaser for Module 3 and Module 7.)# Lab Specification — Module 0.1: What Is a Harness?
**Course**: Master Course — Harness Engineering
**Module / Section**: 0 / 0.1
**Duration**: 45–60 minutes
**Environment**: GitHub Codespace (recommended) or local Docker. No paid accounts required; the optional Pi-source step uses Node.js.
**Visual stack**: This lab follows the course's visual order — **n8n first, then code reading**. You see the loop run before you read it.
---
## Learning objectives
By the end of this lab you will have:
1. **Seen** a harness loop run turn-by-turn in n8n — the turn boundary made visible.
2. **Instrumented** that loop to log each turn: model call, tool dispatched, result, token delta.
3. **Provoked** three failure modes the rest of the course covers (forced early stop, infinite loop, context rot) and observed them at the turn level.
4. **Read** Pi's source with the 6-step outside-in method and written an Architect's Verdict.
5. **Mapped** what you saw in n8n to what you read in Pi — confirming the invariant structure across two representations.
---
## Phase 0 — Environment setup (5 min)
Choose one of two environments.
### Option A — GitHub Codespace (recommended, zero setup)
1. Create a Codespace from any repo (or a blank one).
2. In the Codespace terminal:
```bash
# n8n in a container, data persisted to a local volume
docker run -d --name n8n -p 5678:5678 \
-v n8n_data:/home/node/.n8n \
n8nio/n8n:latest
# Codespace will forward port 5678. Open it in the browser.
```
3. Open the forwarded port `5678`. Complete the one-time n8n owner setup.
### Option B — Local Docker
```bash
docker run -d --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n n8nio/n8n:latest
open http://localhost:5678 # macOS; xdg-open on Linux
```
**Expected output**: n8n UI loads at `http://localhost:5678` (or the Codespace-forwarded URL). You see an empty Workflows canvas.
**Troubleshooting**: If port 5678 is in use, change the left side of `-p 5678:5678` (e.g. `-p 5680:5678`) and open the new port.
---
## Phase 1 — Import and run the harness loop in n8n (10 min)
### Step 1.1 — Import the workflow
1. In n8n: **Workflows → Add Workflow**.
2. Click anywhere on the canvas, then **⌘V / Ctrl+V** to paste. **OR**: top-right ••• menu → **Import from File**.
3. Paste/import the JSON from `02-diagrams.md` → **Diagram 6** (the n8n workflow JSON block).
**Expected output**: Seven nodes appear on the canvas, connected as in the ASCII node graph:
```
Manual Trigger → Assemble Context → Loop → Call Model → Stop or Route?
▲ │
│ ┌───────┴───────┐
│ done tool
│ │ │
│ ▼ ▼
│ Output Dispatch Tool
│ │
│ ┌───────────┼───────────┐
│ ▼ ▼ ▼
│ read_file write_file bash
│ └───────────┴───────────┘
└──────── Append Tool Result to History
```
### Step 1.2 — Wire a credential
The `Call Model` node needs an OpenAI API key.
1. Double-click **Call Model** → **Credential for OpenAI API** → **Create New**.
2. Paste a key. (A `gpt-4o-mini` key is sufficient; the lab costs < $0.05.)
3. **Save**.
> **No API key?** You can still complete Phases 1 (steps 1.1, 1.4) and 2 and 3 fully without spending tokens — those phases use n8n's execution viewer and a mock. The model-call step requires the key only for the live run in step 1.3.
### Step 1.3 — Run the loop on a real task
1. Double-click **Assemble Context**. Edit the `task` assignment to point at a real file you create in the n8n container:
```bash
# in a second terminal
docker exec -it n8n sh -c 'echo "Meeting notes: ship Module 0.1 by Friday. Owner: Brandon." > /tmp/NOTES.txt'
```
Then set `task` in the node to: `Read /tmp/NOTES.txt and summarize it in one sentence.`
2. Click **Execute Workflow** (top right).
3. Watch execution: the highlight travels Trigger → Context → Loop → Model → Route → `read_file` → Append → back to Loop → Model → Route (done) → Output.
**Expected output**: The `Output` node's `final` field contains a one-sentence summary of NOTES.txt. The execution took **2 turns** of the loop: turn 1 read the file; turn 2 produced the summary.
### Step 1.4 — See the turn boundary
1. After execution, click any node, then open **Executions** (left sidebar) → select the latest run.
2. Step through execution with the arrow keys. Each full ring traversal (Model → Route → Tool → Append → Loop) is **one turn**.
**The deliverable observation**: the loop's turn boundary is the edge from `Append Tool Result to History` back into `Loop`. That edge is the single most important concept in the course. You have now watched it.
---
## Phase 2 — Instrument the loop (10 min)
You will add a logging node that writes one record per turn. This is the lab analog of Module 10 (observability), introduced early so the rest of the module's failure modes are observable.
### Step 2.1 — Add a turn logger
1. Add a new node after `Append Tool Result to History`. Type: **Write Binary File** or **HTTP Request** to a local webhook. Simplest: **Code** node.
2. Configure it to append a JSONL line:
```javascript
// Code node — "log turn"
const ts = new Date().toISOString();
const turn = {
timestamp: ts,
tool: $json.name, // read_file / write_file / bash
tool_input: $json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments,
result_preview: ($json.stdout || '').slice(0, 200),
history_length_after: JSON.parse($('Assemble Context').item.json.history).length
};
// append to a file inside the container
const fs = require('fs');
fs.appendFileSync('/tmp/turns.jsonl', JSON.stringify(turn) + '\n');
return { logged: true, ...turn };
```
3. Wire its output back into the `Loop` node (replacing the direct Append → Loop edge).
### Step 2.2 — Re-run and inspect
```bash
docker exec -it n8n sh -c 'cat /tmp/turns.jsonl'
```
**Expected output**: one JSONL line per turn. For the NOTES.txt task, two lines — one for the `read_file` turn, one (empty tool) for the summarizing turn.
**Quality check**: The `history_length_after` field should grow by 2 each turn (assistant message + tool result). If it does not, your wiring is wrong — the tool result is not being appended correctly.
---
## Phase 3 — Provoke the three failure modes (10 min)
The rest of the course is about these. Here you provoke them on purpose, in a controlled environment, so you recognize them later.
### Failure 1 — Forced early stop (Module 1.2, Module 7)
1. In the `Loop` node, set `maxIterations` to **1**.
2. Re-run. The loop reads the file on turn 1, appends the result, returns to Loop — and stops, because the cap is hit before turn 2.
**Expected output**: `Output.final` is empty or contains only the model's first-turn reasoning. The task is incomplete. This is **graceful degradation** — the stop condition fired; nothing crashed.
### Failure 2 — The infinite-loop problem (Module 1.2, Module 7)
1. Restore `maxIterations` to **8**.
2. Break the task so the model keeps calling `read_file` on a path that does not exist: set `task` to `Read /tmp/DOES_NOT_EXIST.txt and summarize it.`.
3. Re-run. Watch the model retry `read_file` each turn until the iteration cap stops it.
**Expected output**: 8 turns, all `read_file` failures, then a forced stop. **This is the stuck-loop pattern.** Module 7.2 builds a detector for exactly this — same tool + same input + same error = stuck.
### Failure 3 — Context rot (Module 3)
1. Restore a working task, but in the `Append Tool Result to History` node, deliberately do NOT append — wire it to pass through without updating `history`.
2. Re-run. The model has no tool result in context on turn 2, so it asks for the file again. And again. Until the cap.
**Expected output**: 8 turns of `read_file`, all reading the same file, because the model never sees the prior result. **This is what broken context management looks like.** Module 3 is the systematic study of how not to do this.
---
## Phase 4 — Read Pi's source with the 6-step method (15 min)
You have now *seen* a harness loop run. Read one in code and confirm the same structure. Pi is the reference thin harness — ~1,200 lines of TypeScript.
### Step 4.1 — Get Pi
> Note: Pi is referenced in the teaching document as the minimal baseline. The exact public location of "Pi" by Mario Zechner has shifted over time; the canonical 80-line loop referenced in the teaching doc is the lab target. If the named repo is unavailable in your region, fall back to the **Aider** alternative in Step 4.5 — the 6-step method is repo-independent.
```bash
# In your Codespace terminal (separate from the n8n container)
git clone https://github.com/anthropics/anthropic-quickstarts
# OR, if available:
# git clone <pi repo URL from the course ecosystem map>
```
### Step 4.2 — Apply the 6 steps
Fill in a 6-phase analysis card for Pi. For each step, record the file path and line numbers.
| Step | Find | Search command | Record |
| --- | --- | --- | --- |
| 1 | Entry point | `cat package.json \| grep -A3 scripts` | main file: ____ |
| 2 | The loop | `grep -rn "while\|messages.create\|complete(" src/` | loop file:line, stop conditions: ___ |
| 3 | Tool registry | `grep -rn "input_schema\|name:" src/ \| grep -i tool` | tools (name + 1-line desc): ____ |
| 4 | Safety layer | `grep -rn "permission\|approve\|sandbox\|max_iter\|deny" src/` | safety mechanisms: ___ |
| 5 | Context manager | `grep -rn "token\|truncat\|compact\|summariz" src/` | present? yes/no, what it does: ___ |
| 6 | Verdict | (write) | one paragraph: see Step 4.3 |
### Step 4.3 — Write the Architect's Verdict
One paragraph. Required elements: loop architecture · tool count · permission model · position on the thickness spectrum · **why** the designers made those choices. Use this template:
```
Pi uses a [loop architecture] with [N] tools ([list]) and a [permission model]
permission model. It sits at the [thin/medium/thick] end of the thickness spectrum.
The designers chose this position because [use case], trading off [what they gave up]
for [what they gained]. This is [correct/incorrect] for that use case because [reason].
```
### Step 4.4 — Confirm the invariant
Answer in two sentences: **Does Pi have the same three-layer structure (loop + tool registry + safety layer) as the n8n workflow? Cite one file:line for each of the three layers.** If yes, you have confirmed the module's central claim — structure is the invariant, thickness is the variable.
### Step 4.5 — Aider fallback (if Pi is unavailable)
```bash
git clone https://github.com/Aider-AI/aider
cloc aider # ~25k LOC, much larger than Pi
```
Apply the same 6 steps. Aider sits thin-medium on the spectrum (~8 tools, ~2k-token prompt, git-gated permission). Comparing Pi and Aider makes the thickness spectrum concrete: same three-layer structure, different thickness, both correct.
---
## Deliverables
Submit the following as a single markdown file (`lab-0.1-report.md`):
- [ ] **Phase 1**: screenshot of the n8n execution viewer showing the 2-turn run on NOTES.txt
- [ ] **Phase 2**: contents of `/tmp/turns.jsonl`
- [ ] **Phase 3**: one screenshot + one-sentence observation for each of the three failure modes
- [ ] **Phase 4**: the completed 6-step analysis card (table above) and the Architect's Verdict paragraph for Pi (or Aider)
- [ ] **Phase 4.4**: the two-sentence invariant confirmation, with file:line citations
---
## Solution key
Instructors / self-graders: the canonical solutions live in `_solutions/lab-0.1-solution.md` (kept separate from the lab so learners do not accidentally read it). Key checks:
- **Phase 1.3**: a correct run produces exactly 2 turns for the NOTES.txt task. >2 turns suggests the model is not emitting `finish_reason: stop` on turn 2 — check the Stop-or-Route node's comparison value.
- **Phase 2**: `history_length_after` grows by exactly 2 per turn. If it grows by 1, the assistant message is missing; if it does not grow, the wiring is broken.
- **Phase 3 Failure 1**: with `maxIterations=1`, exactly 1 turn runs and the summary is incomplete. Failure 2: 8 turns, all failures, then stop. Failure 3: 8 turns, all reads of the same file.
- **Phase 4 verdict**: a correct Pi verdict names ReAct-derived loop, 4 tools, trust-the-model, thin end of spectrum, justifies thinness by the personal-assistant use case. A verdict that calls Pi "under-engineered" or "needs more tools" fails the module's central learning objective.
---
## Stretch goals
1. **Add a 5th tool** to the n8n workflow (`list_dir`) with a schema and a dispatch branch. Re-run. Did agent behavior improve? Did it get worse? (This is a teaser for Module 2.1 — the Vercel finding that cutting tools *improves* results.)
2. **Add a permission gate** as an IF node between `Dispatch Tool` and the tool nodes, blocking any `write_file` call. Re-run a task that requires writing. Observe how the model responds to permission denial. (Teaser for Module 6.)
3. **Replace the iteration-cap stop** with a token-budget stop: keep a running sum of `usage.total_tokens` across turns and stop when it exceeds 5,000. (Teaser for Module 3 and Module 7.)