Document, Deploy, and Hand It Off
What does it mean to entrust your work to another?
Chapter 7 — Document, Deploy, and Hand It Off
“Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.” — Donald E. Knuth, “Literate Programming,” The Computer Journal (1984)
“…what you have heard from me in the presence of many witnesses entrust to faithful men, who will be able to teach others also.” — 2 Timothy 2:2 (ESV)
Why This Matters
Last week you froze a release candidate. The tests are green, the defect log is honest, the change log records what you cut and why. If this were a class assignment you would be finished. It is not. It is a project you are about to hand to somebody else — and right now exactly one person on earth can run it, and you are that person.
This week you wear two hats at once. Technical writer: the person who decides what a stranger needs to know, in what order, and proves it by watching a stranger try. Release engineer: the person responsible for the distance between working code and running software, and between running software and software somebody else can operate. In the sixteen-week edition those are two separate weeks. Here they are one, and the compression is honest rather than brutal, because they are the same act seen from two sides: making a system transferable. The life cycle calls this stretch the transition phase. Verification asked does it do what we said? Transition asks can anyone but you keep it doing that?
Here is the number. The final submission is 50% of your grade — and the milestones that build it are another 25% — and a large share of both is produced this week: the README, the architecture overview, the runbook, the handoff guide, the change log, the license, the release tag, and the evidence that a stranger can actually run the thing. In an eight-week course one week is an eighth of the term, so falling behind is proportionally twice as expensive as in the long edition. Skipping this week does not cost a small penalty now — it moves the same points into Week 8, where the hours have already been spent (Appendix D).
The two-thirds rule applies exactly as it did in Week 1. You committed to three or four Must features, not five or more, so your documentation set is proportionally smaller: four runbook failure entries, not eight; five landmines, not fifteen; one page of architecture, not six. Nothing professional is cut in kind this week — only in volume. A three-feature system documented completely beats a six-feature system documented aspirationally, and a grader can tell the difference in ninety seconds.
The AI thread runs hard from both directions. As a tool, an assistant will draft a README from your repository in ninety seconds, fluent and well-organized and confidently wrong in exactly the places that matter — the install command that was true three commits ago, the variable it inferred from a name, the port it guessed. Generated setup steps fail silently, because prose does not throw exceptions. As a workload, if your system calls a model you are handing a successor a dependency you do not control: a pinned version somebody may deprecate, a key that costs money, a bill nobody has agreed to pay after you graduate. Both sides land on the spine rule — the assistant accelerates the draft; you run every command before it reaches a reader, and you are accountable for what is running.
Underneath it: what does it mean to entrust your work to another? Not to publish it. Not to submit it. To entrust it — to put a thing you made into hands that are not yours, knowing it will be changed, and to prepare those hands on purpose. We take that seriously in §7.13. And one promise: by Friday you will not believe your documentation works. You will have watched a person you did not coach reach a working system on a machine you have never touched, with a stopwatch running. That is the only kind of confidence worth having.
7.1 — Documentation Is Graded Because Documentation Is the Deliverable
Students hear “documentation” and think tax — something you pay after the real work, in a hurry, to satisfy a rubric. Kill that framing now. Your repository has two halves. One is executable by a machine. The other is executable by a person: instructions that, run correctly by a human, produce a working system and a mind that understands it. Both halves can have bugs. Only one of them tells you when it is broken, and it is not the prose. A README is a program whose runtime is a human being. So the discipline is one you already have. You do not ship code you have not run. You do not ship documentation you have not run either — and “running” documentation means a person following it on a machine that is not yours. Here is the set this edition requires. Every document answers exactly one question for exactly one reader; when a document tries to answer two, it answers neither.
| Document | Repo path | Its one reader | Its one question |
|---|---|---|---|
| README | README.md | a stranger, right now | ”Can I run this in fifteen minutes?” |
| Architecture overview | docs/architecture.md §0 | an engineer sizing up the system | ”How is this put together, and why?” |
| Runbook | docs/runbook.md | whoever is on the hook when it breaks | ”It’s broken — or it needs deploying. What do I do?” |
| Handoff guide | docs/handoff.md | the next owner | ”How do I take custody of this?” |
| Decision index | docs/adr/README.md | someone about to reverse a decision | ”Why is it like this?” |
| AI and vendor notes | docs/ai-usage.md | a successor and a grader | ”What can change without the code changing?” |
| Change log | CHANGELOG.md | anyone upgrading | ”What changed between versions?” |
| Clean-machine record | docs/clean-machine-test.md | a grader | ”Did somebody who is not you actually run this?” |
Three merges are the compression, and they are deliberate rather than lazy. The long edition ships a separate onboarding guide; here §2 of the handoff guide is the onboarding guide — the week-one ramp that ends in a merged change. The long edition ships a separate deployment document; here §1 and the rollback section of the runbook carry it, because a runbook that does not say where the thing runs is not a runbook. And the long edition ships a separate third-party notices file; here the license inventory is a table in the handoff guide (generate the standalone file for extra credit if you want it). You write four documents this week instead of seven. They carry the same information.
Coach’s Note — If you fix exactly one document this week, fix
README.md. It is read by everyone — grader, employer, classmate, stranger — and its failure is instantaneous and total. A perfect architecture overview behind a broken README is a beautiful room behind a locked door.
7.2 — The README That Gets a Stranger Running in Fifteen Minutes
Fifteen minutes. That is the bar, and it is not arbitrary — it is roughly how long a motivated stranger fights your project before deciding it does not work. Past fifteen minutes they do not conclude “this needs more setup.” They conclude “this is broken,” close the tab, and, if they are grading you, start writing. Nine sections get them there. This is the standard the Handoff Readiness Auditor scores against and the standard the Milestone-7 rubric uses: (1) what it is, who it is for, and what it does not do, with a status line; (2) prerequisites with versions and a check command each; (3) install, with what success looks like; (4) configure — every variable, what it means, what happens without it; (5) run — the command, the expected output, and the action that proves it works; (6) test — the command, the expected count, the known failures; (7) project layout; (8) troubleshooting with real symptoms and real fixes; (9) license.
Nine, not eleven. The long edition adds a demo section and a contact section; both are worth having and neither is graded here, which is exactly the kind of trade an eight-week budget forces you to make on purpose instead of by accident. Copy the skeleton from code/readme-template.md and fill it against your actual repository. Now the standard, shown, using PantryPilot — the household pantry app that has been this book’s running example since Week 1, and brief #1 in the Fast-Start Catalog. PantryPilot runs on Node and PostgreSQL per its ADR-0001. Your stack will differ; the shape will not. The version most students ship opens with “a pantry app for tracking food, built with modern technologies — still a work in progress, some features coming soon,” followed by a Setup section that says “clone the repo and install dependencies, then start the dev server; should be pretty straightforward.” Count the failures. “Modern technologies” tells a reader nothing and signals the author could not be bothered. “Some features coming soon” is a promise, not a status. “Install dependencies” is not a command. “Should be pretty straightforward” is the author’s opinion about the reader’s next twenty minutes, and it will be wrong. Here is the version that works:
# PantryPilot
Track what food your household has, what expires soon, and what you can cook tonight.
Built for a shared house of four; three people use it weekly.
**Status:** Working. Inventory, expiry alerts, and barcode lookup are complete.
Recipe suggestions are behind a feature flag and require an API key (see Configure).
**What it does not do:** no mobile app, no multi-household support, no nutrition data.
## Prerequisites
Node.js 20.11.1 (`node --version`) · PostgreSQL 16.2 (`psql --version`) · a barcode
lookup API key (see Configure). Node 16 and below will not work — the code calls the
global `fetch`.
That version is longer. That is not why it is better. It is better because a stranger answers three questions in twenty seconds: is this for me, does it work, and what will it cost me to try? And the last clause converts a mysterious failure into a known one. Prerequisites are where your machine lies to you. You have been quietly configuring that laptop for years, and your project depends on that machine while your README describes an imaginary one. First, pin what you actually have — stand in the directory where the project works, print real version strings, and paste the real output. Write “tested on,” not “requires”: you know what worked, not the full range that works. Then hunt the personal with grep -rnE "/litman-books/Users/|/home/[a-z]|localhost:[0-9]{4}|127\.0\.0\.1" README.md docs/ — every hit is a path only you have or a port you never told the reader about. Then find the invisible accounts: an API key, a database you made by hand in a GUI in Week 3, an OAuth app registered under your personal address. Nothing installs for those, and a stranger cannot guess them.
| Weak prerequisite | Strong prerequisite |
|---|---|
| ”Node and Postgres" | "Node.js 20.11.1 (node --version); PostgreSQL 16.2 (psql --version)" |
| "You’ll need an API key" | "A barcode lookup API key. Verify current tier limits with the vendor. Without it, scan returns 503 and manual entry still works." |
| "Set up the database" | "createdb pantrypilot then npm run migrate — expect 12 migrations applied” |
The strong column names the failure. A prerequisite without a failure mode is a suggestion.
Coach’s Note — Write the What it does not do paragraph first, before anything else. It is the hardest ten minutes in the document and the paragraph that makes an experienced reader trust everything else you wrote. Everyone overstates what their project does. Almost nobody states what it doesn’t. Be the exception.
And here is this edition’s specific trap, the one Week 2 warned you about wearing a different costume. If you adopted a Fast-Start brief, the brief’s non-goals are not your non-goals. The catalog told you what brief #1 would not do; you then cut two more things in Week 6. A README whose “what it does not do” section is the brief’s list is a document quietly describing somebody else’s project — the signature failure mode of the accelerated edition, showing up in Week 7 because nobody caught it in Week 2. Read every sentence of your README and ask: did I write this, or did I inherit it?
7.3 — The Architecture Overview and the Decision-Record Index
You wrote a technical specification in Week 3. It is detailed and almost nobody will read it. The architecture overview is different: the one page that lets a competent engineer hold your system in their head in five minutes and then decide whether they need the long version. It goes at the front of docs/architecture.md as a new §0 — Overview. Do not delete the Week-3 sections to make room; they are a rubric line of their own, and this week you update them as built.
One diagram, five paragraphs. The diagram shows components and the arrows between them — not classes, not files. Four to eight boxes; every box a thing that runs or stores; every arrow labeled with what flows across it. Mermaid in Markdown is a good default: it renders on GitHub and lives in version control next to the code.
flowchart LR
U[Browser client] -->|HTTPS / JSON| A[API server]
A -->|SQL| D[(PostgreSQL)]
A -->|HTTPS, cached 24h| B[Barcode lookup API]
A -->|prompt + inventory| L[Recipe model service]
S[Nightly expiry job] --> D
S -->|email| U
Then five paragraphs, each answering one question: what the system does in technical terms; the components and what each owns and deliberately does not; the data — the three or four entities that matter and what is authoritative; one end-to-end flow traced from a real click to a real response through every box; and the consequential decisions, each linked to its record, ending with the one thing that would surprise a new reader. That last clause is the one students skip and reviewers reward. Every real system has a surprise: a table intentionally denormalized, a cache with a strange expiry, a synchronous call that should be a queue but is not because this is an eight-week project. Name it. A surprise you disclosed is a tradeoff; a surprise a reader discovers alone is a bug.
Bad paragraph two: “The system uses a modern three-tier architecture with a frontend, a backend, and a database, organized into controllers, services, and models. This design promotes separation of concerns and maintainability.” Nothing there is false. Nothing there is about your project. You could paste it into ten thousand repositories without editing it, which is the test it fails.
Good paragraph two: “The API server owns all business rules, including the expiry calculation, which is deliberately not a computed column — see ADR-0006. The client is a thin rendering layer; if you find a rule in the client, it is a bug. PostgreSQL is the single source of truth for inventory. The barcode service is treated as untrusted and optional: responses are cached 24 hours and every path that uses it has a manual-entry fallback, because it was down for six hours in Week 5 and the app stayed usable.” Same length. One of them is about a system.
The decision index turns a folder of numbered files into a resource. It lives at docs/adr/README.md so the platform renders it when someone opens the folder:
| ADR | Decision | Status | Consequence you would notice |
|---|---|---|---|
| 0001 | Node + PostgreSQL over Python + SQLite | Accepted | Deployment needs a real database host |
| 0004 | Server-rendered pages, no SPA framework | Accepted | No offline mode; page load on every action |
| 0006 | Expiry computed in the app, not in SQL | Accepted | Rules stay testable; queries are chattier |
| 0009 | Recipe model pinned, never floating | Accepted | Upgrades are deliberate; behavior does not drift |
Status carries the weight and is the field students leave stale: Proposed, Accepted, Superseded by NNNN, Deprecated. Never delete a superseded record — deleting it destroys exactly the history the index exists to preserve. The fourth column is a gift to the successor: it says which decision to reopen when a requirement changes. Then the exercise worth your hour: find the decision with no record. In an eight-week course there is always one, and it is almost always the thing you chose in a Week-5 afternoon under pressure that has shaped everything since. If you adopted a Fast-Start brief and deviated from its suggested stack, that deviation is your unrecorded decision. Write it now, dated honestly, with the context you actually had.
7.4 — The Runbook: Operate It, and What to Do When It Breaks
The README gets a stranger to a running system. The runbook keeps it running, deploys it, and tells whoever inherits it what to do at an hour when you are not answering messages. Its audience is the clearest in the set: a competent person, under time pressure, who does not know your system. Short sentences. Commands, not descriptions of commands. Expected output for everything, because in a crisis the operator’s most valuable information is whether the thing they just ran worked. Seven sections, and in this edition it absorbs the deployment record:
docs/runbook.md
1. Where it runs host, URL, region, what account owns it
2. Deploy numbered commands, always FROM THE TAG, never from a working copy
3. Start / stop / restart + the health check, with healthy and unhealthy output
4. Normal readings startup time, typical response time, harmless noisy log lines
5. Failure catalogue >= 4 entries, mined from docs/defect-log.md
6. Rollback numbered, rehearsed once, with a wall-clock time next to it
7. Escalation + last verified (date, name, and UNVERIFIED on anything you did not run)
The heart is the failure catalogue, and the rule that makes it real: write it from your defect log, not from your imagination. Every defect you fixed already happened once in this system, which makes it far likelier than any failure you could invent. Four entries minimum in this edition — the best ones come from the bugs that embarrassed you. A bad entry reads “Database issues — if the database has problems, check the connection and restart the service if needed.” Read that as somebody whose app is down. Which problems? With what command? How would I know if it is needed? Every clause pushes the decision back onto the reader who came here because they did not know what to do. A good entry:
### F-03 — App returns 500 on every request; log shows ECONNREFUSED 127.0.0.1:5432
- Symptom: every request 500s. `docker compose logs api` repeats ECONNREFUSED 5432.
- First check: `pg_isready -h localhost -p 5432` → healthy is "accepting connections".
- Likely cause: the db container is not running, or started after the API and the API did
not retry. Happened twice in Week 6 (defects DEF-014, DEF-019).
- Fix: `docker compose up -d db`, wait for pg_isready, `docker compose restart api`.
- Verify: `curl -s localhost:3000/health` returns {"status":"ok","db":"connected"}.
- If that fails: the data volume may be corrupt. STOP. Do NOT run `docker compose down -v` —
that deletes the volume. Restore from the backup in §1 and escalate.
That last line separates a runbook from a wiki page: it tells a frightened operator which action is irreversible. A runbook that does not name the destructive commands is worse than none, because it moves fast and confident right up to the edge of data loss. And rollback is a deliverable, not a paragraph. Deploy the tag. Deploy something broken on purpose. Get back using only the numbered steps, with a timer running, and write the wall-clock number down. Then answer the question everyone forgets: if a migration ran, does rolling back the code leave the schema ahead of it? If yes, that is fine — but it belongs in the runbook, in advance, in daylight.
Coach’s Note — Test the runbook the cruel way. Break your own system on purpose — stop the database, revoke the API key, fill the disk — then follow your own entry to recover without improvising. Three deliberate breakages teach you more than three hours of writing. Rehearse the rollback on a calm Tuesday; it is twenty minutes then and impossible on a bad Friday.
7.5 — Configuration and Secrets, Separated From Code
One architectural rule this week, old and non-negotiable: configuration lives in the environment, not in the code. The clearest statement of it is Factor III of the Twelve-Factor App methodology (https://12factor.net/config), and the test it proposes is the one to remember — could you make this repository public right now, this minute, without leaking a single credential? If not, your config and your code are tangled. Configuration is anything that differs between where you develop and where it runs: database URLs, ports, hostnames, feature flags, log levels, API keys, model identifiers. Not “things that might change” — things that differ by environment.
The artifact that carries it is .env.example, committed, and it is a contract. Most students ship four naked lines — DB_URL=, API_KEY=, SECRET=, PORT= — which answer no question a stranger has. What is SECRET for? Where do I get API_KEY? Which can I leave blank? The version that does the job:
# PantryPilot — configuration contract. Copy to .env and fill in. Every variable here is
# read at startup; the app refuses to boot if a REQUIRED one is missing. .env is gitignored.
# REQUIRED — Postgres connection string. Local default after `docker compose up -d db`:
# postgres://pantry:pantry@localhost:5432/pantry
DATABASE_URL=
# REQUIRED — signs session cookies. Generate a fresh value PER ENVIRONMENT:
# openssl rand -base64 32 ... changing this logs everyone out. Intended, not a bug.
SESSION_SECRET=
# OPTIONAL — recipe suggestions. If unset, the feature is hidden and the rest of the app
# works normally. Usage is billed to whoever owns this key. Pin the version deliberately;
# never float to a "latest" alias — see docs/adr/0009.
RECIPE_MODEL_API_KEY=
RECIPE_MODEL=<provider>:<exact-model-version-string>
Every variable answers four questions: what it does, required or optional, where to get it, and a safe local value. That is the difference between a file and a contract. Then enforce it in code — a startup check that names the missing variable and exits beats an hour of your successor’s confusion. code/handoff_audit.py checks the contract against a real environment and flags the dangerous case: a variable your code reads that the contract never mentions.
On secrets, three rules and no exceptions. First, a secret that reached the repository is burned — not “probably fine, the repo is private.” Rotate it; rewriting history is optional cleanup you do after rotating. Second, scan before you tag: grep is a start, dedicated scanners such as gitleaks and trufflehog are better, and platform-side secret scanning is worth enabling if it is available to your repository (availability and terms change, so check current documentation rather than a blog post). Third, production secrets live in the host’s secret store — not in a file you copy up, not in a CI log, not baked into an image.
Coach’s Note — The most common way a student capstone leaks a key is not carelessness. It is a
.envcommitted in Week 1, before there was a.gitignore, and forgotten. Go look at your first ten commits. Today. Before you tagv1.0.0on top of them.
7.6 — Scripted Setup or Containers: Pick One and Prove It
You need one documented path from fresh clone to running. Exactly one. Two half-built paths is worse than one that works, because your successor will pick the broken one.
Scripted setup (script/setup) | Containers (Dockerfile / compose.yaml) | |
|---|---|---|
| Promises | ”Run this on a supported machine and it works" | "Run this anywhere the runtime exists and it works” |
| OS differences | Poorly — you will write branches | Well, by construction |
| Services (DB, cache) | You document installing them | You declare them and they start |
| Cost to your successor | They need your OS and your tool versions | They need the container runtime, and that is all |
| Failure mode | Silent drift; works for you, not them | Image size, build time, file-mounting confusion |
Recommendation, committed to: if your project needs a database or any second service, containerize it — declaring Postgres in a compose file is fifteen lines and deletes a category from your successor’s first day. If your project is a single-process tool with no services — TraceLens, this book’s command-line log analyzer, is exactly that — a scripted setup plus a committed lock file is honest, faster, and better matched to how the tool is actually installed. Do not containerize a CLI to look sophisticated. Do not hand-wave a multi-service web app with “install Postgres.”
A bad setup script is two commands you already knew: npm install then npm start. It checks nothing, says nothing when it fails, and starts the server instead of setting up, so it can never be run twice. A good one:
#!/usr/bin/env bash
set -euo pipefail # fail on error, on unset variable, and inside pipes
need() { command -v "$1" >/dev/null || { echo "MISSING: $1 — see README.md prerequisites"; exit 1; }; }
need node; need npm; need docker
node_major=$(node -p 'process.versions.node.split(".")[0]')
[ "$node_major" -ge 20 ] || { echo "Node 20+ required; found $(node -v)"; exit 1; }
[ -f .env ] || { cp .env.example .env; echo "created .env — fill it in, then re-run"; exit 1; }
docker compose up -d db # idempotent: safe to run twice
npm ci # exact versions from the lock file, not 'npm install'
npm run db:migrate && npm run db:seed -- --demo
echo "Setup complete. Run 'npm run dev', open http://localhost:8080, sign in as demo@example.com"
Four properties separate them: it fails fast with a readable message, it checks prerequisites instead of assuming them, it is idempotent, and it ends by telling you the next command and what you should see. The version numbers are illustrative — pin what your project requires, and make sure the number in the script matches the number in README.md. Note the hybrid: one script as the single entry point, delegating services to a compose file. That is the common professional shape and it counts as one path, because a stranger still runs one command. Then delete the other. The abandoned Dockerfile from Week 5 that no longer builds is not a spare tire; it is a trap, and it will cost your successor two hours before they conclude the project is dead.
7.7 — Versioning, Tagging, Licensing, and the Release You Can Point At
Until now your project has been a moving branch, and a branch is not something you can point at. This week it becomes v1.0.0: a fixed, named, immutable thing a grader, a successor, or an employer can check out and run. Use semantic versioning (https://semver.org/) — MAJOR.MINOR.PATCH. For a capstone, 1.0.0 is the honest label for “the scope I committed to in the specification, delivered.” If you knowingly shipped less, 0.9.0 is a more honest number and costs you nothing — the Week-8 rubric rewards the accurate claim, not the impressive one. Tag it annotated, so it carries an author, a date, and a message: git tag -a v1.0.0 -m "v1.0.0 — pantry tracking, expiry dashboard, barcode add", then git push origin v1.0.0.
Then publish a release on that tag with notes you wrote. Your platform can draft notes from merged pull requests; treat that draft as a first pass, because it lists commits and your reader wants changes. CHANGELOG.md is where the human-readable history lives, in Keep a Changelog shape (https://keepachangelog.com/). “Lots of bug fixes / final version / cleanup” tells a grader nothing and can be checked against nothing. This can:
## [1.0.0] — 2026-04-24
### Added
- Expiry dashboard sorted by days-to-expiry, with a 3-day warning band (FR-014, FR-015).
- Add an item by barcode scan in a single step (FR-007).
### Changed
- Pantry list paginates at 50 items to hold NFR-P2 (p95 page load under 1.5 s with 500
items; measured 1.1 s — see docs/test-results.md).
### Fixed
- Duplicate items created when the lookup endpoint timed out and the client retried (DEF-031).
### Known issues
- Recipe suggestions fail silently instead of surfacing an error on HTTP 429
(DEF-044, deferred — see docs/handoff.md §4).
### Upgrading
- Requires migration 0011; run `npm run db:migrate` before starting v1.0.0.
Every line ties to something a reader can look up. That is the payoff for six weeks of identifiers. And a Known issues section makes a release look more professional, not less — every real release has them. What looks amateur is a changelog implying nothing is wrong followed by a demo where something obviously is.
Deploy from the tag, never from your working copy. If the running thing came from a directory on your laptop, nobody — including you — can say what is in it. That is the entire reason tags exist. What “deployed” honestly means depends on the shape of your project: a public URL for a web app, a published installable artifact for a CLI or library, a scheduled workflow for a batch job. Do not host a CLI to look impressive; packaging is deployment for a command-line project. Do not fabricate free-tier arithmetic either — providers change limits and prices constantly. Do the arithmetic yourself this week, write it in docs/handoff.md, and date it. Appendix A walks the cheapest paths that work, including the no-admin ones. And if you genuinely cannot deploy — sensitive data, a hardware dependency, an institutional rule — that is a legitimate constraint, graded as one if you argue it in writing. The compensating deliverable: a one-command local run a grader can execute, a recorded demo, and a plain statement in the runbook of what hosting would require and cost. What is not acceptable is silence.
Licensing runs in two directions. Outward: a repository with no license file is not “open.” Under default copyright you retain all rights and nobody may legally copy, modify, or redistribute it. Know the one exception precisely, because people get it wrong in interviews: making a repository public on GitHub accepts GitHub’s terms, which let other GitHub users view and fork it within GitHub. That is the entire grant. “Public” is a visibility setting, not a permission grant.
| Option | Others may | Reasonable when |
|---|---|---|
No LICENSE | Look, and fork within the platform. Nothing else. | You have an institutional or commercial reason — write it down |
| MIT | Use, modify, redistribute, commercially; must keep your notice | Maximum reuse, minimum friction — the common capstone default |
| Apache-2.0 | Roughly the same, plus an explicit patent grant | Patent-adjacent work, or you want the extra clarity |
| GPL-3.0 | Use and modify; distributing derived works triggers source obligations | You want downstream work kept open — understand it before choosing |
Use SPDX identifiers (https://spdx.org/licenses/), put the identifier in README.md and the full text in LICENSE. Two honest warnings: this is orientation, not legal advice, and your institution may have a policy about who owns coursework. Ask your program in writing before you publish and keep the answer. Ten minutes now versus a real problem later. Inward: every dependency arrived with a license, and most impose at least one obligation — commonly shipping the notice with any distribution. Run your ecosystem’s license lister and put the inventory in docs/handoff.md with the real obligation named per row. Do not take a model’s word for what a package is licensed under; assistants are wrong about this specific fact often, and the failure is invisible until it matters.
7.8 — The Handoff Package and the Knowledge Only You Have
Here is the mental model. Imagine you are hit by a bus tonight. (The industry really does call it that.) Tomorrow morning a competent engineer who has never seen your project is told to take it over. They cannot call you. What do they need? Not “the code is well commented.” That sentence, and its cousin “everything is in the README,” is the most common handoff document students write, and it is worth zero: it asserts quality instead of demonstrating it, points at a document instead of adding to it, and offers an escape hatch — email me — that is precisely the thing a handoff assumes is unavailable. Start from code/handoff-template.md and land it as docs/handoff.md. Six parts:
- The day-one path. A numbered table: step, exact command, expected result, what to do when it fails. Target: running in under thirty minutes. It overlaps the README on purpose — a successor should never reconstruct a sequence from two documents.
- The week-one ramp, ending in a merged change. Read the architecture overview. Trace one request end to end with the files named in order. Then fix a small, real issue you deliberately left open and labeled
good-first-issue, acceptance criteria already written. A successor who ships something in week one has joined the project; one still reading in week three has not. This section is your onboarding guide. - The ownership table. Every account, key, host, and recurring cost: where it lives, who owns it today, what it costs, and the exact steps to transfer or rotate it. Every row with your name in it is a liability, and the handoff is not finished until those names change.
- The landmines. At least five things that look like bugs and are not, or look fine and are not. Symptom, actual cause, what to do. This section saves the most hours, because the knowledge exists nowhere else — not in code, not in tests, not in the specification. Only in you.
- Licensing, in and out, with the third-party inventory table.
- The AI and vendor dependency section, below.
Bus factor is the number of people who would have to disappear before a project cannot continue. Yours is one — you are the only engineer. But the severity of that one is entirely in your control, measured by how much of the project lives only in your memory. Spend forty minutes hunting it with one question: what have I had to remember instead of read?
| Knowledge only you have | How it bites your successor | Minutes to write | Where it goes |
|---|---|---|---|
| The seed script must run before the first login, or the roles table is empty and every request 500s | Hours debugging an auth system that is fine | 10 | docs/runbook.md, plus a guard in the script |
| The barcode API returns HTTP 200 with an empty body for unknown codes | They “fix” a bug that is the vendor’s behavior | 15 | docs/architecture.md and a regression test |
| Migration 0007 assumes 0006 already ran; out of order it drops a column | Data loss | 20 | An ADR plus a note in CHANGELOG.md |
Under an hour of work against consequences running from a wasted week to destroyed data. Close each item three ways, in this order of preference — make it impossible (a guard, a test, a loud failure), make it visible (runbook or architecture doc), make it findable (the landmines list). Writing it down is the floor, not the ceiling. Backlog hygiene belongs here too, and it is cheap points people leave on the table. “Search is broken” cannot be picked up by you in a month. This can: “DEF-052 — Search ignores the expiry filter when the query is empty. Repro: clear the search box with ‘Expiring soon’ active; all items return. Expected (FR-012): the filter applies regardless of query text. Likely the early return at the top of buildFilter in the search module. Done when the added test passes and the manual repro no longer reproduces.”
The AI dependency you are handing over. If your system calls a model, you are handing your successor something none of your other dependencies is: a component that can change without your code changing. Your lockfile pins your libraries; your container pins your runtime; a hosted model behind an API is pinned by none of that. Six things, in docs/ai-usage.md and summarized in the handoff guide: what the feature does and what it does when it fails (“suggests up to three recipes; on error or timeout the panel is hidden and the rest of the page works”); the exact model identifier and the date you pinned it, quoted from config, never a floating alias; where the prompt lives — a file path, because prompts are source code and belong in a diff, not in a string literal three call frames deep; key and cost ownership, including the blunt question who pays after you graduate (if the answer is nobody, the honest design degrades gracefully with the feature off, and you verify that it does); deprecation, because you should assume your pinned version is retired while your successor owns it; and what must not be assumed stable, plus the fixture set and the command to re-run after any model change. Without that last one, “we upgraded the model” is a change nobody can assess. If your project calls no model, do all six for your most volatile third-party dependency — the payment API, the map service, the auth provider, the site you scrape.
The reflex when you write that register is embarrassment — I should have written this down in Week 3. Skip it; it costs time and buys nothing. Every professional discovers this list on the way out the door. The difference between a professional and an amateur is only that the professional writes it down anyway, at the end, instead of leaving with it.
7.9 — The Assistant Drafts. You Run Every Command.
Be honest about the tool in both directions. An assistant is genuinely good at documentation and at the shape of infrastructure: give it your repository and you get a well-structured README, a plausible container definition, a CI workflow, in less time than it takes to make coffee. Refusing that help in a 160-hour course is not rigor; it is waste. Use it. Now the failure mode, precisely stated. A model generating setup instructions is inferring, not observing. It sees a compose file and infers a command. It sees PORT in your config and infers the default. Those inferences are usually right, which is what makes them dangerous: a document that is 90% correct reads exactly like one that is 100% correct, and the reader who hits the wrong 10% cannot tell whether the error is in your docs or in their machine. They will assume it is their machine. They will burn an hour. Generated infrastructure fails the same way — the shape is right and the specifics are stale or invented: base-image tags that never existed, a package name off by a hyphen, a flag removed two major versions ago. There is a documented supply-chain risk in that neighborhood too: models sometimes suggest package names that do not exist, and attackers register them. Verify that every package you are told to install is the one you meant, from the registry you meant.
| Give the assistant | Never take without running it |
|---|---|
| Structure and section order | Any command |
| Turning rough notes into clear prose | Any version number or file path |
| Candidate troubleshooting entries from your defect log | Any environment variable name |
| Gap-finding: “what would a new user still not know?” | Any port, URL, or expected output |
| Tightening a wordy runbook step | Any claim about free tiers, pricing, or licenses |
The prompts that earn their keep are the gap-finding ones, because that is where a model beats you — it does not share your curse of knowledge. “Here is my README. You are a competent engineer who has never seen this project. List every question you still cannot answer, and every step where you would have to guess.” That is worth an hour of your own re-reading. The verification protocol then has exactly one rule: every command in your documentation gets executed by you, in order, from a clean state, and its real output pasted in. Not skimmed. Executed. Then log it — docs/ai-usage.md gets a row naming the tool, what it drafted, and, the part a grader actually reads, what you found wrong when you ran it. Do not round it down. “3 of 11 commands required correction” is the most credible sentence in your entire submission.
Coach’s Note — The trap is not that the assistant lies. It is that it writes your documentation in a confident voice you did not earn. When you paste a generated section unverified, you are vouching for it with your name. Verification is not paranoia; it is the price of the byline.
7.10 — The Clean-Machine Test: Prove It, Do Not Claim It
Here is the week’s centerpiece, and it is not a document. It is an experiment, run twice, recorded in one file. Copy code/clean-machine-protocol.md into your repository as docs/clean-machine-test.md and fill it in during the runs, not afterward from memory. Run A — reproduction. You, following your own documentation literally, on an environment that is not your development machine: a fresh container from a bare base image, a fresh cloud development environment, a re-imaged lab machine, a borrowed laptop. This one catches “I never committed that file” and “that tool has been on my laptop since sophomore year.” Record where, when, how long, and what broke the first time — because something always breaks the first time, and the fix is the interesting part. What earns the points is a transcript like this, not the sentence “tested on a clean machine and it worked fine”:
Clean-environment run — 2026-04-22, 14:05–14:31
Environment: fresh cloud dev container, blank image, no local tooling
Attempt 1: FAILED at step 3. `npm ci` errored — package-lock.json was gitignored
since Week 2. Committed it (commit a91c4f2).
Attempt 2: FAILED at step 5. Migrations ran; the seed script silently did nothing
because SEED_DEMO defaulted to false. Made it explicit in .env.example.
Attempt 3: PASSED. Clone → running: 9 min 40 s. Smoke test: 34 tests, 12 s.
That transcript is worth more than the working setup itself, because it proves the claim instead of asserting it — and the two defects it found are two afternoons your successor will never lose.
Run B — the stranger. A person who is not you, on a clean environment, using only what is in the repository, while you sit silently and take notes. Six rules, every one of them because students break it:
- A clean environment. Nothing your project installed is already there. Appendix A lists the options, including the no-admin browser path.
- A real tester who is not you. A classmate, a roommate, a sibling who codes. Competent, uncoached.
- You are silent. You may watch. You may not speak, point, type, or fix. The instant you help, the run is void — the help you gave is the documentation you are missing.
- The docs are the only input. They get the repository URL. No verbal preamble, no “oh, you also need to…”.
- The clock runs from opening the repository to completing the acceptance action. Stop at sixty minutes and record the failure honestly.
- Every stumble gets logged, including the ones you think are the tester’s fault. Especially those — “the user did it wrong” is a documentation finding wearing a disguise.
Rule 3 is the one that hurts. Watching a competent person fail to run software you wrote, in silence, while a clock runs, is genuinely uncomfortable. Sit in it. That discomfort is the most efficient documentation feedback that exists. Define your acceptance action before the run: the one concrete thing that proves the system works, not that it started. “The server started” is not it. “Added an item with an expiry date and saw it appear in the Expiring Soon list” is. For TraceLens it is “ran the tool against the sample log in the repository and got the expected anomaly count.” Same protocol, very different projects.
What you will find, roughly in this order, because everyone finds these: a prerequisite you never wrote down; a configuration step that lives only in your fingers; a command whose output looks like an error but is not; a step that assumes the previous step’s directory; a seeded database with no seed script; a variable that fails silently when unset. Then the part that converts an experiment into a grade: fix, re-run, record. Every friction entry becomes a documentation change, and then you run it again and record the second time-to-acceptance. Two numbers, before and after. Milestone 7 grades an honest record of a real run far more generously than a suspiciously clean one, and a run where the tester needed help is a finding, not a failure. Hiding it is the only actual failure available to you this week. In Week 8 you will run this one final time as the submission’s acceptance test — so a bad result now is cheap, and a bad result then is not.
Coach’s Note — When your tester gets stuck, resist the sentence forming in your mouth. Write it down instead. That exact sentence is the one missing from your README, and you now have it in the reader’s own words. Every silent minute buys you a line of documentation you could not have written alone.
7.11 — Interactive Lab: The Handoff Readiness Auditor
Below this chapter on the website is The Handoff Readiness Auditor. Use it twice: once on Monday before you fix anything, once on Friday — the delta is the point. Part one grades your README section by section against the nine-section standard a grader applies — what it is, prerequisites with versions, install, configure, run, test, layout, troubleshooting, license. Paste yours in exactly as it stands right now, no cleanup, no cheating. It flags the exact failures this chapter has been naming: unstated prerequisites, commands with no expected output, hard-coded personal paths, “just run it” hand-waving. Then it projects a stranger’s time to first successful run, which is the number that stings.
Part two scores handoff readiness across five areas — environment reproducibility, configuration and secrets, documentation completeness, issue and backlog hygiene, and knowledge that exists only in your head. Each question wants evidence, not intent: not “is setup documented?” but “when did somebody who is not you last run it, and how long did it take?” Answer honestly; the tool is useless if you grade yourself on what you plan to do this weekend. It renders your successor’s projected first week — what they get stuck on, in what order, for how long — and a prioritized remediation list ordered by hours saved per hour spent. That list is your work queue for the rest of the week, and it is not the same list for everyone.
Then run the mechanical pass the widget cannot do on your local machine, with code/handoff_audit.py: python3 handoff_audit.py .. It checks the nine README sections, hard-coded home directories, hand-waving phrases, unpinned prerequisites, anything that looks like a real secret, whether your .env.example documents every variable, and whether this edition’s required artifacts exist at their canonical paths. It exits non-zero on errors so you can wire it into continuous integration. A clean report is necessary and nowhere near sufficient — no linter can tell you whether a person can follow your instructions. Only a person can.
7.12 — Where Your Hours Went This Week
Twenty hours, honestly budgeted — and four of them were committed before the week started. Log them in docs/hours-log.csv as you go; Week 8 asks for the record, and reconstructing it on the last Sunday is a miserable, inaccurate job.
| Activity | Hours |
|---|---|
| Reading this chapter, the auditor lab, the weekly quiz | 1.0 |
| README rewrite: nine sections, every command run and its real output captured | 2.5 |
| Architecture overview §0 plus the decision-record index | 1.5 |
| Runbook: operations, normal readings, four failure entries, rollback rehearsed and timed | 2.5 |
| Configuration contract, secret sweep through history, the one setup path | 2.5 |
Deploy (or the argued equivalent), verify, tag v1.0.0, release notes, change log, license | 2.5 |
| Handoff guide, backlog grooming, bus-factor register | 1.5 |
| Clean-machine test: both runs, friction log, fixes, re-run | 2.0 |
| Subtotal — documentation, deployment, handoff, clean-machine test | 16.0 |
| Deferred defect fixes carried from Week 6 — the protected reserve | 4.0 |
| Total | 20.0 |
The reserve row is not slack, and it is not a rounding error. It is the four hours you promised yourself in Week 6, when you ordered the fix queue against ten hours — six left in that week plus these four (Chapter 6 §6.5). That is why the sixteen hours above are lean: the reserve was carved out of them before you read a word of this chapter. Spend it Monday and Tuesday, on the deferred defects in the order you already decided, and close them before the documentation work starts — a fix competing with a Friday deadline loses, and then you ship a defect you had already written down as one you would fix. If the queue is genuinely empty, the four hours go straight back into the README and the runbook, which will absorb them without effort. What the reserve may never fund is a new feature. That decision was made in Week 6 and Week 7 does not reopen it.
If your README rewrite takes forty-five minutes, you did not rewrite it — you tidied it. Most of those two and a half hours go to running commands and pasting real output, which is where the value is. If deployment eats six hours, stop and switch to the argued-constraint fallback; it is worth far more than six lost hours chasing a platform.
7.13 — What Does It Mean to Entrust Your Work to Another?
“…what you have heard from me in the presence of many witnesses entrust to faithful men, who will be able to teach others also.” (2 Timothy 2:2, ESV)
Read that verse as an engineer for a moment, because among other things it is a transfer specification. Paul is writing near the end of his life, and he is not asking Timothy to keep what he received. He is asking him to hand it on — and note the chain: Paul to Timothy, Timothy to faithful people, those people to others still. Four links. The thing entrusted is designed from the beginning to survive the person carrying it. That is a remarkable engineering property, and it is not an accident of the sentence; it is the point of it. There is a real distinction worth naming. To publish is to make something available. To entrust is to place something into another’s care, on purpose, prepared for them, with their capacity in mind. Publishing is one-way and costs the publisher nothing. Entrusting is a relationship, and it makes demands: you have to know something about who is receiving it, you have to prepare it for hands that are not yours, and — this is the hard part — you have to let go of controlling what they do with it. Every artifact this week is the second thing rather than the first. The README is written for someone whose knowledge you had to imagine. The runbook is written for someone under pressure whose mistakes you had to anticipate. The decision index preserves the reasoning rather than the conclusion, because your successor may need to reverse your decision and cannot do that responsibly without knowing why you made it. Notice what that means: you are documenting your decisions so that someone can undo them well. That is not the instinct of an author protecting a work. It is the instinct of someone who cares more that the thing continues than that they were right.
The other half of the week has an older complaint attached to it, and it deserves an honest hearing. Ecclesiastes 2:18 — “I hated all my toil in which I toil under the sun, seeing that I must leave it to the man who will come after me” (ESV). That is not a pious thought; it is a man in a bad mood who did the arithmetic on who inherits his work and hated the answer. Anyone who has watched a good system inherited by someone who did not care about it knows exactly what that sentence tastes like. You are about to spend sixteen hours making your work easy to take away from you, and Scripture does not pretend that is a comfortable thing to spend a week on. But notice what the Preacher’s despair rests on: the assumption that the point of the work was that he keeps it. Ownership. Control. The permanence of the builder’s grip. Take that assumption out and the same facts read differently. Paul, writing about a different kind of building, says it as plainly as an engineer could want: “like a skilled master builder I laid a foundation, and someone else is building upon it. Let each one take care how he builds upon it” (1 Corinthians 3:10, ESV). Someone else is building on it. Stated as fact, not tragedy. The only question left to the builder is how carefully he laid it for them.
This is vocation in the ordinary Lutheran sense, and it is the frame this course has run on since Week 1. Your work is not primarily a monument to you; it is service rendered to a neighbor through the station you happen to occupy. This week the neighbor gains a face: your neighbor is also your successor — the engineer who opens this repository at nine in the morning a year from now with a bug report in one hand and no way to reach you. Every undocumented landmine you leave costs that person a day of their life. Every hour you spend this week buys them back a day. That is not paperwork. That is love of neighbor performed in Markdown, and it is the most concrete form of it this course will ever ask of you.
Which is where the clean-machine test stops being a rubric line and becomes something closer to an examination of conscience. Sitting silent while another person struggles with what you built is a small, exact picture of the whole thing: your work, in someone else’s hands, your intentions counting for nothing next to what you actually prepared. You cannot explain. You cannot help. All that reaches them is what you wrote down. Every stumble is a place where you served your own convenience instead of theirs — not maliciously, just by not imagining them. The remedy is not guilt; it is the friction log, and then the fix.
There is a humility in this that runs against a strong instinct, and it is worth saying plainly: you are making yourself unnecessary on purpose. Every instinct in an insecure engineer runs the other way — to be the only one who understands it, to be indispensable, to hold the knowledge that makes you hard to replace. That is job security purchased with somebody else’s time, and it is a small, ordinary form of pride. The mature move is the opposite: write it all down, hand it over completely, and let the work stand without you. Eight weeks is not long enough to get attached to something, and it will happen anyway. Hand it over as one who was always a steward and never an owner. “One who is faithful in a very little is also faithful in much” (Luke 16:10, ESV). This week is the very little. Be found faithful in it.
7.14 — Common Pitfalls
Pitfall: The README documents the project’s ambitions instead of its current state. Example: “PantryPilot suggests recipes, tracks nutrition, and syncs across devices” — when two of those three do not exist and one of them was cut in Week 6. Fix: A Status line and a What it does not do section, both above the fold, describing today’s build. Aspirations belong in the backlog, not the front door. A reader who trusts your non-goals will trust everything else you wrote.
Pitfall: Documentation inherited from a Fast-Start brief that quietly describes somebody else’s project. Example: The README’s non-goals are the catalog’s non-goals; the architecture overview describes the brief’s suggested stack rather than the one you actually built; the feature list still has the requirement you dropped in Week 6. Fix: Read every sentence and ask did I write this, or did I inherit it? Adapting beats authoring, but only when you actually adapt. Then write the ADR recording where and why you deviated from the brief — that is your unrecorded decision.
Pitfall: “Just run the install command and you’re good to go.”
Example: A Setup section that is three imperative sentences with no commands, no expected output, and no failure path.
Fix: Every step gets a literal command in a fenced block, a stated expected result, and — for the steps that commonly fail — what the failure looks like. code/handoff_audit.py flags this class of hand-waving by name.
Pitfall: A runbook written from imagination rather than from the defect log.
Example: Generic entries — “if the service is slow, check resource usage” — for failures that have never once occurred, while the two outages that actually happened in Week 6 are absent.
Fix: Open docs/defect-log.md and convert real defects: symptom, first check, likely cause, fix, verification, escalation. Then break your own system deliberately and follow your own entry to recover.
Pitfall: Two half-finished setup paths, and a README that mentions both without saying which is current.
Example: A Dockerfile from Week 5 that no longer builds sits beside a working script/setup. The successor picks the container path, loses two hours to a build error, and concludes the project is abandoned.
Fix: Pick one, make it work, delete the other, and say in one sentence which path is supported. A broken alternative is a trap you left behind.
Pitfall: A clean-machine test where you helped. Example: “It mostly worked — I just had to tell them about the database step and fix one path for them.” Fix: That run is void, and the two things you said out loud are your two highest-priority documentation fixes. Patch the docs and run again with a fresh environment. Graders trust an honest record with friction in it far more than a flawless one.
7.15 — Reps
The reps are in the exercises, and this week they are the milestone taken in pieces. Work them Monday through Thursday and Milestone 7 is mostly assembled by Friday. Preview:
- Rep 1 — score your README against the nine-section standard, honestly, before you touch it.
- Rep 3 — rewrite
.env.exampleas a contract and check it withcode/handoff_audit.py. - Rep 5 — four runbook entries mined from your defect log, one naming a destructive command.
- Rep 8 — tag a release candidate, write the notes yourself, then rehearse the rollback on a timer.
- Rep 10 — let an assistant draft your troubleshooting table, run every command it wrote, and count the wrong ones.
Then take the on-page Check Your Reps quiz — one of the eight weekly quizzes that make up 15% of your grade, and your early warning if this week’s ideas have not landed.
7.16 — This Week’s Milestone
Milestone 7 — Milestone 7: Documentation Set, Deployable Release & Handoff Package. The full set: README.md to the nine-section standard, docs/architecture.md §0, docs/runbook.md with deploy and a rehearsed rollback, docs/handoff.md, docs/adr/README.md, the successor-facing section of docs/ai-usage.md, a configuration contract with no secrets in the history, one proven setup path, an annotated v1.0.0 tag with release notes and a CHANGELOG.md entry, a LICENSE, and docs/clean-machine-test.md recording both runs.
These are not extra work stacked on the final submission — they are lines in the Week-8 rubric, produced in the week that has hours for them. The milestones are graded twice for exactly this reason: skipping this week does not save you points, it forfeits the milestone and moves the rest to Week 8, where there is no time left to earn them. See Appendix D for the contract and Appendix C for the templates.
7.17 — Coach’s Final Word
For six weeks you have been the only person who could run this thing. That was fine — necessary, even. You were building. This week that ends, and it should. A project only one person can run is not finished software; it is a private skill with a repository attached. The moment a stranger clones your work and gets it running without you in the room, it stops being a demonstration of what you can do and becomes a thing that exists on its own.
Do the unglamorous parts. Rehearse the rollback even though nothing is broken. Write the five landmines even though you remember them perfectly. Rotate the key even though the repository is private. Nobody applauds any of it, and all of it is the difference between a project that survives you and a project that ends when your laptop does. And you will not enjoy the clean-machine test — nobody does. You will sit on your hands while a competent person fails at something you find trivial, and every instinct will scream at you to reach over and type. Don’t. That hour of silence is worth more than the ten hours of writing before it, because it is the only hour that tells you the truth. Then fix what it found and run it again.
One week left. Next week you tell the truth about all of this in thirty minutes and hand it in. This week, you make it explainable, runnable, and somebody else’s. Write it down. Prove it works.
See you on Monday.
Up next: the exercises turns this week into eleven reps · Milestone 7 is Milestone 7 · then Chapter 8 — Deliver: the final submission, the thirty-minute presentation, and the retrospective. Previous: Chapter 6. Reference: Appendix A (workbench and clean environments), Appendix B (Fast-Start Catalog), Appendix C (document kit), Appendix D (grading contract), Appendix E (glossary).
Week 7 Knowledge Check
## Prerequisites
Node and Postgres. You'll need an API key too.
Set up the database, then you should be pretty much good to go. node --version); PostgreSQL 16.2 (psql --version)" and "Without it, scan returns 503 and manual entry still works." Prerequisites are where your machine lies to you — you have been quietly configuring that laptop for years. Stand in the directory where the project works, print real version strings, and write tested on, not requires: you know what worked, not the full range that works. Then hunt the personal with grep -rnE "/litman-books/Users/|/home/[a-z]|localhost:[0-9]{4}|127\.0\.0\.1" README.md docs/, and go find the invisible accounts — the API key, the database you made by hand in a GUI in Week 3, the OAuth app under your personal address. Nothing installs for those. And "pretty much good to go" is the author's opinion about the reader's next twenty minutes; handoff_audit.py flags that class of hand-waving by name. docs/runbook.md. What does §7.4 say is wrong with it?### Database issues
If the database has problems, check the connection and restart the
service if needed. Make sure your credentials are correct. docker compose down -v — that deletes the volume"), because that line is what separates a runbook from a wiki page: it tells a frightened operator which action is irreversible. Then test it the cruel way — break your own system on purpose and recover using only your own entry, without improvising. .env file, before the repository had a .gitignore. What does §7.5 say you do first?gitleaks and trufflehog are better, and platform-side secret scanning is worth enabling if it is available to your repository. Production secrets live in the host's secret store — not in a file you copy up, not in a CI log, not baked into an image. The Coach's Note names the usual culprit exactly: the most common way a student capstone leaks a key is not carelessness, it is a .env committed in Week 1 before there was a .gitignore, and forgotten. Go look at your first ten commits before you tag v1.0.0 on top of them.