Prototype · design

collaborative-generation

Original design: a comment system for the spherical generation and Iwahori generation pages, and only those. Everything it deploys carries one name: the GitHub repo, the laptop folder and the launchd job are all collaborative-generation. A reader comments on a block; your laptop notices; Claude Code drafts a reply from the page and the reader's summary; GitHub emails it. No server and no API key. Every collaborator has a GitHub account.

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:

Thick span#def-thick

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.

  • 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.sh adds the label spherical/def-thick from the title prefix, since a label in the link is ignored for readers without triage rights. A title without the prefix gives block: none, and the reply asks which block. rev is 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.json on the repo's public Pages site, which holds no thread text.
Left: the public proof page with three blocks, each with a button reading comment, threads: Thick span with anchor def-thick, Two index sets, two roles with anchor index-sets, and I, Conditional generation with anchor main-proposition. A reader note sits under Thick span. Right top: a new issue in the private collaborative-generation repo with the title prefix spherical/def-thick, a quoted phrase, the reader's free-text question, and a footer giving page, anchor def-thick and rev. Right bottom: notes.json on the public Pages site of the same repo, holding approved notes only. Arrows: 1 click opens the issue; 2 /publish adds a note; 3 the page reads the JSON.
The anchor goes into the issue; only approved notes come back to the page.
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

Table 1. · Email notifications by event.
eventemailed
A reader opens an issueyou, if you watch the repo (“All activity”)
The AI replies, 20 minutes after the commentthe reader and everyone else who has commented in that thread; not you, since it posts as you
A reader replies to that emailit becomes a comment; poll.sh picks it up on its next run
A thread is quiet for 14 dayseveryone 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

Table 2. · Hosting options for replies.
optionreplies whenneeds
Laptop polls (chosen)laptop open; overdue comments answered within 2 min of wakingClaude Code and gh logged in; one launchd file
Self-hosted Actions runner on the laptoplaptop opena registered runner; queued jobs are cancelled after about 24 h, so a weekend away loses replies
Claude Code Action on GitHub's runnersalwaysclaude 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.sh still checks for length and labels.
  • If a reader says a step is wrong, the reply does not rule on it; the label needs-maintainer sends it to you.
  • Only the issue's author can trigger /publish or /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 /summary in 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.
Table 3. · Automatic changes and safeguards.
changeautomaticsafeguard
AI reply in a threadyesfirst line “AI · unverified”
Reader summaryyesrebuilt from threads; the reader can ask for it and correct it
Your answer under a blocknoyou run publish.sh; shown as “answer from the author”
Reader note under a blockyesthe 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 pagenoyou copy a note into the page when you want to
Definitions, statements, proofsnever—

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.
Table 4. · Where information is stored.
whatwherewho can readwho writes
Page textpublic Trees, branch gh-pageseveryoneyou
Comments and conversationsprivate issues of collaborative-generationcollaboratorsreaders, you, bot
Bot scripts and promptprivate collaborative-generation, branch maincollaboratorsyou
Notes and comments.jspublic collaborative-generation, branch gh-pageseveryoneyou: answers, script; bot: reader notes
Reader summaries, thread mirrorlaptop ~/collaborative-generation/local/youpull.sh: threads; bot: summaries
gh and Claude Code loginslaptop 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/.

  1. 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
  2. 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.

  3. 21:42 → 22:02laptop · poll.sh
    G2

    ③ poll.sh runs every 2 minutes. It first runs pull.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?
  4. 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
  5. 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
    cwlin4916 commented on #12

    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:

    • 𝒟 = Dbc(𝔸¹), 𝒮 = {δa}: then 𝒮⊥ = 0, so ⊥(𝒮⊥) = 𝒟;
    • but Thick(𝒮) holds only objects with finite support, so the constant sheaf k𝔸¹ is missing.

    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.

  6. 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.
  7. 23:10 → 09:01laptop closed
    G2

    Nothing 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
  8. 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 /publish from the issue's author. Since /publish closes 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: ..."}
  9. 09:02laptop · post.sh
    G3 · G4

    ⑧ Checks (/publish by 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 ..."}]
     }
  10. 09:04anyone · browser
    G1 · G4

    ⑨ GitHub Pages has rebuilt the collaborative-generation site; the next visitor to the page sees:

    Thick span#def-thick
    reader 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

Table 5. · Changes left by the walkthrough.
wherewhat changed
private issues#12: two comments by Alice, two AI replies, closed
laptop local/readers/alice.md created, one local commit
public notes.jsonone note under spherical/def-thick
public Treesnothing

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

Table 6. · Local scripts and their roles.
partrunswrites
pull.shevery 10 min (stage 1); inside poll.sh afterlocal/threads/ and cursor; the block label on new issues, from the title prefix (block: none if missing). No Claude, no comments.
publish.shby you, publish.sh 12; by post.sh on /publishOpens 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.shevery 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 -ponce per such issuenothing: tools Read, Grep, Glob; returns JSON
post.shafter each claude -pthe 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.

Stage 1. Goal G1: every comment is an issue tied to its block, kept on the laptop; your answers reach the page. All parts new: the public proof pages (spherical-generation and iwahori-generation, with a comment and threads chip), the private GitHub issues (label added by pull.sh), the laptop scripts (pull.sh every 10 minutes, publish.sh by hand; claude -p and post.sh greyed as stage 2), the laptop library (threads/; drafts/ greyed as stage 2) and the public notes.json holding your answers with the page's rev. Arrows: 1 click, page to issues; 2 pull, issues to scripts; 3 mirror, scripts to library; 4 you answer, you to issues; 5 publish.sh 12, you to scripts; 6 push, scripts to notes.json; 7 the page shows your answer, notes.json to the pages. Footer: no automatic writer, no Claude.

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

Stage 2. Goal G2 and G3: a reply you would send unedited, about 20 minutes after each comment, and a summary per reader. The laptop scripts gain claude -p (poll.sh every 2 minutes, claude -p read-only, post.sh every write; waits 20 minutes and stays quiet if someone else answers) and the library gains drafts and readers. Arrows: 1 poll from issues to the scripts; 2 draft and summary from the scripts to the library; 3 shadow, you read each draft; 4 reply, live, from the scripts to the issues. notes.json still holds only your answers.

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

Stage 3. Goal G4: a resolved thread leaves a note under its block, with no action from you. notes.json gains a writer, the bot, and holds your answers and reader notes, each with the page's rev. Arrows: 1 /publish from the issues to the scripts (claude -p drafts the note, post.sh calls publish.sh); 2 push to notes.json; 3 the page shows the note. You delete a bad note or copy a good one into the page.

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

Table 7. · Stage exit checks.
stagedone whenundo
1Within 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
2Shadow: 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
3The first 5 reader notes stay as written.delete the entries with "by": "reader"

Review of the first staging

Table 8. · Earlier design issues and repairs.
problemfix
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