When the loop converges and the course is written, one last thing happens on its own: the course is published. Two independent tiers — a private link-only gist, and a hosted Cloudflare Pages site — give the work somewhere to live beyond the folder it was born in. The repo course/ stays the canonical home; publishing only adds reach.
Every loop ends the same way. The artifact converges — it actually meets the measurable "done" you set at the start — and a small, self-contained course is written to explain it. At that exact moment one last thing happens automatically: the course gets published, so it lives somewhere other than the folder it was born in. That final step is the Publish Gate.
Publishing happens in two tiers, and they are independent. The first tier is a private gist — a single, link-only snapshot you can hand to anyone, that opens as a navigable little site. The second tier is a hosted website on Cloudflare Pages — a real, live URL on a global network, kept private by a login gate. You can have one, both, or neither, and the system tells you exactly which tiers actually went out.
This is the one moment in the whole loop where the system reaches outward on its own — it creates something on the internet without stopping to ask. That sounds bold, so the rule around it is deliberately narrow: it is the only standing-authorized outward action, it only ever publishes the course you just made, and it skips itself the instant the situation says it shouldn't. Everything else the loop does stays on your machine.
Think of it like… finishing a book. The manuscript on your desk is the real, editable original — that never goes away. But once it's done, you also make two copies that leave the desk: a single bound proof you can mail to a friend (the gist), and a copy placed on a library shelf where anyone with a library card can read it (the hosted site). The desk copy is still home; the other two just let the work travel.
The Publish Gate runs on convergence, after the visual-teach course has passed its own Ship Gate. It is described in the loop-engineering skill as the general Publish Gate rule: on converging, besides delivering the course, publish it. Two scripts implement the two tiers — publish-course-gist.js and publish-course-pages.js — both invoked with node against the course directory.
The harness otherwise keeps its hands on your machine. Publishing is the single exception that is pre-approved (you do not get asked each run), and it is fenced on three sides: it publishes only the course artifact, it defaults to private/secret visibility, and it is skipped whenever the scope forbids publishing or the relevant CLI is unauthenticated. That fencing is what makes a standing authorization safe.
It helps to hold three places in your head, because they do different jobs.
The repo course/ directory is the original. It is where the course is authored and where every link already resolves. Nothing about publishing replaces it — it stays the canonical, navigable home for the course, and it is the place you keep editing.
The gist is a private, off-machine snapshot. Think of it as a frozen copy you can share with a link, without giving anyone access to your repo or your machine. It is always attempted, because it needs almost nothing to work.
The Cloudflare Pages site is the hosted tier — a genuine website with its own address, served from a fast global network, that you can keep private behind a login. It is the tier you reach for when you want people to simply open a link in a browser and read, with no download step.
The key idea: these are layers, not replacements. The repo is home; the gist is a portable copy; the hosted site is the public-facing reading room. Publishing adds reach without ever taking the original away.
A gist is strictly flat: its server rejects any push containing a subdirectory (pre-receive hook declined). The course tree is not flat — it has lessons/, demos/, and pt/. So publish-course-gist.js flattens the tree into collision-free top-level filenames and rewrites every internal relative .html link to its new flat name. The payoff: a downloaded ZIP or git clone of the gist opens as a fully navigable course, and because each page is self-contained (inline CSS/SVG/JS) individual pages also render standalone via gistpreview.
Cloudflare Pages preserves the directory tree and serves relative links natively, so it is a real navigable site on a CDN with no rewriting. That is the structural difference between the two tiers: gist = flattened private snapshot; Pages = tree-preserving hosted site.
The gist is the tier that almost always goes out, because it asks for so little. You point one script at the finished course folder, it bundles up every page into a single private gist, and it prints back a URL. The gist is secret — which on GitHub means link-only: not listed anywhere, not searchable, visible only to someone you hand the link to. That is as close to private as a gist gets.
What you get is a snapshot frozen in time. It does not track your repo, it does not change when you keep editing — it is the course exactly as it stood the moment you published. Anyone with the link can read it in the browser, or download the whole thing as a ZIP and open it offline, and it still works as a clickable course.
There is one small piece of plumbing worth knowing: a gist cannot contain folders. The course lives in folders. So before it uploads, the script quietly flattens everything into a single layer of files and rewrites the links between pages so they still point to the right place. You never see this — you just get a gist that behaves like the real course.
# run on convergence, pointed at the finished course directory node ~/.claude/skills/loop-engineering/publish-course-gist.js ./course # optional: give it a description; default visibility is SECRET (link-only) node ~/.claude/skills/loop-engineering/publish-course-gist.js ./course --desc "Loop Engineering — visual course" # requires the gh CLI to be authenticated first: gh auth status
~/.claude/skills/loop-engineering/publish-course-gist.js. It needs node and an authenticated gh CLI — check with gh auth status, and if it is not logged in, run gh auth login once. Then pass the course directory as the first argument. The default visibility is secret; you would only add --public in the rare case you actually want a listed, searchable gist.Because a gist rejects subdirectories, the script walks the course tree (lessons/, demos/, pt/), maps each nested path to a unique flat filename, and rewrites every internal relative .html href to match. The result is collision-free and self-consistent: open any page from the flat set and its links resolve. The repo/course dir stays the canonical navigable home; the gist is an off-machine, shareable, private snapshot — not the source of truth.
Default is SECRET — unlisted, URL-only, the closest GitHub offers to "private" for a gist. --public overrides (rarely wanted). The script ends by printing the gist URL.
The second tier puts the course on a real website. Instead of a snapshot you download, this is a live address on Cloudflare's global network — fast to load from anywhere, and a true site where the folders and links work just as they do at home, with nothing to flatten. You reach for it when you want the simplest possible reader experience: send a link, they click, they read.
This tier asks for a little more setup. Once, on your machine, you log into Cloudflare through a tool called wrangler. That login is a one-time, human step — you do it in a browser, and the access token never leaves your computer. After that, the publish script can deploy the site whenever the loop converges.
By default a freshly deployed Pages site is reachable by anyone who knows the URL. To make it genuinely private — readable only by people you name, from anywhere — you turn on Cloudflare Access, a login gate, in the Cloudflare dashboard. That is a one-time click-through the human does; it cannot be automated with the publish token, so the script simply reminds you to do it rather than pretending it can.
# one-time, on your machine — interactive OAuth in a browser (the human does this) npx wrangler login # then deploy the course to Pages and get a live URL node ~/.claude/skills/loop-engineering/publish-course-pages.js ./course # optional: name the project / pick the branch node ~/.claude/skills/loop-engineering/publish-course-pages.js ./course --project loop-course
~/.claude/skills/loop-engineering/publish-course-pages.js and needs node plus an authenticated wrangler and a Cloudflare account. Do the one-time npx wrangler login yourself (browser OAuth). To lock the site down afterwards, open the Cloudflare dashboard: Workers & Pages → your project → Settings → enable the Access policy, or set up a Zero Trust Access application for <project>.pages.dev allowing only your email.The wrangler OAuth token (from npx wrangler login) lets the script deploy, but it carries no Zero-Trust scope — so a freshly deployed Pages site is public-by-URL. Making it private is a Cloudflare Access configuration, a one-time dashboard step the human performs (Workers & Pages → project → Settings → Access policy; or Zero Trust → Access → Applications → self-hosted, domain <project>.pages.dev, policy Allow → your email). It cannot be done with the wrangler token, so the script prints the reminder and lets the human finish it.
Pages preserves the tree and serves relative links natively (no flatten/relink), giving a real CDN-hosted site. The gist is the always-on private snapshot; Pages is the hosted tier. The skill's guidance is to run both on convergence.
Because publishing is the one outward move the loop makes on its own, it follows a simple, conservative rule: publish on convergence unless there is a clear reason not to. There are exactly two reasons to skip a tier — the scope of the task explicitly forbids publishing, or the tool that tier needs isn't logged in. If the scope says "keep this internal," nothing goes out. If gh isn't authenticated, the gist tier is skipped; if wrangler isn't logged in, the Pages tier is skipped. No drama, no failure — just a tier that quietly doesn't run.
It's worth seeing how the three kinds of participants in the harness relate to this one gate, because each has a different stake in it.
You (human)
You get the payoff: a private snapshot you can hand to anyone via a link, and a live URL you can simply open in a browser. You also hold the one-time keys — the gh and wrangler logins, and the Cloudflare Access toggle that makes the hosted site truly private.
The LLM driving the loop
It runs the gate automatically on convergence and reports the URLs. It skips a tier only if the scope forbids publishing or the CLI for that tier is unauthenticated — never on a whim. It does not invent other outward actions; this is the single one it's allowed to take.
The agents & the repo
Across delegated agents the repo course/ dir stays the canonical navigable home. The gist and the site are derived copies that travel; the source of truth never moves off the machine, so everyone keeps working against the same original.
Takeaway
Publishing is reach, not relocation. On convergence the loop pushes a private gist and (when set up) a hosted site, skips a tier only when scope or auth says so, and leaves the repo course/ as home.
Feel how the two tiers depend on their preconditions. Each publish tier — gist and cloudflare-pages — only goes out when the loop has converged and the right CLI is authenticated. Flip a publish tier on while its precondition is off and a warning slides in telling you exactly what's missing; the summary line always reads back which tiers are actually live, with the repo course/ always standing behind them.
What is published
The tiers are a flat boolean record; a second object lists the preconditions each one requires. After any toggle the editor checks each enabled tier against its requires list. A publish tier that is on but missing a precondition is unsatisfied — it draws the inline warning and is excluded from the published set the summary reads back. The two base preconditions map to the real-world skip rules: gh-auth gates the gist, wrangler-login gates Pages, and converged gates both.
const requires = { converged: [], // the gate only fires after convergence gh_auth: [], // gh CLI authenticated wrangler_login: [], // one-time browser OAuth gist: ['converged', 'gh_auth'], // skipped if gh is unauthenticated pages: ['converged', 'wrangler_login'] // skipped if wrangler is not logged in }; function publishedSet() { // a publish tier counts only when ON and all preconditions are satisfied return ['gist', 'pages'].filter(id => flags[id] && missingDeps(id).length === 0); }