Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ test-results/
playwright-report/
.factory-base/
**/.stryker-tmp/
.labs/
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: prove-homework doctor dev check check-affected prove levels claude smoke hooks eval factory exercise
.PHONY: lab next progress prove-homework doctor dev check check-affected prove levels claude smoke hooks eval factory exercise

doctor:
@node -v | grep -qE '^v2[2-9]' && echo "ok node $$(node -v)" || (echo "need Node 22+" && exit 1)
Expand Down Expand Up @@ -51,3 +51,13 @@ levels:
# A session that ignores your own ~/.claude, so a demo shows only this repo's config.
claude:
CLAUDE_CONFIG_DIR=$(CURDIR)/.workshop-home claude

# Hands-on labs, start to advanced. HINT=1 reveals one hint; ID=<lab> picks a lab; progress is in .labs/ (gitignored).
lab:
npx tsx scripts/labs/main.ts lab $(ID) $(if $(HINT),--hint)

next:
npx tsx scripts/labs/main.ts next

progress:
npx tsx scripts/labs/main.ts progress
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"!**/node_modules",
"!fixtures",
"!supplied",
"!.labs",
"!test-results",
"!playwright-report",
"!.workshop-home",
Expand Down
56 changes: 56 additions & 0 deletions docs/labs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Labs: one line from first prompt to a running factory

The levels explain. The labs make you do it. Take them in order, one command at a time. Each lab has one goal, exact steps, a proof that is red until you finish, hints that appear one at a time, and a line on what you just learned.

Two workshops use the same path.

- **Claude Code, beginner to pro:** the `start`, `daily`, `team` and `advanced` lanes.
- **Software Factory:** the `factory` lane, after `start` and `daily`.

## Commands

```
make progress the lane map with a bar per lane
make next show the next lab without running anything
make lab show the current lab, run its proof, record a pass
make lab HINT=1 reveal the next hint, one per call
make lab ID=daily-03 work on any lab, in any order
```

Colour turns off when `NO_COLOR` is set or output is not a terminal. Progress sits in `.labs/progress.json`, which git ignores. Your work for each lab goes under `.labs/work/`, so a lab never changes tracked files.

Run Claude Code in one terminal and `make lab` in a second one beside it.

## The path

| Lab | Lane | Goal | Pairs with |
| ------------- | -------- | --------------------------------------------- | ------------------------------------------ |
| `start-01` | start | Ask Claude about the repo | [hand-built](../levels/hand-built.md) |
| `start-02` | start | Run the gate yourself | [hand-built](../levels/hand-built.md) |
| `start-03` | start | Plan before you edit | [context](../levels/context.md) |
| `start-04` | start | Teach Claude a personal habit | [context](../levels/context.md) |
| `daily-01` | daily | Continue where you left off | [hand-built](../levels/hand-built.md) |
| `daily-02` | daily | Write a rule that loads only where it applies | [context](../levels/context.md) |
| `daily-03` | daily | Package a habit as a skill | [skills](../levels/skills.md) |
| `daily-04` | daily | Block a mistake with a hook | [boundary](../levels/boundary.md) |
| `team-01` | team | Deny what Claude must never read | [boundary](../levels/boundary.md) |
| `team-02` | team | Write the review rules | [verification](../levels/verification.md) |
| `team-03` | team | Run a proof and keep the receipt | [verification](../levels/verification.md) |
| `team-04` | team | Package your setup as a plugin | [sharing](../levels/sharing.md) |
| `factory-01` | factory | Turn a ticket into an intent | [F1 Plan](../factory/f1-plan.md) |
| `factory-02` | factory | Write acceptance before code | [F2 Design](../factory/f2-design.md) |
| `factory-03` | factory | See what a run would do | [F3 Build](../factory/f3-build.md) |
| `factory-04` | factory | Check the workflow contract | [F8 Workflows](../factory/f8-workflows.md) |
| `advanced-01` | advanced | Write a read-only reviewer subagent | [execution](../levels/execution.md) |
| `advanced-02` | advanced | Measure what a call costs | [agent SDK](../agent/agent-sdk.md) |
| `advanced-03` | advanced | Work in a parallel worktree | [agent teams](../electives/agent-teams.md) |
| `advanced-04` | advanced | Write a check that walks the whole tree | [workflows](../levels/workflows.md) |

## How the labs are kept correct

- A test parses every lab and fails if one lacks a goal, steps, a proof or two hints.
- A test runs every proof in an empty folder and expects red, then applies a known solution and expects green. A proof that passes with no work, or can never pass, fails the build.
- A test fails if a lab or its pairing is missing from the table above.
- `levels.json` gives every lab file to trunk, so every level copy carries the labs.

To add a lab: write `labs/<lane>/NN-kebab-name.md`, add its row here, add its solution to `tests/structure/lab-solutions.test.ts`, then run `make check`.
30 changes: 30 additions & 0 deletions labs/advanced/01-reviewer-subagent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Write a read-only reviewer subagent

**Pairs with:** `docs/levels/execution.md`

## Goal

Define a subagent that can read and search but cannot write, so a review can never change the code.

## Steps

1. Read a real one: `ls .claude/agents` then `cat` one.
2. Make the folder: `mkdir -p .labs/work/agents`.
3. Write `.labs/work/agents/reviewer.md` with front matter `name`, `description`, `tools` and `model`, then a short prompt.
4. The `tools` line lists only `Read, Grep, Glob`.

## Proof

```sh
f=.labs/work/agents/reviewer.md; grep -q '^name: reviewer' $f && grep -q '^description: ' $f && grep -q '^tools: Read, Grep, Glob$' $f && grep -q '^model: ' $f && ! grep -Eq '^tools:.*(Write|Edit|Bash)' $f
```

## Hints

1. Four front matter keys: `name`, `description`, `tools`, `model`.
2. The tools line is exactly `tools: Read, Grep, Glob`.
3. Pick a small model for the `model` line. A reviewer reads, it does not need the largest.

## What you just learned

The tools list is the fence. A reviewer without Write cannot fix things quietly, so its report is the whole truth.
30 changes: 30 additions & 0 deletions labs/advanced/02-measure-a-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Measure what a call costs

**Pairs with:** `docs/agent/agent-sdk.md`

## Goal

Run one headless call with a turn cap and read its cost from the JSON result.

## Steps

1. Make the folder: `mkdir -p .labs/work`.
2. Call with a cap: `claude -p "List the folders in this repo." --max-turns 2 --output-format json > .labs/work/call.json`.
3. Read the cost: `node -e "console.log(require('./.labs/work/call.json').total_cost_usd)"`.
4. Write the number into `.labs/work/cost.txt`.

## Proof

```sh
grep -q 'total_cost_usd' .labs/work/call.json && grep -Eq '^[0-9]+(\.[0-9]+)?$' .labs/work/cost.txt
```

## Hints

1. Two files: `.labs/work/call.json` from the call and `.labs/work/cost.txt` with one number.
2. `echo 0.012 > .labs/work/cost.txt` shows the shape. Use your own number.
3. `--max-turns` stops the loop, so the cost has a ceiling.

## What you just learned

Cost is a field in the output, not a surprise on the invoice. Cap the turns and read the number.
30 changes: 30 additions & 0 deletions labs/advanced/03-parallel-worktree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Work in a parallel worktree

**Pairs with:** `docs/electives/agent-teams.md`

## Goal

Create a second checkout of the repo so two Claude sessions can edit at once without touching each other's files.

## Steps

1. Make the folder: `mkdir -p .labs/work`.
2. Add the worktree: `git worktree add .labs/work/wt -b lab/parallel`.
3. Start a session in it: `cd .labs/work/wt && claude`, then leave with `/exit`.
4. Remove it when you are done: `git worktree remove .labs/work/wt`. Keep it until the proof is green.

## Proof

```sh
test -f .labs/work/wt/.git && grep -q 'gitdir' .labs/work/wt/.git
```

## Hints

1. A worktree has a `.git` file, not a folder, that points at the main checkout.
2. `-b lab/parallel` makes a new branch, because one branch cannot be checked out twice.
3. Run the add command from the repo root, not from inside `.labs`.

## What you just learned

Parallel work needs separate working folders. Two sessions in one folder overwrite each other.
35 changes: 35 additions & 0 deletions labs/advanced/04-structural-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Write a check that walks the whole tree

**Pairs with:** `docs/levels/workflows.md`

## Goal

Write a script that fails when any file name is not kebab-case, then prove it fails on a bad tree.

## Steps

1. Read one: `cat scripts/check-conventions.ts`.
2. Make the folder: `mkdir -p .labs/work`.
3. Write `.labs/work/check-kebab.mjs`. It takes a folder, walks it, prints each file whose name is not lowercase with dashes and dots, and exits 1 if it found any.
4. Try it on the docs: `node .labs/work/check-kebab.mjs docs; echo $?`.

## Proof

```sh
s=.labs/work/check-kebab.mjs
node $s docs >/dev/null 2>&1 || exit 1
mkdir -p .labs/work/bad && : > .labs/work/bad/BadName.txt
node $s .labs/work/bad >/dev/null 2>&1; r=$?
rm -rf .labs/work/bad
test $r -eq 1
```

## Hints

1. Use `readdirSync(dir, { recursive: true })` and test each name with a regular expression.
2. Test the last path segment only, such as `/^[a-z0-9.-]+$/`. Folders can be tested the same way.
3. Print each offender, set `process.exitCode = 1` when the list is not empty.

## What you just learned

One script that walks every file beats a reviewer's memory. Prove it by planting a bad file and watching it go red.
30 changes: 30 additions & 0 deletions labs/daily/01-resume-a-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Continue where you left off

**Pairs with:** `docs/levels/hand-built.md`

## Goal

Start a session, leave it, and pick it up again with its context intact.

## Steps

1. Make the folder: `mkdir -p .labs/work`.
2. Start a session and save its metadata: `claude -p "Remember the word tangerine." --output-format json > .labs/work/session.json`.
3. Continue the same session: `claude -p --continue "What word did I ask you to remember?" > .labs/work/recall.md`.
4. Read the answer: `cat .labs/work/recall.md`.

## Proof

```sh
grep -q 'session_id' .labs/work/session.json && grep -qi 'tangerine' .labs/work/recall.md
```

## Hints

1. Two files must exist: `.labs/work/session.json` and `.labs/work/recall.md`.
2. `--continue` reopens the most recent session in this folder.
3. In the interactive app the same move is `claude --continue`, or `/resume` to choose from a list.

## What you just learned

A session is a saved conversation. Resume it instead of re-explaining the task.
30 changes: 30 additions & 0 deletions labs/daily/02-path-scoped-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Write a rule that loads only where it applies

**Pairs with:** `docs/levels/context.md`

## Goal

Write a rule file scoped to one folder, so Claude reads it only when it works there.

## Steps

1. Read a real one: `ls .claude/rules` then `cat` any file in it.
2. Make a draft folder: `mkdir -p .labs/work/rules`.
3. Create `.labs/work/rules/api-errors.md` with a `paths:` front matter for `api/**` and two rules about customer-facing errors.
4. When you like it, copy it to `.claude/rules/` in your own project.

## Proof

```sh
head -1 .labs/work/rules/api-errors.md | grep -q '^---$' && grep -q '^paths:' .labs/work/rules/api-errors.md && test "$(grep -c '^- ' .labs/work/rules/api-errors.md)" -ge 2
```

## Hints

1. The file starts with `---`, then a `paths:` list, then `---`, then bullets starting with `- `.
2. A path is a glob such as `api/**`.
3. Write rules as checks you could fail, such as "an error to a customer never names the model".

## What you just learned

Rules cost context only when their paths match. Scope every rule to the files it governs.
30 changes: 30 additions & 0 deletions labs/daily/03-first-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Package a habit as a skill

**Pairs with:** `docs/levels/skills.md`

## Goal

Write a skill that Claude loads by its description, so you stop pasting the same instructions.

## Steps

1. Read one: `ls .claude/skills` then open a `SKILL.md`.
2. Make the folder: `mkdir -p .labs/work/skills/commit-message`.
3. Create `.labs/work/skills/commit-message/SKILL.md` with `name:` and `description:` front matter, then the steps.
4. The description says when to use it, not what it is.

## Proof

```sh
f=.labs/work/skills/commit-message/SKILL.md; grep -q '^name: commit-message' $f && grep -q '^description: Use when' $f && test "$(grep -c . $f)" -ge 8
```

## Hints

1. Front matter needs exactly `name: commit-message` and a `description:` that starts with `Use when`.
2. The body should have at least a few lines of steps.
3. Good first steps: read the staged diff, write a subject under 60 characters, say why in the body.

## What you just learned

A skill is loaded on demand. The description is the trigger, so write it as a situation.
32 changes: 32 additions & 0 deletions labs/daily/04-first-hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Block a mistake with a hook

**Pairs with:** `docs/levels/boundary.md`

## Goal

Write a hook that stops Claude from editing `.env`, and prove it by feeding it both cases.

## Steps

1. Read a real hook: `cat .claude/hooks/pretooluse-file-guard.mjs`.
2. Make the folder: `mkdir -p .labs/work/hooks`.
3. Write `.labs/work/hooks/block-env.mjs`. It reads JSON from stdin, looks at `tool_input.file_path`, and exits with code 2 and a message on stderr when the path ends in `.env`. Otherwise it exits 0.
4. Try it: `echo '{"tool_input":{"file_path":".env"}}' | node .labs/work/hooks/block-env.mjs; echo $?`.

## Proof

```sh
h=.labs/work/hooks/block-env.mjs
echo '{"tool_input":{"file_path":".env"}}' | node $h >/dev/null 2>&1; test $? -eq 2 || exit 1
echo '{"tool_input":{"file_path":"a.ts"}}' | node $h >/dev/null 2>&1
```

## Hints

1. Read all of stdin first: `const input = JSON.parse(readFileSync(0, "utf8"))`.
2. Exit code 2 tells Claude Code to block the call and show stderr to the model.
3. Import `readFileSync` from `node:fs` and end with `process.exit(0)` for the allowed case.

## What you just learned

A hook is a program, not a prompt. Exit code 2 blocks the action every time, even when the model would have forgotten the rule.
30 changes: 30 additions & 0 deletions labs/factory/01-write-intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Turn a ticket into an intent

**Pairs with:** `docs/factory/f1-plan.md`

## Goal

Write the one-page intent a factory run starts from: the problem and how anyone will know it is done.

## Steps

1. Read the stage: `cat docs/factory/f1-plan.md`.
2. Make the folder: `mkdir -p .labs/work/specs/1`.
3. Write `.labs/work/specs/1/intent.md` with the headings `## Problem`, `## Done when` and `## Out of scope`.
4. Under Done when, write two checks a command could run.

## Proof

```sh
f=.labs/work/specs/1/intent.md; grep -q '^## Problem' $f && grep -q '^## Done when' $f && grep -q '^## Out of scope' $f && test "$(grep -c '^- ' $f)" -ge 2
```

## Hints

1. Three headings, spelled exactly, plus at least two bullets.
2. "Done when" is a list of bullets such as "`make check` exits 0".
3. Out of scope names one thing you will not do. It stops a run from growing.

## What you just learned

The factory reads only what was committed. An intent with a checkable "done" is the contract every later stage tests against.
Loading
Loading