Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,7 @@ agent-reference.local.json
.venv/
__pycache__/
*.pyc
.pytest_cache/
.pytest_cache/

# Pages the page lab builds from its samples
tools/page-lab/built/
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ HTML is exempt, by `**/*.html` in `.oxfmtrc.json`. Formatting it broke tags acro

## Pages: a method, recipes and kits, never templates

`skills/instrument-page` makes one self-contained HTML page, and it has no page types. It is a method (find the point, direct a shape, a feel and a hero, lead with the point), a stylesheet the agent never reads (`lib/foundation.css` and `lib/foundation.js`, which `page.mjs` writes into the page), a check in two halves (`page.mjs` reads every rule the file itself shows, and `lib/probe.js`, which the agent evaluates in its own browser tool, measures layout at desktop, link-preview, dark and phone sizes; each prints one FAIL line per problem), recipes (`cookbook.md`: techniques such as a comparison matrix, a ranked pick, a timeline, steps, figures with a chart), and kits (`kits/<name>/`: map, playground, storyboard, whiteboard), which are capabilities a page reaches for, not kinds a user picks. It replaced twenty-eight templates because an agent handed a template fills it in, and pages made that way came out near-identical and scored worst in evaluation. See [`docs/decisions/2026-10-05-create-page-is-a-method-with-kits.md`](docs/decisions/2026-10-05-create-page-is-a-method-with-kits.md).
`skills/instrument-page` makes one self-contained HTML page, and it has no page types. It is a method (find the point, direct a layout, a feel and a hero, lead with the point), a stylesheet the agent never reads (`lib/foundation.css` and `lib/foundation.js`, which `page.mjs` writes into the page), a check in two halves (`page.mjs` reads every rule the file itself shows, and `lib/probe.js`, which the agent evaluates in its own browser tool, measures layout at desktop, link-preview, dark and phone sizes; each prints one FAIL line per problem), recipes (`cookbook.md`: techniques such as a comparison matrix, a ranked pick, a timeline, steps, figures with a chart), and kits (`kits/<name>/`: map, playground, storyboard, whiteboard), which are capabilities a page reaches for, not kinds a user picks. It replaced twenty-eight templates because an agent handed a template fills it in, and pages made that way came out near-identical and scored worst in evaluation. See [`docs/decisions/2026-10-05-create-page-is-a-method-with-kits.md`](docs/decisions/2026-10-05-create-page-is-a-method-with-kits.md).

- **Do not add a template, an example gallery or a page type.** A new kind of page is a recipe in `cookbook.md` when it is a technique, or a kit when it needs a library wired up or a drawing kit that would be wrong to retype. Never a second skill that makes pages: every skill's description shares the agent's catalog budget.
- `SKILL.md` names every recipe by its heading and links every kit's `README.md` and every file in `references/`. The runtime hands an agent at most fifty filenames from a loaded skill, depth-first, so a file the router does not name is one an agent may never open. `pnpm check:page-skill` enforces that, the description's 400-character budget, the fifty-file ceiling, and that every kit's `skeleton.html` is a page `page.mjs` accepts.
Expand All @@ -89,6 +89,7 @@ HTML is exempt, by `**/*.html` in `.oxfmtrc.json`. Formatting it broke tags acro
- `lib/check.mjs` lists the hosts a page may load from, and `allowed-sources.json` at the root is what the platform's pages worker derives every hosted copy's `connect-src` from, so an origin added there needs a pages deploy before shared pages honor it. `pnpm check:page-skill` fails when the check allows a host the file does not.
- **A page skill starts no process and opens no browser.** Inside the user's app, launching their real browser raises an operating system permission prompt in the app's name. Layout is checked by `lib/probe.js` in the agent's own browser tool. Publishing stays with `share.mjs` and `share.py`, since many hosts cannot run the page's Share button. `scripts/page-skill.test.ts` fails when any file in instrument-page or wireframe names a process call, or a network write outside the share scripts. See [`docs/decisions/2026-10-06-page-skills-launch-nothing.md`](docs/decisions/2026-10-06-page-skills-launch-nothing.md).
- `tools/page-check` is the Chromium check for evals and development: `page.mjs`, then the same `lib/probe.js` over the DevTools protocol, then screenshots. It runs only a downloaded Chromium (`PAGE_CHECK_CHROME`, or agent-browser's, Playwright's or Puppeteer's cache), never an installed browser. Nothing under `skills/` imports it.
- `tools/page-lab` is a local lab for designing the stylesheet: a specimen of every token and class, six sample pages, and a panel that switches feel, layout and theme, all reading `foundation.css` live (`pnpm page-lab`). Its samples are fixtures for checking the stylesheet; never copy one into the skill or show one to an agent.
- `skills/wireframe` is its own skill, because wireframes are drawn in Tailwind's vocabulary on their own shell (`shell.html`, which carries the old starter's skin, compiled sheet and share widget), and projects carry their own wireframe kits in that vocabulary.
- An example page in a recipe or kit exists to show the shape, not to be authoritative. Name things that exist, and link them to their maker's or vendor's own page, never a deep SKU, a search result or anything that rots: an agent copying a page keeps its links.
- Numbers are the easiest thing for an agent to invent and the hardest for a reader to check. A page carries a figure only when the prompt, a source, or arithmetic over shown inputs gave it; scores use coarse scales (letters, a few dots, a word) unless every input is on the page, and never a decimal that implies precision the research does not have.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Pages are neutral, with four feels and two layouts

Status: accepted, 2026-10-24. Updates [`2026-10-05-create-page-is-a-method-with-kits.md`](2026-10-05-create-page-is-a-method-with-kits.md): the method, the stylesheet, the check, the recipes and the kits stand; what the stylesheet looks like and what the agent picks on `<html>` change.

## Context

The foundation stylesheet carried the Instrument brand into every page: teal as the default accent, an uppercase teal kicker, teal step numbers and tags. Pages are meant to be portable containers for any explainer, so the brand read as a template. Several details looked machine-made: a colored stripe down the side of rounded cards, a ring around every tag, rules above and below the footer, a triangle on each disclosure.

The two attributes the agent set on `<html>` mixed two ideas. Six feels named situations (urgent, warm, crew) next to looks (ledger, festive), so the agent had to map a situation onto a look. Four shapes (card, read, sheet, wall) named how a page is used, but card and read differed by 2rem of column, and sheet and wall were both one wide column. The stylesheet also pushed every grid, card row, flow diagram and four-column table onto a wider track automatically, so most pages had two left edges.

## Decision

- **Neutral structure, intentional color.** Text, links, rules, labels and the kicker are neutral in every feel. Anything without `data-tone` or `data-c` is gray. The accent shows only where a page marks something, and SKILL.md tells the agent to mark only what carries the point. The kicker is sentence case in Inter.
- **Four feels, named for how they look, picked by what carries the page:** `editorial` when prose does (a serif), `bold` when one fact does (heavy type, one hot color, square corners), `dense` when tables do (small type, tight rows), and `minimal` for everything else. A feel is a look, not a topic: an incident or a deadline takes the feel its content calls for and marks what is urgent with `data-tone="bad"`.
- **Two layouts.** `data-layout` replaces `data-shape`: `column` (the default) or `wide` (a roster or big table; prose keeps a reading width on the same left edge; prints landscape). Card's real content, that a link dropped in chat must make its point on the first screen, is the cookbook recipe "A one-screen card".
- **One left edge.** Everything sits in the column. `.bleed` is the deliberate way out, for a table or chart that needs the room.
- **Old names keep working.** The stylesheet draws calm as minimal; warm as editorial; urgent, festive and crew as bold; ledger as dense; card and read as column; sheet and wall as wide. `page.mjs` accepts them, and SKILL.md teaches only the new ones.

## Options weighed

- **Keep situation names and restyle them.** Urgent lost most of its look once kickers and unmarked elements went neutral, and any feel can show urgency through marks. A situation name asks the agent to translate; a look name does not.
- **More feels (swiss, colorful, warm, technical, cute).** Each overlapped another: Swiss with bold, colorful with `data-c` in any feel, warm with editorial, technical with code blocks that already sit well in every feel. Every feel has to be crafted and checked in light, dark and phone, so fewer, better ones won.
- **Keep shapes and fix their widths.** The CSS behind four shapes was two widths; the rest was writing advice, which belongs in the method.
- **Keep modules wide by default and add `.narrow`.** Readers found pages with two edges chaotic; opting in to width is rarer and safer than opting out.

## Consequences

- `--pill` sets the rounding of buttons, tags, bars, dots and step numbers; bold sets it to 0. `--fill` is the neutral tint for unmarked things.
- The layout probe's "cramped" rule now leads with fewer columns or shorter cells before `.bleed`.
- A page using a removed name that never shipped (colorful, swiss, technical) fails the check.
2 changes: 2 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const config: KnipConfig = {
"skills/instrument-page/lib/foundation.*",
"skills/instrument-page/lib/probe.js",
"skills/wireframe/*.mjs",
// The page lab's panel, which its HTML pages load by a script tag.
"tools/page-lab/lab.js",
],
ignoreBinaries: [
"actionlint", // Used by scripts/check-actions.ts
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"test:scripts": "vitest run --project scripts",
"generate:skills-md": "node scripts/generate-skill-md.ts",
"page-check": "node tools/page-check/check.mjs",
"page-lab": "node tools/page-lab/serve.mjs",
"page-lab:build": "node tools/page-lab/build.mjs",
"syncpack": "syncpack",
"update:actions": "actions-up"
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-page-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ function checkSkeletons(errors: string[]) {
const wants: [RegExp, string][] = [
[/^<!doctype html>/i, "a doctype on the first line"],
[/<!--\s*direction:[^>]*\bpoint\s*=/, "a direction comment with a point"],
[/<html\b[^>]*\bdata-shape="(card|read|sheet|wall)"/, "a data-shape"],
[/<html\b[^>]*\bdata-layout="(column|wide)"/, "a data-layout"],
[/<title>[^<]+<\/title>/, "a title"],
[/<meta\s+name="description"/, "a meta description"],
[/class="[^"]*\bhero\b/, 'an element with class="hero"'],
Expand Down
6 changes: 3 additions & 3 deletions scripts/page-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ const browser = findChrome();
describe.skipIf(!browser.bin)("lib/probe.js in Chromium", () => {
const CHECK = join(import.meta.dirname, "../tools/page-check/check.mjs");
const page = (extra: string) => `<!doctype html>
<!-- direction: reader="parents" point="the late bus costs less" shape=read feel=calm hero="two bars" -->
<html lang="en" data-shape="read" data-feel="calm">
<!-- direction: reader="parents" point="the late bus costs less" layout=column feel=minimal hero="two bars" -->
<html lang="en" data-layout="column" data-feel="minimal">
<head><title>The late bus costs less</title><meta name="description" content="The 4:10 bus is $12 cheaper per child than the 3:30 bus."></head>
<body><header><hgroup><p>For class parents</p><h1>The late bus costs less</h1></hgroup></header>
<section class="hero"><svg viewBox="0 0 400 120" width="400" height="120" role="img" aria-label="Two bars"><rect x="0" y="10" width="300" height="40" data-c="1"></rect><rect x="0" y="70" width="220" height="40" data-c="2"></rect></svg></section>
Expand All @@ -136,7 +136,7 @@ ${extra}
});

it(
"reports a table squeezed into the column, not one that went wide",
"reports a table squeezed into the column",
{ timeout: 120000 },
async () => {
const cell =
Expand Down
Loading
Loading