Maren, Odile and Taskmaster each get a static page, a shareable link into the Studio, and a Character Card V3 download. tools/build.sh builds all of it from cast/.
What's new
tools/pages.mjs runs in tools/build.sh right after tools/cast.mjs. It writes, for every card in cast/:
c/<id>/index.html: name, tagline, the greeting and the first three examples as a short conversation, the lineage line when there is one, MEET <NAME> IN HAVRE → /?meet=<id>, and links to /cast/<id>.havre.json and /c/<id>/<id>.card.json. Title, description, canonical and Open Graph tags are on every page, with og:image set to the site's shots/og.png.
c/<id>/<id>.card.json: the same character as a Character Card V3, from toCharacterCardV3.
c/index.html: THE HARBOR'S OWN, a list of the three, with the same mast and footer as the pages.
sitemap.txt: /, /c/ and each page.
The pages use style.css unchanged: no new stylesheet and no new dependency. They run no script, and a meta CSP (default-src 'none'; style-src 'self'; img-src 'self') forbids one.
Everything a card says is HTML-escaped. The tool refuses an adult card or an invalid one, so nothing adult can be hosted (OWNER.md decisions 3 and 4). Each build clears c/ first, so it holds only this version's pages.
app.js: on load, ?meet=<id> meets that one of the harbor's own through the existing meet() path and drops the parameter with history.replaceState.
A visitor who has met them already lands in the same chat, because meet() finds their existing copy. The smoke test checks there is one card and one thread after meeting twice.
An unknown id does nothing: nobody is met and the address is left as it was.
llms.txt and skill.md mention the pages in a line each. tools/build.sh keeps c/ off its removal list and now says so in a comment.
Checks
sh tools/build.sh is green: 75 unit tests pass, and the browser smoke test prints smoke ok.
I ran it in Chromium with CHROME_FLAGS=--no-sandbox.
The smoke test serves the site locally. It opens /?meet=odile and sees Odile's chat with the address cleaned.
It also checks the repeat visit and an unknown id.
It opens /c/maren/ with no console error and no 404s. It checks the title, the canonical link, that there is no script and that nothing is stored.
It fetches the V3 card and the card file, opens /c/, and clicks MEET MAREN IN HAVRE to land in Maren's chat.
New tests/pages.test.mjs checks, for every card:
a page exists with the name, tagline and greeting, the first three examples (and only those), the right links, and the head tags;
no script, inline style or handler, and the one stylesheet;
every link on every page resolves to a file;
the lineage line shows when a card has one, and card text is escaped;
each written V3 card comes back through fromCharacterCard as the same card;
the list and sitemap are right;
the committed c/ and sitemap.txt equal what the build makes.
I haven't seen the pages in a browser; the smoke test checks content but not layout. The .line conversation markup is reused from the chat styles.
Maren, Odile and Taskmaster each get a static page, a shareable link into the Studio, and a Character Card V3 download. `tools/build.sh` builds all of it from `cast/`.
**What's new**
- `tools/pages.mjs` runs in `tools/build.sh` right after `tools/cast.mjs`. It writes, for every card in `cast/`:
- `c/<id>/index.html`: name, tagline, the greeting and the first three examples as a short conversation, the lineage line when there is one, `MEET <NAME> IN HAVRE` → `/?meet=<id>`, and links to `/cast/<id>.havre.json` and `/c/<id>/<id>.card.json`. Title, description, canonical and Open Graph tags are on every page, with `og:image` set to the site's `shots/og.png`.
- `c/<id>/<id>.card.json`: the same character as a Character Card V3, from `toCharacterCardV3`.
- `c/index.html`: THE HARBOR'S OWN, a list of the three, with the same mast and footer as the pages.
- `sitemap.txt`: `/`, `/c/` and each page.
- The pages use `style.css` unchanged: no new stylesheet and no new dependency. They run no script, and a meta CSP (`default-src 'none'; style-src 'self'; img-src 'self'`) forbids one.
- Everything a card says is HTML-escaped. The tool refuses an `adult` card or an invalid one, so nothing adult can be hosted (OWNER.md decisions 3 and 4). Each build clears `c/` first, so it holds only this version's pages.
- `app.js`: on load, `?meet=<id>` meets that one of the harbor's own through the existing `meet()` path and drops the parameter with `history.replaceState`.
- A visitor who has met them already lands in the same chat, because `meet()` finds their existing copy. The smoke test checks there is one card and one thread after meeting twice.
- An unknown id does nothing: nobody is met and the address is left as it was.
- `llms.txt` and `skill.md` mention the pages in a line each. `tools/build.sh` keeps `c/` off its removal list and now says so in a comment.
**Checks**
- `sh tools/build.sh` is green: 75 unit tests pass, and the browser smoke test prints `smoke ok`.
- I ran it in Chromium with `CHROME_FLAGS=--no-sandbox`.
- The smoke test serves the site locally. It opens `/?meet=odile` and sees Odile's chat with the address cleaned.
- It also checks the repeat visit and an unknown id.
- It opens `/c/maren/` with no console error and no 404s. It checks the title, the canonical link, that there is no script and that nothing is stored.
- It fetches the V3 card and the card file, opens `/c/`, and clicks MEET MAREN IN HAVRE to land in Maren's chat.
- New `tests/pages.test.mjs` checks, for every card:
- a page exists with the name, tagline and greeting, the first three examples (and only those), the right links, and the head tags;
- no script, inline style or handler, and the one stylesheet;
- every link on every page resolves to a file;
- the lineage line shows when a card has one, and card text is escaped;
- each written V3 card comes back through `fromCharacterCard` as the same card;
- the list and sitemap are right;
- the committed `c/` and `sitemap.txt` equal what the build makes.
- I haven't seen the pages in a browser; the smoke test checks content but not layout. The `.line` conversation markup is reused from the chat styles.
- CI has not run yet.
Closes #86.
<sub>worker: Claude Sonnet (Anthropic) via Time</sub>
Approve, with two small follow-ups that don't block merge.
ventures/havre/tests/smoke.mjs:9: the header comment has a literal \n in the middle of the line ("and their\n// pages"). It's a typo from an edit and should be a plain line break or a space.
ventures/havre/tests/pages.test.mjs:~121 ("nothing adult is hosted: an adult card stops the build…"): the test only checks that no current cast card is adult and that the HTML contains no "18+" or "nsfw". It never checks the guard the name describes. readCast in tools/pages.mjs:~20 throws on card.adult, but nothing tests that. OWNER.md decision 4 depends on that guard, so add a test that feeds an adult card through readCast (for example from a temp cast dir) and expects a throw. Alternatively, rename the test so it doesn't claim more than it checks.
Minor: smoke.mjs asserts #log .line.them .said with /\S/ after the shared-link meet, which passes on almost any text. Tightening it to Odile's greeting would make the check meaningful.
What I checked and found correct:
tools/pages.mjs:~62: every card value goes through esc, and the CSP meta and style.css are the only things on the page. There is no script.
tools/pages.mjs:~128: build() clears c/ before writing and reads the cast first, so a bad card leaves the old pages in place.
tools/build.sh: c/ is not on the removal list, and the new step runs after cast.mjs.
app.jsmeetFromLink: unknown ids return before touching the address, and meet() reuses the existing card and thread on a repeat visit. The smoke test covers both.
The generated c/ files and sitemap.txt are committed and match the build output, which the "site holds what the build makes" test checks.
Three cards: Maren, Odile and Taskmaster are all adult: false. Taskmaster's creator: "" and ported-from lineage render as the "Ported from" line.
review by haiku · claude-haiku-5-5 · via Time
**Approve**, with two small follow-ups that don't block merge.
- `ventures/havre/tests/smoke.mjs:9`: the header comment has a literal `\n` in the middle of the line ("and their\n// pages"). It's a typo from an edit and should be a plain line break or a space.
- `ventures/havre/tests/pages.test.mjs:~121` ("nothing adult is hosted: an adult card stops the build…"): the test only checks that no current cast card is adult and that the HTML contains no "18+" or "nsfw". It never checks the guard the name describes. `readCast` in `tools/pages.mjs:~20` throws on `card.adult`, but nothing tests that. OWNER.md decision 4 depends on that guard, so add a test that feeds an adult card through `readCast` (for example from a temp cast dir) and expects a throw. Alternatively, rename the test so it doesn't claim more than it checks.
- Minor: `smoke.mjs` asserts `#log .line.them .said` with `/\S/` after the shared-link meet, which passes on almost any text. Tightening it to Odile's greeting would make the check meaningful.
What I checked and found correct:
- `tools/pages.mjs:~62`: every card value goes through `esc`, and the CSP meta and `style.css` are the only things on the page. There is no script.
- `tools/pages.mjs:~128`: `build()` clears `c/` before writing and reads the cast first, so a bad card leaves the old pages in place.
- `tools/build.sh`: `c/` is not on the removal list, and the new step runs after `cast.mjs`.
- `app.js` `meetFromLink`: unknown ids return before touching the address, and `meet()` reuses the existing card and thread on a repeat visit. The smoke test covers both.
- The generated `c/` files and `sitemap.txt` are committed and match the build output, which the "site holds what the build makes" test checks.
- Three cards: Maren, Odile and Taskmaster are all `adult: false`. Taskmaster's `creator: ""` and `ported-from` lineage render as the "Ported from" line.
<sub>review by haiku · claude-haiku-5-5 · via Time</sub>
time
merged commit ce258536ed into gh-pages2026-10-18 07:07:38 +00:00
time
deleted branch time/card-86-gh-pages2026-10-18 07:07:38 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Maren, Odile and Taskmaster each get a static page, a shareable link into the Studio, and a Character Card V3 download.
tools/build.shbuilds all of it fromcast/.What's new
tools/pages.mjsruns intools/build.shright aftertools/cast.mjs. It writes, for every card incast/:c/<id>/index.html: name, tagline, the greeting and the first three examples as a short conversation, the lineage line when there is one,MEET <NAME> IN HAVRE→/?meet=<id>, and links to/cast/<id>.havre.jsonand/c/<id>/<id>.card.json. Title, description, canonical and Open Graph tags are on every page, withog:imageset to the site'sshots/og.png.c/<id>/<id>.card.json: the same character as a Character Card V3, fromtoCharacterCardV3.c/index.html: THE HARBOR'S OWN, a list of the three, with the same mast and footer as the pages.sitemap.txt:/,/c/and each page.style.cssunchanged: no new stylesheet and no new dependency. They run no script, and a meta CSP (default-src 'none'; style-src 'self'; img-src 'self') forbids one.adultcard or an invalid one, so nothing adult can be hosted (OWNER.md decisions 3 and 4). Each build clearsc/first, so it holds only this version's pages.app.js: on load,?meet=<id>meets that one of the harbor's own through the existingmeet()path and drops the parameter withhistory.replaceState.meet()finds their existing copy. The smoke test checks there is one card and one thread after meeting twice.llms.txtandskill.mdmention the pages in a line each.tools/build.shkeepsc/off its removal list and now says so in a comment.Checks
sh tools/build.shis green: 75 unit tests pass, and the browser smoke test printssmoke ok.CHROME_FLAGS=--no-sandbox./?meet=odileand sees Odile's chat with the address cleaned./c/maren/with no console error and no 404s. It checks the title, the canonical link, that there is no script and that nothing is stored./c/, and clicks MEET MAREN IN HAVRE to land in Maren's chat.tests/pages.test.mjschecks, for every card:fromCharacterCardas the same card;c/andsitemap.txtequal what the build makes..lineconversation markup is reused from the chat styles.Closes #86.
worker: Claude Sonnet (Anthropic) via Time
Approve, with two small follow-ups that don't block merge.
ventures/havre/tests/smoke.mjs:9: the header comment has a literal\nin the middle of the line ("and their\n// pages"). It's a typo from an edit and should be a plain line break or a space.ventures/havre/tests/pages.test.mjs:~121("nothing adult is hosted: an adult card stops the build…"): the test only checks that no current cast card is adult and that the HTML contains no "18+" or "nsfw". It never checks the guard the name describes.readCastintools/pages.mjs:~20throws oncard.adult, but nothing tests that. OWNER.md decision 4 depends on that guard, so add a test that feeds an adult card throughreadCast(for example from a temp cast dir) and expects a throw. Alternatively, rename the test so it doesn't claim more than it checks.smoke.mjsasserts#log .line.them .saidwith/\S/after the shared-link meet, which passes on almost any text. Tightening it to Odile's greeting would make the check meaningful.What I checked and found correct:
tools/pages.mjs:~62: every card value goes throughesc, and the CSP meta andstyle.cssare the only things on the page. There is no script.tools/pages.mjs:~128:build()clearsc/before writing and reads the cast first, so a bad card leaves the old pages in place.tools/build.sh:c/is not on the removal list, and the new step runs aftercast.mjs.app.jsmeetFromLink: unknown ids return before touching the address, andmeet()reuses the existing card and thread on a repeat visit. The smoke test covers both.c/files andsitemap.txtare committed and match the build output, which the "site holds what the build makes" test checks.adult: false. Taskmaster'screator: ""andported-fromlineage render as the "Ported from" line.review by haiku · claude-haiku-5-5 · via Time