Implementation update · 18 October 2026. The affine page now has a right-hand comment composer and a Cloudflare Worker that submits private GitHub issues for visitors without GitHub accounts. Interface sources · Setup and delivery contract. The original GitHub-form prototype and roadmap below are retained as design history.
1 · Goals
Open a goal for how it works. §2 follows one comment through all of them; §3 is the plan to build.
G1A comment belongs to one block, and the reader types it freely.Blocks already carry ids. Both workspaces reuse some, so a block's key names its workspace: spherical/def-thick, iwahori/def-thick. Proof steps and the sections of the family pages reuse ids across pages, so their keys add the page: iwahori/group/step-1.
Each block gets a chip with two buttons. comment opens GitHub's new-issue form, already filled in with the block. threads lists that block's issues. Try both:
For a set 𝒮 of objects of a stable ∞-category 𝒟, Thick𝒟(𝒮) is the smallest full subcategory that contains 0 and 𝒮 and is closed under isomorphisms, shifts, cofibers and retracts.
Select a phrase in the block first and it is quoted in the comment.
- [def-thick] Do we need 𝒟 idempotent-complete?
- The comment is free text. It is GitHub's ordinary editor: Markdown, LaTeX in
$…$, pasted screenshots, @-mentions of other collaborators. Only the title prefix and the footer line are filled in for the reader. - Why the typing happens on GitHub, not on the page. A static page cannot post as the reader without a server holding an app secret, and embeddable widgets such as giscus or utterances need a public repo. Collaborators are already logged in to GitHub.
- Replying later works on the issue page or by replying to the notification email.
poll.shadds the labelspherical/def-thickfrom the title prefix, since a label in the link is ignored for readers without triage rights. A title without the prefix givesblock: none, and the reply asks which block.revis the page's date; the bot checks whether the page changed since.- No counts on the page. A public page cannot read a private repo's issues. Approved notes reach it through
notes.jsonon the repo's public Pages site, which holds no thread text.

G2Every comment gets a reply about 20 minutes later while your laptop is open.Comment systems work when someone answers (Literature).
A launchd job runs bot/poll.sh every 2 minutes. A comment is answered once it is 20 minutes old, unless someone else has replied by then; /publish and /summary are handled at once. Claude Code (claude -p, your subscription) reads; bot/post.sh writes. Steps ③–⑥ of §2 show one run.
Who gets an email
| event | emailed |
|---|---|
| A reader opens an issue | you, if you watch the repo (“All activity”) |
| The AI replies, 20 minutes after the comment | the reader and everyone else who has commented in that thread; not you, since it posts as you |
| A reader replies to that email | it becomes a comment; poll.sh picks it up on its next run |
| A thread is quiet for 14 days | everyone in it: the bot closes it with “reply to reopen” |
Email is a by-product, not the trigger: poll.sh reads the GitHub API directly, which is more reliable than parsing mail.
Why the laptop polls
| option | replies when | needs |
|---|---|---|
| Laptop polls (chosen) | laptop open; overdue comments answered within 2 min of waking | Claude Code and gh logged in; one launchd file |
| Self-hosted Actions runner on the laptop | laptop open | a registered runner; queued jobs are cancelled after about 24 h, so a weekend away loses replies |
| Claude Code Action on GitHub's runners | always | claude setup-token stored as a secret; still your subscription, no API key |
The third row is the fallback if replies must arrive while the laptop is closed. All three draw on your subscription's usage limits.
Safeguards
- A comment may contain instructions aimed at the model. Claude can only read the bot folder, which holds no keys, so the worst outcome is a bad reply, which
post.shstill checks for length and labels. - If a reader says a step is wrong, the reply does not rule on it; the label
needs-maintainersends it to you. - Only the issue's author can trigger
/publishor/summary.
G3The system remembers each reader as a short summary written by an LLM.No levels to classify; the reader can ask for their summary and correct it.
Alice's summary a few weeks after §2:
# alice · updated 2026-11-03 from #12 #19 #23 Works on geometric Satake; fluent with perverse sheaves. New to the spans on this site: asked why Thick ⊊ ⊥(𝒮⊥) (#12) and what H5 asks of the fibre (#19). Accepts a counterexample as an answer; follows adjoint arguments. Open: whether H3 is needed in Proposition I (#23).
- Rebuilt whenever one of the reader's issues closes, for any reason, from all of their threads; at most 120 words; never edited piece by piece.
- Kept only on the laptop, in
local/readers/under a local git repo. Its history is the tracking over time. If the laptop is lost, every summary is rebuilt from the issues. - A reader sees theirs by writing
/summaryin one of their threads; the bot replies with it, so others in that thread see it too. To correct it they reply in the thread, and the next rebuild reads that (open learner model, Literature [11]).
G4Pages improve without you in the loop, through notes, never by editing the mathematics.The bot writes notes to the prototype's own Pages site, not to Trees.
| change | automatic | safeguard |
|---|---|---|
| AI reply in a thread | yes | first line “AI · unverified” |
| Reader summary | yes | rebuilt from threads; the reader can ask for it and correct it |
| Your answer under a block | no | you run publish.sh; shown as “answer from the author” |
| Reader note under a block | yes | the commenter types /publish; at most 80 words; shown as “reader note · not checked by the author”; marked once the page is newer than the note |
| Wording or example in the page | no | you copy a note into the page when you want to |
| Definitions, statements, proofs | never | — |
Notes live in notes.json on the collaborative-generation Pages site, so nothing the bot writes can change the mathematics or collide with your edits to Trees. A wrong note is undone by deleting one entry. Flagged threads carry needs-maintainer.
G5No server and no API key; conversations and summaries stay private.One GitHub repo for issues, code and notes; your laptop and Claude subscription for the replies.
| what | where | who can read | who writes |
|---|---|---|---|
| Page text | public Trees, branch gh-pages | everyone | you |
| Comments and conversations | private issues of collaborative-generation | collaborators | readers, you, bot |
| Bot scripts and prompt | private collaborative-generation, branch main | collaborators | you |
Notes and comments.js | public collaborative-generation, branch gh-pages | everyone | you: answers, script; bot: reader notes |
| Reader summaries, thread mirror | laptop ~/collaborative-generation/local/ | you | pull.sh: threads; bot: summaries |
gh and Claude Code logins | laptop macOS keychain | — | you |
“Bot” means the scripts on your laptop, posting through your gh login. Collaborators can read the bot's code, which holds no secrets. Each reply sends the thread, the block and the reader's summary to Claude under your subscription.
2 · Walkthrough: one comment, end to end
Alice asks about the Thick span; nine steps from her click to the note on the page.
Hypothetical. Alice is a collaborator; your laptop is open. The block is the real Thick span on spherical-generation/index.html; Bob already has thread #9 open on it. Paths are relative to ~/collaborative-generation/.
- Tue 21:40Alice · browser
G1① She selects “closed under isomorphisms, shifts, cofibers and retracts”, presses comment, finishes the title and types her question.
title [spherical/def-thick] Why is Thick ⊊ ⊥(𝒮⊥)? > closed under isomorphisms, shifts, cofibers and retracts. I thought 𝒮 detecting zero made Thick(𝒮) and ⊥(𝒮⊥) equal. What fails? --- page: spherical-generation/index.html · anchor: def-thick · rev: 2026-10-16
- 21:41GitHub
G2② Issue #12 exists in
collaborative-generation. You get an email if you watch the repo; Bob gets none, since he is not in this thread. - 21:42 → 22:02laptop ·
poll.sh
G2③
poll.shruns every 2 minutes. It first runspull.sh, which mirrors each updated issue into one file and labels new ones; then waits until the comment is 20 minutes old. Nobody else answers, so it calls Claude.21:42:06 #12 new · by alice · label spherical/def-thick 21:42:06 wrote local/threads/12.md page unchanged since rev 21:42:06 #12 reply due 22:01 … 22:02:05 #12 no other reply · claude -p for #12
# local/threads/12.md # 12 · [spherical/def-thick] Why is Thick ⊊ ⊥(𝒮⊥)? author: alice · state: open · block: spherical/def-thick page: spherical-generation/index.html · anchor: def-thick rev: 2026-10-16 · page unchanged since ## alice · 2026-10-20 21:41 > closed under isomorphisms, shifts, cofibers and retracts. I thought 𝒮 detecting zero made Thick(𝒮) and ⊥(𝒮⊥) equal. What fails? - 22:02laptop ·
claude -p
G2 · G3④ Claude Code reads the files itself, using read-only tools. About a minute.
$ claude -p "$(cat bot/prompt.md) Issue 12." --allowedTools "Read,Grep,Glob" --output-format json Read local/threads/12.md Grep id="def-thick" local/site/…/spherical-generation/index.html → line 91 Read local/site/…/spherical-generation/index.html lines 91–100 Read local/site/…/spherical-generation/thick-spans.html linked from line 96 Read local/readers/alice.md → none: first thread from alice Grep "block: spherical/def-thick" local/threads/ → 9.md (bob), 12.md
{"reply": "They agree only after enlarging the category. In 𝒟 itself ...", "labels": [], "note": null, // no /publish yet "summary": null} // the issue is still open - 22:03laptop ·
post.sh
G2⑤ Checks, then posts. Nothing public is written.
✓ JSON parses ✓ labels ⊆ {needs-maintainer} ✓ reply 640 chars ≤ 3000 ✓ note, summary null gh issue comment 12 --body-file reply.md # first line “AI · unverified”, hidden bot marker - 22:03Alice · email
G2⑥ GitHub emails her the reply.
cwlin4916 <notifications@github.com>
Re: [cwlin4916/collaborative-generation] [spherical/def-thick] Why is Thick ⊊ ⊥(𝒮⊥)? (Issue #12)AI · unverified. Drafted by Claude from this block … They agree only after enlarging the category. In 𝒟 itself a set can detect zero without thickly generating: …Reply to this email directly, view it on GitHub, or unsubscribe. - 23:10 → 09:01laptop closed
G2Nothing runs. At 07:55 Alice replies to the email from her phone; GitHub adds it to #12:
So the problem is taking ⊥ inside 𝒟 rather than in Ind 𝒟. Thanks, that answers it. /publish
- Wed 09:01laptop ·
poll.sh,claude -p
G2 · G3 · G4⑦ You open the laptop. The next run resumes from the 23:08 cursor and finds Alice's comment, with
/publishfrom the issue's author. Since/publishcloses the issue, Claude also rebuilds her summary from every thread she wrote.Grep "^author: alice" local/threads/ → 12.md
{"reply": "Yes: in Ind 𝒟 the inclusion of the colimit-closed span has a right adjoint, and its cofiber lands in 𝒮⊥; in 𝒟 that adjoint need not exist. Published a note under the block and closed this issue.", "labels": [], "note": "In 𝒟 itself, ⊥(𝒮⊥) can be strictly larger than Thick(𝒮): skyscrapers on 𝔸¹ detect zero, but their thick span holds only finite-support objects. Taken in Ind 𝒟 and intersected with 𝒟, the two agree.", "summary": "# alice · updated 2026-10-21 from #12\nNew to the spans on this site: ..."} - 09:02laptop ·
post.sh
G3 · G4⑧ Checks (
/publishby the author, note 41 words ≤ 80, summary ≤ 120), then writes. Alice is emailed the reply and the closure.gh issue comment 12 --body-file reply.md gh issue close 12 # local/readers/alice.md (laptop only) · new file · local commit # alice · updated 2026-10-21 from #12 New to the spans on this site: asked why Thick ⊊ ⊥(𝒮⊥) (#12), resolved by the skyscraper example and the Ind 𝒟 comparison. Accepts a counterexample as an answer; follows adjoint arguments. # pages/notes.json (public) · commit, push through publish.sh: the bot's only public write { + "spherical/def-thick": [{"issue": 12, "by": "reader", "rev": "2026-10-16", "text": "In 𝒟 itself, ⊥(𝒮⊥) can be ..."}] }
- 09:04anyone · browser
G1 · G4⑨ GitHub Pages has rebuilt the
collaborative-generationsite; the next visitor to the page sees:Thick span#def-thickreader note · not checked by the author (#12) — In 𝒟 itself, ⊥(𝒮⊥) can be strictly larger than Thick(𝒮): skyscrapers on 𝔸¹ detect zero, but their thick span holds only finite-support objects. Taken in Ind 𝒟 and intersected with 𝒟, the two agree.
After the walkthrough
| where | what changed |
|---|---|
| private issues | #12: two comments by Alice, two AI replies, closed |
laptop local/readers/ | alice.md created, one local commit |
public notes.json | one note under spherical/def-thick |
public Trees | nothing |
Your part: none required. Two claude -p runs and one public push. The overnight comment waited until the laptop opened, then was answered by the first poll after waking.
3 · Plan
Everything the prototype adds. One new repo and one laptop folder, both named collaborative-generation; Trees gets one script line per page. §4 builds it in three stages.
GitHub ├── cwlin4916/Trees unchanged, except │ └── subpages/case-study-1-ramifiedrelative/proof_graphs/ │ ├── spherical-generation/*.html + one <script> line loading comments.js │ └── iwahori-generation/*.html + the same line └── cwlin4916/collaborative-generation private; collaborators get Read ├── Issues one per comment; labels <workspace>/<block id>, needs-maintainer ├── branch main private: the bot │ ├── README.md │ ├── .gitignore local/ pages/ │ ├── bot/ │ │ ├── pull.sh GitHub → local/threads/, adds block labels; no Claude │ │ ├── publish.sh one issue → a note in notes.json, push, close │ │ ├── lib.sh thread.py shared settings; JSON helpers │ │ ├── poll.sh runs pull.sh, then claude -p (stage 2) │ │ ├── prompt.md what Claude is told (below) │ │ └── post.sh checks Claude's JSON, does every write │ └── launchd/ │ └── com.cwlin4916.collaborative-generation.plist └── branch gh-pages public through Pages ├── comments.js draws the chips, shows the notes ├── notes.json {"spherical/def-thick": [{"issue", "by", "page", "rev", "date", "text"}], …} └── revs.json current rev of each page with a note Your laptop ├── ~/collaborative-generation/ clone of main; Claude runs here │ ├── bot/ launchd/ README.md │ ├── pages/ worktree of gh-pages; publish.sh writes notes.json │ └── local/ gitignored; never leaves the laptop │ ├── cursor time of the last successful pull │ ├── since the bot acts only on issues opened after this (stage 2) │ ├── site/ sparse clone of Trees gh-pages: the two workspaces only │ ├── threads/ 9.md 12.md … one file per issue │ ├── drafts/ replies held in shadow mode (stage 2) │ └── readers/ alice.md bob.md … own local git ├── ~/Library/LaunchAgents/ │ └── com.cwlin4916.collaborative-generation.plist copy; stage 1: 600 s → pull.sh; stage 2: 120 s → poll.sh └── macOS keychain gh login, Claude Code login
What each part does
| part | runs | writes |
|---|---|---|
pull.sh | every 10 min (stage 1); inside poll.sh after | local/threads/ and cursor; the block label on new issues, from the title prefix (block: none if missing). No Claude, no comments. |
publish.sh | by you, publish.sh 12; by post.sh on /publish | Opens the latest answer on #12 in your editor; you keep, shorten or rewrite it. Then one entry in notes.json under the issue's block, with by and the page's rev; push; close #12 with a link to the block. |
poll.sh | every 2 min while awake (stage 2) | Runs pull.sh; reopens a closed issue that gets a new comment; closes issues quiet for 14 days. Then, for issues opened after since, runs claude -p once per issue whose latest comment is 20 minutes old with no other reply, holds a command, or has just closed. |
claude -p | once per such issue | nothing: tools Read, Grep, Glob; returns JSON |
post.sh | after each claude -p | the reply (to local/drafts/ in shadow mode); on /publish, the note through publish.sh, without the editor (stage 3); on any close, local/readers/<login>.md (local commit) |
# bot/prompt.md
You answer comments on the spherical and Iwahori generation pages. You can only read files.
1. Read local/threads/<N>.md. Its header gives author, state, block, page, anchor, rev.
2. Read the block id="<anchor>" in local/site/…/<page> and the pages it links to.
If the block is none, ask which block the reader means.
3. Read local/readers/<author>.md if present, and the other threads on that block.
4. reply: answer the latest comment from the pages; say when you are unsure. If the
reader says a step is wrong, do not rule on it: add the label needs-maintainer.
If the latest comment is /summary from the author, reply with their summary.
5. note: only if the author wrote /publish; at most 80 words.
6. summary: only if the state is closed or the author wrote /publish; at most
120 words, rebuilt from every thread whose author is that reader.
Return only JSON: {"reply", "labels", "note", "summary"}; unused fields are null.
4 · Deployment in three stages
Stage 1 has no automatic writer: people write, and the scripts only copy, label, and publish what you choose. Stage 2 adds the bot writing privately, Stage 3 the bot writing publicly. Each stage is used for real before the next begins. In each figure, bold parts are new and dashed parts come later.
Stage 1 · Comments, a local library, your answers on the page
Deployed 2026-10-16. Repo, Pages, the launchd job and the comments.js line on all 31 published workspace pages; tested end to end with two issues, since deleted. Exit test due 2026-10-30.

Create the private repo collaborative-generation, invite collaborators with Read, turn on Pages from gh-pages, push comments.js with an empty notes.json, and add <script src="https://cwlin4916.github.io/collaborative-generation/comments.js"> to the spherical generation and Iwahori generation pages. On the laptop, clone main to ~/collaborative-generation with the pages/ worktree, and load the plist running pull.sh every 10 minutes: it copies each new or changed issue into local/threads/ and labels it by its block, so the library and the threads button fill without you, and nothing replies. You answer in the issue as usual. When an answer belongs on the page, publish.sh 12 lets you edit it, puts it under its block as an “answer from the author” with the page's rev, and closes #12; a change to the mathematics itself you make in Trees as always. This tests G1 and measures demand: if no one comments, no bot will fix that.
Stage 2 · The bot replies: shadow, then live

Create the sparse site clone, local/drafts/ and git init local/readers; write prompt.md, post.sh and poll.sh, which runs pull.sh and then Claude; point the plist at poll.sh every 2 minutes. The bot acts only on issues opened after since, so Stage 1 threads stay yours. It begins in shadow mode: post.sh writes each reply to local/drafts/ instead of posting, while you keep answering and publishing. One flag then switches it live: replies post under your name 20 minutes after a comment, unless someone else has answered. Threads quiet for 14 days close, every close rebuilds the reader's summary, and /summary returns it. Nothing public is written by the bot. This tests G2, G3 and G5.
Stage 3 · Reader notes

Add the /publish path to post.sh: when a thread's author types /publish, Claude drafts a note of at most 80 words and post.sh hands it to publish.sh, which shows it as “reader note · not checked by the author”. It reuses Stage 1's push, so the only new thing is who writes. Every note, yours or a reader's, carries the page's rev; once the page is newer, comments.js marks it “written for an earlier version”. This tests G4.
When to move on, and how to undo
| stage | done when | undo |
|---|---|---|
| 1 | Within 2 weeks: ≥ 3 issues from ≥ 2 collaborators, each in local/threads/ with its block label, and ≥ 1 answer published. If not, ask collaborators why before building more. | remove the script line; launchctl unload the plist |
| 2 | Shadow: 8 of 10 drafts you would send unedited. Live: 2 weeks with no reply deleted, and one summary written. | point the plist back at pull.sh |
| 3 | The first 5 reader notes stay as written. | delete the entries with "by": "reader" |
Review of the first staging
| problem | fix |
|---|---|
| Stage 1 kept nothing: threads lived only on GitHub, and your answers never reached the page. | pull.sh mirrors every thread to the laptop; publish.sh puts your answer under its block. |
Stage 1's exit test asked for labels, but labels came from poll.sh, which only exists in Stage 2; threads would list nothing. | pull.sh adds them in Stage 1, without Claude. |
| Stage 2 put AI replies in front of collaborators before you had read one. | Shadow mode first. |
| Exit tests could not be checked: “a few real issues”, “when you no longer need to correct them”. | Counts and a time limit; Stage 1 has a stop rule. |
| Iwahori generation came last, bundled with the only public write, though it adds no risk and too few comments is the main risk (Literature). | It joins Stage 1. |
| Stage 2's first poll would act on every Stage 1 thread: closures, summaries and emails in one burst. | The bot acts only on issues opened after since. |
| A note went silently stale when you edited its block. | Notes store rev and are marked when the page is newer. |
| No stage could be undone. | One undo step each; undoing Stage 2 returns to Stage 1. |
5 · Decisions
- Decided: replies post as you, with “AI · unverified” as their first line.
- Decided: the bot waits 20 minutes, so a collaborator can answer first. In NB, 85% of replies came from other students (Literature §2).
- Open: your Claude account's “Help improve Claude” setting also covers collaborators' comments processed through it.
AI · unverified Drafted by Claude from this block and Three spans compared; not checked by the author.
They agree only after enlarging the category. In 𝒟 itself a set can detect zero without thickly generating:
What holds is Thick𝒟(𝒮) = 𝒟 ∩ ⊥(𝒮⊥) with both orthogonals taken in Ind 𝒟 (Three spans §2, step (2)). Inside 𝒟 there are fewer test objects, so the left orthogonal grows.