This module builds no feature. Twelve modules each added one thing to the chat workbench and tagged it, a tag is a name git puts on one exact commit, so module-32 is the whole app as it stood when module 32 was done, and no further. This last module gives you three things that turn twelve tagged commits into one project you can hold: one command that stands the app up and proves it works, a walk through the tags so you can see each piece arrive, and an index that ties every choice the code makes back to the record that explains it.
Start with the whole picture, because after twelve modules you have twelve local views and no single one. The app is a browser and a small server talking over one long-lived connection (module 29). It runs its agents under a closed configuration that ignores your personal Claude setup (module 30), gives each agent its own process and persona (module 31), and lets them mail each other at a turn boundary (module 32) under caps only a person resets (module 33). It keeps a knowledge base the agents write by hand (module 34) with skills that keep it honest (module 35), notes per agent (module 36), and a reader that keeps the main window small (module 37). Hooks hold the agents inside the lines (module 38), a two-axis policy decides what needs a person’s sign-off (module 39), and the whole repository is a plugin you can load anywhere (module 40).
One command stands it up and certifies it
The way in is a single script. It does not replace the real tools, npm run check, npm test, and the plugin validator are still what does the work. It runs them in order and stops at the first failure, so one green run means the whole app is sound on your machine:
node scripts/install.mjs
Read scripts/install.mjs . Two properties make it safe to lean on. It is idempotent, a word meaning running it twice is the same as running it once, because every step checks before it acts, and it is read-only against tracked files, so it never edits anything git is recording. It only installs dependencies, which are untracked, and runs the suites.
scripts/install.mjs Sequences the real tools and gates on each. Idempotent and read-only against tracked files.
#!/usr/bin/env node
// scripts/install.mjs, stand the chat-workbench up and certify it, in one command.
//
// The contract (see docs/decisions/0173):
// - Idempotent. Every step is a check first; work happens only when the check
// says it is missing. Running this twice does no harm and repeats no install.
// - Read-only against tracked files. It writes nothing the repository tracks.
// - It gates. Node too old, a failing check, test, or validate each stops the
// run with a non-zero exit and a named reason.
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const failures = [];
// A .cmd shim (npm on Windows) is refused by spawn unless it goes through a
// shell, so shell out for those and run a real executable directly.
function run(cmd, args) {
const shell = process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd);
const r = spawnSync(cmd, args, { cwd: repoRoot, stdio: "inherit", shell });
return r.error ? 127 : (r.status ?? 0);
}
// 2. Dependencies. Present already means done, the idempotent step.
if (existsSync(join(repoRoot, "node_modules"))) {
console.log(" node_modules present, nothing to install");
} else {
const cmd = existsSync(join(repoRoot, "package-lock.json")) ? "ci" : "install";
if (run(npm, [cmd]) !== 0) process.exit(1);
}
// 3-5. Check, test, validate, each gated onto failures.
if (run(npm, ["run", "check"]) !== 0) failures.push("check");
if (run(npm, ["test"]) !== 0) failures.push("test");
// ... step 5 runs the bundled offline validator; step 6 exits 1 if failures.length A full run on 2026-08-30, trimmed to the step markers and each suite’s totals, ends like this, every step exited 0:
[4/6] npm test
# tests 304 # pass 304 # fail 0 (server)
6 passed (client)
# tests 30 # pass 30 # fail 0 (tools)
# tests 17 # pass 17 # fail 0 (hooks)
[5/6] plugin validate --strict
✔ Validation passed
[6/6] Result
all checks passed
Watch it grow: the tag walk
Because each module is one tag, and each tag is the one before it plus a single thing, you can move through the build like frames of a film. Check out a tag and the repository is that module. Run the install script and you certify the app at that point in its life:
git checkout module-32 # the app as of module 32, and no further
node scripts/install.mjs # certify that tag
git checkout main # back to the finished app
The “adds” for each tag is read from the decision records that first appear at it, not from a plan written ahead of time. That is the whole tag walk, from the root at module-29 to the plugin at module-40:
| Tag | Adds |
|---|---|
module-29 | The root. One agent that stays alive: a browser and a server over one connection, one long-lived session fed by a queue, the turn boundary as the rule for when the next message is read. |
module-30 | A closed configuration: an app-owned config directory, project settings only, strict MCP config, the repository loaded as a local plugin. |
module-31 | A roster of agents: one process per agent, started by the first message, each with its own persona; the sidebar and the theme. |
module-32 | Agents that message each other: mail delivered into the recipient’s queue and read at its next turn boundary. |
module-33 | Caps only a person resets: a session ceiling that never decays, alongside a per-minute rate limit. |
module-34 | The wiki, by hand: a knowledge base with a provenance check on every entry. |
module-35 | Skills that keep the wiki honest: a capture skill and a freshness audit; a skill names the file to read, never the fact. |
module-36 | Each agent keeps its own notes: a scratchpad per agent, plus a record of what a turn actually did. |
module-37 | Keep the main window small: a reader subagent with no edit tools that returns at most three evidenced proposals. |
module-38 | Hooks that keep agents inside the lines: a fail-open bookkeeping hook and a Stop hook that will not loop. |
module-39 | Approval as data: a silent list and an outbound list as data, a typed confirmation for anything that leaves the machine, a write guard. |
module-40 | Ship the plugin: the manifest declares three surfaces, validated strict and load-certified. |
module-41, this module, adds no application code: INSTALL.md, the install script, the decision index, and its four records.
Every choice has a record
Twelve modules leave behind a pile of decisions, and the reason for each is one file in docs/decisions/. docs/DECISIONS-INDEX.md lists all 162 of them. Its trick is worth naming: the module each record belongs to is read from git, the first tag whose commit contains that file, not typed in by hand where it could drift. So the index cannot claim a record arrived at a module it was not actually written for.
docs/DECISIONS-INDEX.md One row per decision record, its module read from the first tag that contains the file.
# The decisions, indexed
Every choice made while building the workbench is one file in docs/decisions/,
with its reasoning. This lists them all, each row naming the module its record
belongs to, a module read from the tags, not from the record's prose.
162 records in all, the last four introduced at module-41 with this index.
| # | Title | Module | Introduced at tag |
| ---- | -------------------------------------------------------- | ------ | ----------------- |
| 0001 | The server owns the session, the browser owns the view | 29 | `module-29` |
| ... | (160 rows) | ... | ... |
| 0173 | The install script is a sequence of gates | 41 | `module-41` |
| 0176 | The track is complete: what the whole application is | 41 | `module-41` | The break: a tag is that module and no further
Here is the mistake most people make first with a tag walk. You check out an early tag to study it, then look for something a later module added:
git checkout module-33
grep -r "typed confirmation" server/ # nothing, that arrived at module-39
The typed confirmation for outbound tools is a module 39 idea. At module-33 it does not exist yet, because a tag is a photograph of one moment, not a view of the finished app with history attached. Nothing is broken and nothing is missing. You are looking at the app before that feature was built. The fix is to know which module added the thing you want (the tag walk above, or the decision index) and check out that tag or later. Then git checkout main to return to the whole app, where everything is present at once.
What it does not build, on purpose
A finished project is also defined by what it left out, and naming that stops you reading an absence as an oversight. The track does not build hosting or cost dashboards, a second programming language, a desktop wrapper, a nightly self-commit, or a mail system beyond the queue-and-wake design. The capstone runs on your machine and stops there. Each of those is a real next step, and each was left out so the one idea per module stayed one idea.
Does it transfer?
The tag-per-module method is the most portable thing here, and it has nothing to do with Claude. Any project you build in visible steps can carry a tag at each step, and the payoff is exactly what the tag walk showed: a new reader moves through the history one change at a time instead of facing the whole thing at once. The install-and-certify-in-one-command idea travels too: a script that checks, does not assume, and gates on real tools is good practice in any repository someone else will clone. And the decision index is the habit underneath both: write down why, next to what, so the project can explain itself after you have forgotten. That habit is the one this whole track was quietly teaching.