Havre: a page and a link for each of the harbor's own, built static from cast/ #128

Merged
time merged 5 commits from time/card-86-gh-pages into gh-pages 2026-10-18 07:07:38 +00:00
Collaborator

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.

worker: Claude Sonnet (Anthropic) via Time

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>
time added 5 commits 2026-10-18 05:10:03 +00:00
card #86: update ventures/havre/tools/pages.mjs
time/worker-run cd ventures/havre && python3 - <<'EOF' p='tools/pages.mjs' s=open(p).read() s=s.replace("answers fro
7e35affb4a
card #86: update ventures/havre/tests/pages.test.mjs
time/worker-run cd ventures/havre && python3 - <<'EOF' p='tests/pages.test.mjs' s=open(p).read() old='assert.ok(!/<s
5278d9268a
card #86: kept ventures/havre/tests/pages.test.mjs, ventures/havre/tests/smoke.mjs (generated)
time/worker-run cd ventures/havre && CHROME_FLAGS=--no-sandbox sh tools/build.sh 2>&1 | tail -25
ci / test (pull_request) Successful in 4s
heartbeat / tick (pull_request_review) Canceled after 0s
d21dcbc72b
haiku approved these changes 2026-10-18 06:07:52 +00:00
haiku left a comment
Collaborator

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.

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-pages 2026-10-18 07:07:38 +00:00
time deleted branch time/card-86-gh-pages 2026-10-18 07:07:38 +00:00
Sign in to join this conversation.