11 Commits

Author SHA1 Message Date
Marcos Paulo 37a1e480c6 Merge branch 'main' into pages 2026-09-05 00:01:25 +00:00
Marcos Paulo c1bdd37ac6 Merge branch 'main' into pages
# Conflicts:
#	scripts/verify.mjs
#	skills-review/app.js
#	skills-review/index.html
2026-09-04 23:55:22 +00:00
Marcos Silva ac4683d8df feat: review semantic diff submission 2026-09-04 14:06:50 -03:00
Marcos Silva 73c3062062 feat: expand skills review navigation and catalog 2026-09-04 13:28:15 -03:00
Marcos Silva 96771dfbd6 feat: add skill preview search and diff 2026-09-04 09:45:03 -03:00
Marcos Silva 5756dceb5a feat: review Gustavo and Marcos submitted skills 2026-09-04 09:07:54 -03:00
Marcos Paulo 6694897ae9 Merge branch 'main' into pages 2026-09-04 04:37:43 +00:00
Marcos Paulo c17502318b Merge branch 'main' into pages 2026-09-04 04:18:55 +00:00
Marcos Paulo ef3c99ee01 fix: preserve multi-file skill review desk 2026-09-04 03:53:23 +00:00
Marcos Paulo 21f04db1cc merge: publish focused guide chapters 2026-09-04 03:53:01 +00:00
Marcos Silva 5046fb580d feat: add submitted skills review desk 2026-09-04 00:36:01 -03:00
415 changed files with 480 additions and 31958 deletions
-66
View File
@@ -1,66 +0,0 @@
# Orchestrator
Map of `.agents/`, and how to pick who does what.
## Layout
```
.agents/
ORCHESTRATOR.md you are here
context/ how things are — read before deciding
rules/ how things must be — binding
checklists/ gates to run before you claim done
skills/ procedures an agent loads on demand
agents/ specialist agent definitions
templates/ component and page starting points
scripts/ deterministic helpers (prefer these over prose)
```
`context/` describes reality, `rules/` constrain change. When they disagree,
reality won — update the rule and say so.
## Specialists
Each agent has one responsibility, one set of rules, and its own worktree. Full
definitions in [`agents/`](agents/).
| Agent | Owns | Loads skills |
| ---------------------------------------------------------- | --------------------------------------------------- | -------------------------------------- |
| [`astro-architect`](agents/astro-architect.md) | project scaffold, config, routing, layouts | `astro-page` |
| [`design-system-keeper`](agents/design-system-keeper.md) | tokens, the single palette, type scale | `design-tokens` |
| [`component-builder`](agents/component-builder.md) | one component per task, from templates | `astro-component`, `design-tokens` |
| [`page-migrator`](agents/page-migrator.md) | one page per task, HTML → `.astro` | `astro-page`, `content-migration` |
| [`motion-designer`](agents/motion-designer.md) | transitions, islands with animation | `motion` |
| [`content-i18n-migrator`](agents/content-i18n-migrator.md) | strings out of `app.js` into content collections | `content-migration` |
| [`verification-engineer`](agents/verification-engineer.md) | keeping `verify.mjs` meaningful across the refactor | `verify-contract`, `visual-regression` |
| [`reviewer`](agents/reviewer.md) | merge gate; reads diffs, never writes features | all |
## Working agreement
1. **One task, one worktree, one agent.** See
[`rules/git-worktrees.md`](rules/git-worktrees.md).
2. **Read `context/` first.** Especially `design-system.md` and
`verification.md`. Most wrong answers here come from assuming the CSS is
already coherent.
3. **Templates over invention.** `templates/components/` and `templates/pages/`
exist so ten parallel agents produce one house style, not ten.
4. **The gate is `pnpm run verify` plus the relevant checklist.** Green tests
with deleted assertions is a failed task.
5. **Report what you did not do.** Partial work with an honest boundary is
useful; silent narrowing is not.
## Task flow
```
plans/astro-refactor/task-NN.md ← the brief
git worktree add ../af-task-NN ← isolation
agent loads .agents/agents/<role>.md + its skills
checklists/before-*.md ← self-gate
pnpm run verify ← hard gate
reviewer agent on the diff ← merge gate
```
-50
View File
@@ -1,50 +0,0 @@
---
name: astro-architect
description:
Owns the Astro scaffold — config, routing, layouts, build pipeline, and the
publishing decision. Use for task 01 and any later change to astro.config.mjs,
package.json, or the deploy path. Do not use for component or page work.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You own the foundation. Everything other agents build sits on your decisions, so
wrong choices here are expensive and late-discovered.
**Read first**: `.agents/context/architecture.md`,
`.agents/context/publishing.md`, `.agents/rules/astro.md`. **Load skill**:
`astro-page`.
## You own
`astro.config.mjs`, `package.json`, `tsconfig.json`, `src/layouts/`, the CI
workflow, and the `pages`-branch publishing decision. You are the **only**
writer of these. Other agents report problems with them; they do not edit.
## Non-negotiable outcomes
- `base: '/ai-for-dummies'` set and **verified against the real host**, not just
`pnpm run preview`. Base-path bugs are the most likely production-only
failure.
- Every existing URL resolves identically, trailing slash included.
- Zero JS by default. Astro ships none unless a component asks.
- `hands-on/starter/` and `hands-on/rules/` copied to `public/` **verbatim,
unprocessed**. They are lab fixtures; the exercise is that they are plain
files.
- No UI framework, no CSS framework, no runtime dependencies.
## The publishing decision is yours to make and document
Astro emits `dist/`; the Gitea Pages Server serves a branch and cannot build.
Choose between committing `dist/` to `pages` and having Gitea Actions build it
(recommended), per `context/publishing.md`. Then **rewrite
`docs/operations-guide.md` in the same task** — it currently states `pages` is
"the exact published source", which your change makes false.
Known trap: this Gitea's act-runner registration lives in an `emptyDir`, so a
pod restart silently kills CI. Put that in the runbook.
## Done when
`pnpm run build` succeeds, one migrated page serves correctly from the real host
under `/ai-for-dummies/`, `pnpm run verify` and `node scripts/audit-ui.mjs` are
green, and the operations guide matches reality.
-46
View File
@@ -1,46 +0,0 @@
---
name: component-builder
description:
Builds one Astro component per task from the project templates. Use for the
component-extraction tasks (05-09). Do not use for page migration, token
changes, or verify.mjs.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You build **one component per task**, from a template, in your own worktree.
Many of you run in parallel — that is why templates and rules exist, so ten
agents produce one house style rather than ten.
**Read first**: `.agents/rules/componentization.md`, `.agents/rules/astro.md`,
`.agents/rules/theming.md`. **Load skills**: `astro-component`, `design-tokens`.
## Scope discipline
Your task names one component. If you find a second that "obviously" needs
extracting, **write it in your task report — do not build it.** Another agent
owns it, and two agents editing the same file is the failure mode worktrees
exist to prevent.
You may not edit `tokens.css`, `verify.mjs`, `astro.config.mjs`, or
`src/content/config.ts`. If you need a change there, report it.
## Rules that bite
- No raw hex, no px font sizes, no ad-hoc breakpoints. Tokens only.
- **Never reshape CSS to slip past `check-tokens.mjs`** — e.g. the `font:`
shorthand to hide a px size it would catch as `font-size:` — and never point a
legacy value at the nearest token that happens to exist. Both are silent
redesigns. Keep the true value and mark it
`/* token-gap: <reason>; owner design-system-keeper */`, which waives the
finding and queues it. You may not add tokens. See `.agents/rules/gates.md`.
- No `client:*` unless genuinely interactive, with written justification.
- Every ARIA attribute from the markup you replace survives. `verify.mjs`
asserts several by name.
- Keep the `gap:1px` over a coloured parent trick where the original used it.
- Under ~120 lines of markup. More means two components.
## Done when
`.agents/checklists/before-component.md` is fully checked, rendered text diffs
clean against the markup you replaced, screenshots at four widths look
unchanged, and `pnpm run verify` is green **with no assertion deleted**.
-58
View File
@@ -1,58 +0,0 @@
---
name: content-i18n-migrator
description:
Moves bilingual copy out of app.js and catalog.js into typed Astro content
collections without altering a string. Use for tasks 03-04 and any later
content relocation. Do not use for markup or styling.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You own `src/content/` and `src/content/config.ts`, and you are their only
writer. Your job is a lossless move, not an edit.
**Read first**: `.agents/rules/content-i18n.md`. **Load skill**:
`content-migration`.
## What you are moving
~50 `{ en, pt }` keys from `app.js` (`phases`, `handsOnPrompts`, `modelGuide`,
`skillSources`, `skillInstallPrompts`), plus 24 review entries from `catalog.js`
and `submitted-catalog.js`.
These are hand-written translations with deliberate tone. **Copy them
mechanically. Never retype.** Retyping introduces drift nobody notices until a
Portuguese speaker does.
## Procedure
Extract → write into collection → diff extracted-before against extracted-after
→ only then delete the source. If the diff is not empty, you changed content.
Fix it before continuing.
Both locales required in the schema. A missing `pt` must be a **build error**,
never a silent English fallback — that is how bilingual sites quietly become
monolingual.
## The trap that will catch you
`skill-reviews/improved/**/SKILL.md` is generated from `catalog.js` by
`scripts/build-skill-review.mjs` and the output is **committed**. Move
`catalog.js` and the generator keeps running against nothing — silently. Either
re-point it or replace it, and update `package.json`, `README.md`,
`docs/operations-guide.md`, and the review desk footer, all of which reference
it.
Also: the review desk's diff view compares original and improved **source
text**. If you convert `improved` to rendered Markdown, keep the raw string
available or the diff view breaks.
## The language-switching decision
Client-side swap (matches today, no URL change — recommended) vs route-based
`/en/` `/pt/` (better SEO, changes every URL, needs redirects). Surface it, get
a decision, record it. Either way `<html lang>` tracks the active language.
## Done when
The string diff is empty, both locales validate, the generator still produces
identical output, and `pnpm run verify` is green.
-51
View File
@@ -1,51 +0,0 @@
---
name: design-system-keeper
description:
Owns src/styles/tokens.css — the palette, type scale, and breakpoints. Use for
task 02 and any later token change or check-tokens failure. Do not use for
building components.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You own the token layer and are its only writer. Your job is to make "maintain
the same styles" true and verifiable.
**Read first**: `.agents/context/design-system.md` — it documents three drifting
palettes and a broken `@font-face`. **Load skills**: `design-tokens`,
`visual-regression`.
## The two decisions you must surface, not silently make
1. **Three palettes → one.** `--ink` exists as `#172f42`, `#122534`, `#173044`;
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
visibly different — screenshot both and get a human decision.
2. ~~**The fonts have never rendered.**~~ **Settled 2026-09-05 — do not
reopen.** The malformed `@font-face` was escalated and the human chose the
real fonts. Manrope and DM Mono are self-hosted in `public/fonts/`, wired
through `public/fonts/fonts.css`, which `BaseLayout.astro` links and the
legacy root `styles.css` `@import`s. **Do not delete these faces and do not
replace the stacks with `Arial`/`ui-monospace`** — that instruction is
obsolete. You may add `--font-sans` / `--font-mono` tokens pointing at them.
## You own
`src/styles/tokens.css`, `src/styles/base.css`, and
`.agents/scripts/check-tokens.mjs`.
Deliver: one value per token, a named type scale (`--step-*`) replacing 14
ad-hoc `clamp()` triples, five named breakpoints replacing sixteen, and an
enforcement script wired into `pnpm run verify`.
## Preserve the house style
Flat colour blocks, `1px` hairlines, near-zero radius, tight negative tracking
on display type, `Georgia, serif` emphasis spans, and the `gap:1px` over a
coloured parent trick used for grid separators. That last one is deliberate —
never convert it to `border`.
## Done when
Every token has exactly one value, `check-tokens.mjs` passes, and before/after
screenshots at 560/800/1100/1600 px **plus the eight removed breakpoint widths**
are attached to your task report with every visible difference explained.
-51
View File
@@ -1,51 +0,0 @@
---
name: motion-designer
description:
Adds and audits animation — transitions, state changes, optional view
transitions. Use for task 17 and any change involving movement. Do not use for
static layout or styling work.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You add motion to an editorial-print design where motion is punctuation, not
decoration. Your most valuable output is often **deciding not to animate**.
**Read first**: `.agents/rules/animation.md`. **Load skill**: `motion`.
## The question you must answer per animation
What does this motion tell the user that the static state does not? Valid:
something changed; attention needs directing to what changed; a layout shift
needs smoothing. "It feels more polished" is not valid — on this design it reads
as a generic template, which is the one thing this site's identity avoids.
Shipping a component static is a legitimate, common, correct outcome.
## Hard constraints
- `transform` and `opacity` only. Animating layout properties fails the 200ms
INP budget.
- 150250ms UI feedback, ≤400ms page transition. `cubic-bezier(.2,0,0,1)` in,
`ease-out` out. No bounce or elastic — wrong register.
- One thing moves at a time. No staggered card cascades.
- `prefers-reduced-motion` honoured **and tested** via DevTools emulation. The
end state must still be correct: reduced, not broken.
- No animation library. This site's thesis is having no runtime dependencies.
## View transitions
Astro's `<ClientRouter />` is the only sanctioned motion dependency. Before
enabling it, verify JS-disabled navigation, browser back/forward, the review
desk's query-param deep links, and reduced-motion — all still work.
## Also audit what exists
Several current stylesheets already honour `prefers-reduced-motion`. Inventory
existing motion, flag anything animating a layout property, and fix it. That is
often higher value than anything you add.
## Done when
Every animation has a written purpose, animates only compositor properties,
respects reduced motion under test, and screenshots of start and end states are
attached.
-46
View File
@@ -1,46 +0,0 @@
---
name: page-migrator
description:
Migrates one hand-written HTML page to an Astro route with identical URL,
content, and JS budget. Use for the page-migration tasks (10-16). Do not use
for component extraction or config changes.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You migrate **one page per task**. The bar is that a visitor cannot tell.
**Read first**: `.agents/context/architecture.md`,
`.agents/context/verification.md`. **Load skills**: `astro-page`,
`content-migration`, `visual-regression`.
## Snapshot before you touch anything
Rendered-text snapshot plus screenshots at 560/800/1100/1600 px, from the
vanilla site. Without a before, you cannot prove an after. This is not optional
and it is the step agents skip.
## Hard constraints
- **URL identical**, trailing slash included. Internal links go through
`import.meta.env.BASE_URL`, never a hand-written absolute path.
- **Query params survive** — the review desk uses `?author=`, `?skill=`,
`?view=`, `?file=`, `?compare=`, `?render=` and they are shared externally.
- **JS budget does not grow.** A page that shipped zero JS still ships zero.
Seven of ten pages are in that category.
- Every `data-*` hook, ARIA attribute, `<title>`, and `<meta>` preserved.
- Reuse existing components before creating new ones.
- Do **not** port `responsive.css` wholesale. Take what the page needs; prove
the rest is dead and delete it.
## hands-on/ is not yours
`hands-on/starter/` and `hands-on/rules/` are lab fixtures that ship from
`public/` unchanged. Their value is being plain dependency-free files an
attendee hands to an agent. If your task appears to ask you to componentize
them, it is wrong — stop and report.
## Done when
`.agents/checklists/before-page.md` complete, snapshot diff empty (or every line
justified), `node scripts/audit-ui.mjs` and `pnpm run verify` green, screenshots
compared, and your task report lists what you deliberately left alone.
-54
View File
@@ -1,54 +0,0 @@
---
name: reviewer
description:
Merge gate. Reviews a task branch diff against its brief and the project
rules. Use before merging any refactor task. Never writes features or fixes
findings itself.
tools: Read, Grep, Glob, Bash
---
You are the merge gate. You read diffs and report. **You do not write features
and you do not fix what you find** — you name it precisely enough that the
owning agent can.
**Read**: the task file, then every rule in `.agents/rules/` relevant to the
diff.
## Order of checks — highest-value first
1. **Coverage did not fall.** `grep -c 'throw new Error' scripts/verify.mjs`
against `origin/main`. A drop by anyone other than `verification-engineer`,
with a stated reason, is an automatic reject. This is the single most likely
way content silently disappears.
2. **Scope.** `git diff --stat origin/main` matches the brief. Contended files
(`tokens.css`, `verify.mjs`, `astro.config.mjs`, `src/content/config.ts`)
edited only by their owner.
3. **Content preserved.** Snapshot diff attached and empty, or every line
justified. No attached evidence means not reviewed — send it back.
4. **URLs and query params unchanged.** Trailing slashes. `BASE_URL` used
instead of hand-written absolute paths.
5. **JS budget.** A previously-zero-JS page still ships zero. Every `client:*`
has a written justification.
6. **Tokens.** No raw hex, px font sizes, or ad-hoc breakpoints outside
`tokens.css`.
7. **Accessibility.** ARIA attributes from the original survived. Native
elements. Focus ring intact.
8. **Motion.** Compositor properties only; `prefers-reduced-motion` honoured.
9. **Hygiene.** No `.serena/`, `__pycache__/`, `dist/`, or scratch files staged.
## Output format
```
path:line: <severity>: <problem>. <fix>.
```
Severities: `blocker` (content loss, coverage drop, URL change, scope
violation), `major` (rule violation, missing evidence), `minor` (style, naming).
No praise, no summary of what the diff does — the author knows. Findings only.
If there are none, say so in one line.
## What you do not do
Do not suggest improvements outside the task's scope. Scope creep at review time
is how a bounded task becomes an unbounded one. Note it as a follow-up instead.
-53
View File
@@ -1,53 +0,0 @@
---
name: verification-engineer
description:
Keeps scripts/verify.mjs meaningful across the migration and builds the
snapshot/visual-regression net. Use for tasks 18-19 and whenever a verify
assertion needs re-pointing. The only role permitted to reduce coverage.
tools: Read, Write, Edit, Bash, Grep, Glob
---
You own `scripts/verify.mjs`, `scripts/audit-ui.mjs`, `.agents/snapshots/`, and
the visual-regression tooling. You are their only writer, and the **only role
allowed to remove an assertion** — with a written reason per removal.
**Read first**: `.agents/context/verification.md`. **Load skills**:
`verify-contract`, `visual-regression`.
## Why the role exists
42 assertions pin this site's real content. They will all break during the
migration, and the natural agent response to a red test is to delete it. That
turns a content-loss bug into a green build. You are the check on that.
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease across the
migration.
## Translating, not deleting
- **Content tokens** (`data-phase="plan"`) → re-point at built output; the token
should survive rendering. If it does not, a component dropped content.
- **Implementation details** (`const phases`, `renderTree`) → these look
deletable and are not. They pin a feature. Replace with an output-level
assertion of the same feature.
- **Asset versions** (`app.js?v=…`) → assert the built HTML references a hashed
asset.
## Build the stronger net first
Token matching cannot catch a dropped paragraph. Land rendered-text snapshots
for all ten routes **before** the page migrations start, or the migrators have
no baseline. This is early, blocking work.
## Fix the audit gap
`audit-ui.mjs` rejects external `<script>`/`<link>` but misses external URLs
inside CSS — which is exactly how a broken Google Fonts `@font-face` got into
this "dependency-free" site. Add `@import`, `src: url(https:…)`, and
`url(https:…)` detection.
## Done when
Coverage has not fallen, every removal has a reason, snapshots exist for all ten
routes, `check-tokens.mjs` and the extended audit are wired into
`pnpm run verify`, and the suite runs green on the migrated site.
-18
View File
@@ -1,18 +0,0 @@
# Checklist: before you call a component done
- [ ] It appears (or will appear) in **three** places, or has a name a person
says out loud
- [ ] Lives in the right folder: `primitives/`, `blocks/`, or `islands/`
- [ ] Typed `interface Props`; every field intentional; no `any`
- [ ] **No raw hex, px font sizes, or ad-hoc breakpoints** — tokens only
- [ ] Ships zero JS, or has a `client:*` directive with a written justification
- [ ] Markup under ~120 lines
- [ ] Native elements: `<button>` for actions, `<a>` for navigation
- [ ] Keyboard: tab to it, operate with Enter/Space, focus ring visible
- [ ] Interactive state exposed via ARIA (`aria-pressed`, `aria-current`), not
just colour
- [ ] Any motion respects `prefers-reduced-motion` and animates only
`transform`/`opacity`
- [ ] Renders correctly at 560 / 800 / 1100 / 1600 px
- [ ] Both `en` and `pt` strings present; no hard-coded copy
- [ ] `pnpm run verify` green with no assertion deleted
-15
View File
@@ -1,15 +0,0 @@
# Checklist: before you merge a task branch
- [ ] `origin/main` **merged in** (never rebased), conflicts resolved in the
worktree
- [ ] `pnpm run verify` and `node scripts/audit-ui.mjs` both green
- [ ] **Assertion count in `verify.mjs` did not fall**
(`grep -c 'throw new Error' scripts/verify.mjs`)
- [ ] Only files in your task's scope changed — `git diff --stat origin/main`
matches the brief
- [ ] No contended file edited unless you own it (see `rules/git-worktrees.md`)
- [ ] No `.serena/`, `__pycache__/`, `dist/`, or scratch files staged
- [ ] Commit message: what, why, and what you deliberately did not do
- [ ] Task report written: verified behaviours, known gaps, follow-ups
- [ ] `reviewer` agent has read the diff
- [ ] Worktree and branch cleaned up after merge
-20
View File
@@ -1,20 +0,0 @@
# Checklist: before you call a page migrated
- [ ] URL identical to the old one, **trailing slash included**
- [ ] Any query params the old page honoured still work (`?author=`, `?skill=`,
`?view=`, `?file=`, `?compare=`, `?render=`)
- [ ] Rendered text content diffs clean against the pre-migration snapshot — or
every difference is listed and justified
- [ ] Same number of JS bytes or fewer; a previously-zero-JS page still ships
zero
- [ ] `<title>`, `<meta name="description">`, and `<meta name="viewport">`
preserved
- [ ] One `<h1>`; heading levels do not skip
- [ ] Every ARIA attribute from the original survived (`verify.mjs` asserts
several)
- [ ] No external `<script>`, `<link>`, `@import`, or `url()`
`node scripts/audit-ui.mjs` green
- [ ] Internal links go through `BASE_URL`, not hand-written absolute paths
- [ ] Screenshots at 560 / 800 / 1100 / 1600 px compared against the old page
- [ ] `<html lang>` correct and still switching with the language toggle
- [ ] `pnpm run verify` green with no assertion deleted
-76
View File
@@ -1,76 +0,0 @@
# Context: architecture, current and target
## Current (no build step)
Ten hand-written HTML pages, each linking its own CSS and one ES module:
| Route | Page | Script | Stylesheets |
| -------------------- | -------------------------- | ---------------------- | --------------------------------------------- |
| `/` | `index.html` | — | `chapters.css`, `landing.css` |
| `/full-guide/` | `full-guide/index.html` | `app.js` (50 KB) | `styles.css`, `responsive.css`, `audit.css` |
| `/summary/` | `summary/index.html` | — | `chapters.css` |
| `/models/` | `models/index.html` | — | `chapters.css` |
| `/agents/` | `agents/index.html` | — | `chapters.css` |
| `/skills/` | `skills/index.html` | `skills/app.js` | `skills/styles.css` |
| `/rules/` | `rules/index.html` | `rules/app.js` | `rules/styles.css` |
| `/skills-review/` | `skills-review/index.html` | `skills-review/app.js` | `skills-review/styles.css`, `change-lens.css` |
| `/hands-on/starter/` | lab fixture | own | own |
| `/hands-on/rules/` | lab fixture | own | own |
Weight is concentrated: `app.js` 50 KB, `responsive.css` 30 KB,
`skills-review/catalog.js` 27 KB, `skills-review/submitted-catalog.js` 18 KB.
### What each big file actually is
- **`app.js`** — not really application code. It is a **bilingual content
database** (`phases`, `handsOnPrompts`, `modelGuide`, `skillSources`,
`skillInstallPrompts`, each keyed `{en, pt}`) plus ~12 small `render*`
functions that swap `innerHTML` on tab clicks. ~50 `en:` keys. The content
should become data; only the tab behaviour is interactive.
- **`responsive.css`** — a 30 KB append-only layer of overrides bolted on top of
`styles.css`. Expect large parts to be dead once layout moves into components.
Do not port it verbatim.
- **`skills-review/catalog.js`** — the real data model of the review desk: one
entry per submitted skill with `id`, `author`, `title`, `status`, `focus`,
`wins[]`, `improve[]`, `extras`, `improved` (full markdown). 24 entries across
`catalog.js` + `submitted-catalog.js`. This is already a content collection in
all but name.
- **`skills-review/files.js` / `submitted-files.js`** — generated file
manifests.
- **`vote.js`** — the vote widget island; talks to `vote-service/`.
## Target (Astro)
```
src/
content/ catalog entries, chapter copy, EN/PT strings (typed collections)
layouts/ BaseLayout, ChapterLayout, GuideLayout
components/ .astro by default; islands only where marked
styles/ tokens.css, base.css, then per-component styles
pages/ routes mirroring today's URLs exactly
public/
hands-on/ lab fixtures copied verbatim, never processed
```
### Non-negotiables for the target
- **URLs do not change.** `/full-guide/`, `/skills-review/`,
`/hands-on/starter/` and the rest must resolve exactly as they do now,
trailing slash included. Existing links (including `docs/`, SilverBullet, and
shared URLs with `?author=…&skill=…&view=…` query params) must keep working.
- **Zero JS by default.** Seven of the ten pages ship no JavaScript today. They
must still ship none. Islands are opt-in, per component, and justified.
- **`hands-on/` stays vanilla.** It goes in `public/` untouched. It is a lab
fixture, not a component.
- **No external runtime requests.** `audit-ui.mjs` enforces this and it is part
of the site's thesis. Self-host anything you add.
- **The review desk's query-param deep links keep working** — `?author=`,
`?skill=`, `?view=`, `?file=`, `?compare=`, `?render=`. They are documented in
the page footer and shared externally.
## Companion service
`vote-service/` is a Go API on its own Kubernetes deploy cycle, reached by the
review desk over `window.SKILLS_REVIEW_VOTE_API`. The refactor does not touch
it. Keep the global, or replace it with a build-time `PUBLIC_VOTE_API` env var —
but if you do, update `vote-service/README.md` in the same change.
-40
View File
@@ -1,40 +0,0 @@
# Context: full-guide language switching
## Decision
The Astro full guide keeps the current client-side language switch on its
existing `/full-guide/` URL. It server-renders both locale variants and the
language-toggle island shows the selected variant after `client:idle` hydration.
This deliberately preserves the current no-URL-change contract, including links
shared without a locale segment, and avoids a route/redirect and publishing
change. The cost is duplicated localized HTML and both locales in the response.
That is acceptable for this small guide and avoids sending duplicated string
data through every interactive island.
## Markup and island contract for task 15d
- Render each static localized fragment twice. Put `data-language-content="en"`
or `data-language-content="pt"` on its outer element. English is visible in
server HTML; the toggle uses the native `hidden` attribute for the inactive
locale.
- Add `<LanguageToggle />` to the guide top bar. Astro's `client:idle` directive
is only valid for framework components; this `.astro` island defers its
browser setup with `requestIdleCallback` (and a timeout fallback) instead. Do
not hydrate the page or use `client:load`; the control is deliberately
idle-priority.
- The island owns the `ai-for-dummies-language` localStorage key. Every read and
write remains inside `try`/`catch`, because previews may disable storage.
- On each selection the island sets `<html lang>` to `en` or `pt-BR`, updates
its `[data-lang]` buttons' `.active` class and `aria-pressed` state, updates
`[data-language-content]`, then dispatches `ai-for-dummies:languagechange` on
`window`. The event detail is `{ language: 'en' | 'pt' }`.
- The guide selector island (15a) must read `document.documentElement.lang` when
it hydrates and listen for that event. On receipt it must re-render the
currently active phase and all active selector panels from their collection
data. This preserves todays `applyLanguage` behaviour without coupling the
toggle to page selectors.
This is a page-local contract: the existing `/rules/` toggle continues using its
own `rules-language` key and must not be changed as part of full-guide
migration.
-109
View File
@@ -1,109 +0,0 @@
# Context: the design system (as it actually is)
Read this before touching CSS. Everything here was extracted from the current
files, not assumed.
## There is no single palette. There are three.
The same semantic names carry different values depending on which stylesheet
loaded them:
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
| ---------- | -------------------------------- | ------------------------------------------ | ----------------------- |
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
| `--ink` | `#172f42` | `#122534` | `#173044` |
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
| `--line` | `#d8dee2` | `#d0d5d2` | `#d5dde1` |
| `--blue` | `#527f9f` | `#215675` | `#5683a1` |
| `--gold` | `#efc76b` | `#ebbf58` | `#efc86d` |
| `--accent` | `#7c78a8` | — | — |
| `--deep` | `#102536` | — | — |
| `--red` | — | `#a7483f` (chapters only) | — |
| `--violet` | — | `#6b668f` (review desk only) | — |
Most deltas are a few units per channel — drift, not intent. `--blue` is the
exception: `#527f9f` vs `#215675` is a visible difference and may be deliberate.
**Decision required before any component work** (task 02). Options:
- **Canonicalize** to one palette. Recommended. The sub-perceptual deltas
collapse; only `--blue` needs a human's eye on a before/after screenshot.
- **Keep three named surfaces** (`--surface-guide`, `--surface-chapter`,
`--surface-lab`) if the drift turns out to be intentional per section.
Do not "just pick one" silently in the middle of another task. This is its own
reviewed change with visual diffs attached.
## The typography — fixed 2026-09-05
`styles.css` line 1 used to read:
```css
@font-face {
font-family: Manrope;
src: url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap');
}
```
`src:` in an `@font-face` must point at a font binary. That URL returns a CSS
stylesheet, so no browser could load a face from it. For the whole life of the
site, every `font-family:Manrope,Arial,sans-serif` rendered as **Arial** and
every `font:… 'DM Mono',monospace` rendered as the **generic monospace** face —
`'DM Mono'` was never declared as a family at all.
**This was escalated and the human chose the real fonts.** Manrope and DM Mono
are now self-hosted in `public/fonts/`, latin and latin-ext subsets only, under
the SIL Open Font License. One `fonts.css` serves both trees: Astro links it
from `BaseLayout.astro`, the legacy root `styles.css` `@import`s it. Self-hosted
rather than linked from Google because `scripts/audit-ui.mjs` rejects any
external `<link>`/`<script>`, and because the site is presented in workshop
rooms with unreliable networks.
**This changed how every page renders**, deliberately. It is the one sanctioned
visual change in the migration. Screenshots taken before 2026-09-05 show Arial
and are no longer a valid baseline.
`Georgia, serif` for emphasis (`h1 em`, `.hero em`) is untouched and still real.
## Type scale
`Georgia, serif` is used deliberately for emphasis (`h1 em`, `.hero em`) and is
real — it is a system font, so it does render. Keep it.
Sizes are all `clamp()`, roughly:
| Role | Value |
| --------------- | --------------------------------------------------------- |
| Display / `h1` | `clamp(56px,9vw,126px)` |
| Section `h2` | `clamp(36px,5vw,65px)` |
| Sub-head | `clamp(24px,3vw,38px)` |
| Pull-quote | `clamp(22px,3vw,36px)` |
| Body | `15px/1.6``18px` |
| Eyebrow / label | `1011px` monospace, `letter-spacing:.08.1em`, uppercase |
There are 14+ distinct clamp triples doing near-identical jobs. Collapse to a
named scale (`--step-0``--step-6`) during tokenization; the visual result
should be unchanged within a pixel or two at common viewports.
## Breakpoints
Sixteen distinct max-widths are in use: 420, 520, 530, 560, 600, 620, 720, 800,
850, 880, 900, 1000, 1050, 1100 — plus `min-width:1600px` and
`min-width:2200px`.
Collapse to a named set (suggested: 560 / 800 / 1100 / 1600 / 2200) and prove
equivalence with screenshots at the _old_ breakpoint values, since that is where
regressions will hide.
`@media(prefers-reduced-motion:reduce)` is already respected in several
stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
## House style worth preserving
The visual identity is editorial-print: flat colour blocks, hairline `1px`
rules, uppercase monospace eyebrows with wide tracking, very tight negative
letter-spacing on display type (`-.06em``-.08em`), grid layouts with
`gap:1px` over a background colour to fake borders, and near-zero border-radius.
That last trick (`gap:1px` + parent background) is used everywhere. It is
intentional. Do not replace it with `border`.
-55
View File
@@ -1,55 +0,0 @@
# Context: publishing, and what a build step changes
## How it works today
The Gitea Pages Server serves the **`pages` branch tree directly**. There is no
build. `main` and `pages` end up with byte-identical trees; `pages` exists only
because the Pages Server publishes a branch, not a directory.
Live at `https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/`.
Full procedure, including the fact that `merge --ff-only main` fails (the
histories diverged), is in
[`../../docs/operations-guide.md`](../../docs/operations-guide.md).
## What Astro changes
Astro emits `dist/`. The Pages Server cannot run a build, so **something has to
put built output on `pages`**. Pick one, deliberately, in task 01:
| Option | How | Cost |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A. Build locally, commit `dist/` to `pages`** | `pnpm run build`, copy `dist/*` into the `pages` worktree, commit | `pages` stops being "the exact source". Diffs become unreadable. Publishing depends on one workstation. Simple, no new infra. |
| **B. Gitea Actions builds and pushes `pages`** | workflow on `main``pnpm install --frozen-lockfile && pnpm run build` → force-push `dist/` to `pages` | `pages` becomes a machine-owned branch (force-push is fine _because_ nothing else writes it). Needs the act-runner to be healthy. **Recommended.** |
| **C. Serve from a container instead** | drop Pages Server for an nginx pod behind the existing ingress | most control, most infra, changes the URL story |
**Recommended: B**, with A as the documented manual fallback for when the runner
is down. Note the known failure mode: this Gitea's act-runner registration lives
in an `emptyDir`, so a pod restart silently kills CI until re-registered. The
runbook must say "if the site stopped updating, check the runner first".
Whichever you pick, `docs/operations-guide.md` must be rewritten in the same
task — it currently promises `pages` is "the exact published source", and that
stops being true under A and B.
## Base path
The site is served from a **subdirectory**: `/ai-for-dummies/`. Astro needs
`base: '/ai-for-dummies'` in `astro.config.mjs`, and every internal link must go
through `import.meta.env.BASE_URL` or Astro's `<a href={...}>` helpers rather
than a hand-written absolute `/models/`.
This is the single most likely source of "works locally, 404s in production" in
this migration. Verify it on the real host, not just `pnpm run preview`.
## Verification before you call it published
```bash
curl -sS -o /dev/null -w '%{http_code}\n' \
"https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/?v=$(git rev-parse --short HEAD)"
```
The `?v=` cache-buster matters: the Pages Server caches, and a stale 200 looks
exactly like a successful deploy. Check one nested route
(`/ai-for-dummies/skills-review/`) and one static asset too — the base-path bug
shows up on assets first.
-54
View File
@@ -1,54 +0,0 @@
# Context: the verification contract
`scripts/verify.mjs` is 13 KB, 42 `throw new Error` sites, 16 checkpoints. It
reads 26 source files and asserts that specific **string tokens** appear in them
`data-phase="plan"`, `renderTree`, `.change-lens`,
`styles.css?v=20260904-vote-widget`, and so on.
## Why this matters more than it looks
These assertions are the only thing standing between this site and silent
content loss during a large refactor. They are also **all going to break**,
because they assert against files that will stop existing.
The failure mode to guard against: an agent runs `pnpm run verify`, sees red,
and "fixes" it by deleting the assertion. The suite goes green and the site
loses a section. **Deleting an assertion is a change that requires review, the
same as deleting a feature.**
## How the contract must evolve
Three kinds of assertion, three different fates:
| Kind | Example | Fate |
| ------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| **Content presence** | `'data-phase="plan"'` in `full-guide/index.html` | Re-point at built output (`dist/`) — the token should survive rendering. If it does not, the component dropped content. |
| **Implementation detail** | `'const phases'`, `'renderTree'` in `app.js` | Obsolete. Replace with an assertion about _behaviour or output_, never delete outright. |
| **Cache-busting version** | `'app.js?v=20260904-vote-widget'` | Obsolete — Astro hashes assets. Replace with "the built HTML references a hashed asset". |
**Rule: the assertion count must not fall.** Every removed token is replaced by
one that pins the same user-visible fact against the new architecture. The
verification engineer owns this and is the only role allowed to reduce coverage,
with a written reason per removal.
## The stronger check to add
Token-matching is brittle. During the migration, add a **rendered-output diff**:
snapshot the current site's DOM text content per route, then assert the Astro
build produces the same text. That catches dropped paragraphs the way token
matching cannot.
```bash
# before migrating a page, from the vanilla site:
node .agents/scripts/snapshot-route.mjs /models/ > .agents/snapshots/models.txt
# after: same script against dist/, diff must be empty (or reviewed)
```
See [`../skills/verify-contract/SKILL.md`](../skills/verify-contract/SKILL.md).
## Also in the suite
`scripts/audit-ui.mjs` asserts every page has a viewport meta and **no external
`<script>`/`<link>`**. Keep it and extend it: it currently misses external URLs
inside CSS (`@font-face src`, `@import`, `url()`), which is exactly how the
broken Google Fonts request in `styles.css:1` got in.
-53
View File
@@ -1,53 +0,0 @@
# Rule: accessibility
## The gate is a keyboard, not a scanner
Automated tooling catches roughly **57%** of accessibility defects, and the
misses cluster exactly where usability is decided: focus visibility, focus
obscured by sticky elements, target size, and drag alternatives. Run axe, then
do the manual sweep anyway.
**Manual sweep, every interactive component:**
1. Tab through it. Every control reachable, in a sensible order.
2. Focus ring visible at every stop — this site uses
`outline: 3px solid #a7483f; outline-offset: 2px`. Keep it.
3. Operate it with Enter and Space. Escape closes anything that opened.
4. Nothing is reachable only by hover or only by pointer.
5. Zoom to 200%. Nothing clipped, nothing overlapping.
## Semantics
- Native elements first. `<button>` for actions, `<a>` for navigation. A `<div>`
with a click handler is a defect, not a style choice.
- ARIA only when HTML cannot express it. The current code does this well —
`role="tablist"`, `aria-pressed`, `aria-current="page"`, `aria-label` on
regions. Preserve every one during migration; they are asserted in
`verify.mjs`.
- One `<h1>` per page. Heading levels never skip.
- Every image needs `alt`. Decorative images get `alt=""`.
## State
Interactive state must be exposed, not just painted:
```html
<!-- wrong: only colour says it is selected -->
<button class="active">Improved draft</button>
<!-- right -->
<button class="active" aria-pressed="true">Improved draft</button>
```
The vote widget and preview switcher already do this. Match them.
## Contrast
Body text ≥ 4.5:1, large text ≥ 3:1, UI boundaries ≥ 3:1. Check any new
combination against the token palette — `--muted` on `--paper` is the pair most
likely to fail; verify before shipping.
## Bilingual content
`<html lang>` must change with the language toggle, not just the text. Screen
readers pick pronunciation from it. This already works today — do not regress
it.
-63
View File
@@ -1,63 +0,0 @@
# Rule: animation
This is an editorial-print design. Motion is punctuation, not decoration.
## Budget
- **Purpose or nothing.** Motion may signal a state change, direct attention to
what just changed, or smooth a layout shift. Nothing else.
- Duration: **150250ms** for UI feedback, up to 400ms for a page transition.
Longer reads as sluggish; shorter reads as a glitch.
- Easing: `cubic-bezier(.2,0,0,1)` for entrances, `ease-out` for exits. Never
`linear` for anything a person watches. Never bounce/elastic — wrong register
for this design.
- One thing moves at a time. Staggered cascades of cards are a template default;
this site has a point of view and does not do them.
## `prefers-reduced-motion` is mandatory
Several current stylesheets already honour it. Every new animation must:
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
Reduced motion means _reduced_, not _broken_: the end state must still be
correct and the interface still usable. Test it — in DevTools, Rendering →
Emulate `prefers-reduced-motion`.
## Performance
- Animate **`transform` and `opacity` only.** They composite on the GPU.
Animating `width`, `height`, `top`, `left`, or `margin` forces layout on every
frame and will show up as a failed INP.
- `will-change` only on an element about to animate, removed after. Leaving it
on permanently costs memory and can _hurt_ performance.
- Prefer CSS transitions. Reach for the Web Animations API only for sequencing
that CSS cannot express. Do not add an animation library — it is a runtime
dependency on a site whose thesis is having none.
- INP budget is **200ms**. An animation that delays interaction response fails.
## Astro view transitions
If page transitions are wanted, use Astro's `<ClientRouter />`. It is the only
sanctioned motion dependency, and it must:
- degrade cleanly with JS disabled (it does — full navigation)
- respect `prefers-reduced-motion`
- not break the review desk's query-param deep links or browser back/forward
## Accessibility
- Never animate anything that conveys information on its own. Motion is
redundant reinforcement.
- Nothing flashes more than three times per second.
- Focus must stay visible throughout a transition, and focus order must not
change because of one.
-75
View File
@@ -1,75 +0,0 @@
# Rule: Astro
Binding for every `.astro` file.
## Zero JS is the default
A component ships no JavaScript unless it has a `client:*` directive. Seven of
this site's ten pages ship no JS today and must continue to.
- Never add `client:load` without justifying it in the PR description.
- Prefer, in order: no JS → `client:visible``client:idle``client:load`.
- An island is a **leaf**, not a wrapper. Hydrate the tab panel, not the page.
## Islands in this project
Only these need interactivity. Anything else claiming island status is wrong:
| Island | Why | Directive |
| --------------------------------- | ---------------------------------- | ---------------- |
| Guide phase/tab switchers | click-driven panel swap | `client:visible` |
| Review desk catalog + file viewer | search, filter, fetch source files | `client:load` |
| Vote widget | talks to `vote-service/` | `client:visible` |
| Language toggle | swaps EN/PT across the page | `client:idle` |
## Structure
```astro
---
// 1. imports
// 2. Props interface
// 3. destructure Astro.props
// 4. derived values — no side effects, no fetch in components
---
<!-- markup -->
<style>
/* component-scoped */
</style>
```
- Typed props always: `interface Props { … }`, then `const { … } = Astro.props`.
- Data loading belongs in `src/content/` collections or the page frontmatter,
not inside a component.
- No barrel files (`index.ts` re-export hubs). They cost tree-shaking and invite
cycles.
## Content collections
All copy lives in `src/content/`, typed with a Zod schema in
`src/content/config.ts`. The review desk's `catalog.js` maps onto a collection
almost one-to-one — do that rather than importing a 27 KB JS file.
## Styles
- Component styles go in the component's `<style>` block. Astro scopes them.
- Only tokens and true resets live in global CSS.
- Do not port `responsive.css` verbatim. It is an override layer whose reason
for existing disappears once layout is componentized. Port what a component
needs, prove the rest is dead, delete it.
## URLs and the base path
The site is served from `/ai-for-dummies/`. Set `base` in `astro.config.mjs` and
never hand-write an absolute internal path. Use `import.meta.env.BASE_URL`.
Existing routes are load-bearing and must not change, including trailing slashes
and the review desk's query params.
## Never
- No UI framework (React/Vue/Svelte) unless a task brief explicitly calls for
it. Astro components plus a little vanilla JS cover everything here.
- No CSS framework. This site has a hand-built visual identity — see
[`theming.md`](theming.md).
- No external runtime requests. Self-host. `audit-ui.mjs` enforces it.
-48
View File
@@ -1,48 +0,0 @@
# Rule: code style
## Match what is there
This codebase has a real voice: dense one-liner CSS, terse ES modules, comments
that explain _why_ and never _what_. Do not reformat it into someone else's
house style as a side effect of a task.
The one exception is CSS minification-by-hand — `styles.css` is single-line and
unreadable. Component `<style>` blocks should be normally formatted. That is an
improvement, not a style disagreement.
## Comments
Write the comment that stops the next person from making a mistake. The existing
codebase does this well:
```js
// The cluster's nginx ingress runs with `use-forwarded-headers` off, so it
// *overwrites* X-Forwarded-For with its own downstream peer — the VPS's
// tailnet address — which would collapse every visitor into a single voter.
```
That comment earns its place. `// set the colour` does not.
## Naming
- Components `PascalCase.astro`; everything else `kebab-case`.
- Booleans read as assertions: `isOpen`, `hasVoted`, not `open`, `voted`.
- No abbreviations that are not already in the codebase's vocabulary.
## TypeScript
Astro brings TS. Use it: typed props, typed collections, `strict` on. Never
`any` — if the type is genuinely unknown, `unknown` plus a narrow.
## Dead code
Delete it. Do not comment it out, do not leave it behind a flag. Git remembers.
This matters here specifically: `responsive.css` is 30 KB of accumulated
overrides, and the temptation during migration will be to port it wholesale
"just in case". Prove each rule is needed or drop it.
## Commits
Present tense, lowercase, `type: subject`, matching the existing log (`feat:`,
`fix:`, `docs:`). The body explains why, and states what you did not do.
-54
View File
@@ -1,54 +0,0 @@
# Rule: componentization
## When to make a component
Extract when the same markup appears **three times**, or when a block has a name
a person would use out loud ("the eyebrow", "the route card", "the phase
panel").
Do not extract on the second occurrence. Two similar blocks often diverge; the
premature abstraction costs more than the duplication.
## Sizes
- A component that exceeds ~120 lines of markup is doing two jobs. Split it.
- A page that is a bare list of components with no markup of its own has been
over-split. Pages are allowed to contain layout.
## Boundaries
```
src/components/
primitives/ Eyebrow, Rule, Callout, CodeBlock — no domain knowledge
blocks/ RouteCard, PhasePanel, HandoffTable, SkillPackage — composed, page-agnostic
islands/ interactive only; each one justified per rules/astro.md
```
- Primitives never import blocks.
- Blocks never import page-specific data; they take props.
- Islands are leaves. An island must not wrap static children that could have
been server-rendered.
## Props
- Typed `interface Props`, every field. No `any`, no untyped rest spread.
- Required by default. Optional props need a default and a reason.
- Pass data, not markup. If you find yourself passing an HTML string, you want a
`<slot>`.
## Named exports, no barrels
Import the file you need. Barrel `index.ts` files break tree-shaking and create
import cycles; bulletproof-react advises against them and so do we.
## The catalog is data, not components
The 24 review-desk entries are content, not 24 components. One
`SkillReviewCard.astro` iterating a typed collection. If you are writing the
25th near-identical component, stop and model the data.
## Do not componentize
`hands-on/starter/` and `hands-on/rules/` are lab fixtures. Their whole value is
being flat, dependency-free files an attendee hands to an agent. They ship from
`public/` unchanged.
-59
View File
@@ -1,59 +0,0 @@
# Rule: content and i18n
## Every user-visible string is content
No hard-coded copy in components. Strings live in `src/content/`, typed with
Zod, and reach components as props or collection entries.
## The existing shape
`app.js` already stores content as `{ en: '…', pt: '…' }` objects across
`phases`, `handsOnPrompts`, `modelGuide`, `skillSources`, and
`skillInstallPrompts` — about 50 `en:` keys. That shape is fine and should be
carried across, not redesigned:
```ts
// src/content/config.ts
const localized = z.object({ en: z.string(), pt: z.string() });
```
Both locales are **required**. A missing `pt` must be a build error, not a
silent English fallback — that is how bilingual sites quietly become
monolingual.
## Migrating strings
Copy, do not retype. These are hand-written translations with specific tone
(`'Transforme ambiguidade em trabalho'`). Retyping introduces typos and drift.
Move the literal, then diff the extracted content against the original file to
prove nothing changed:
```bash
node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
node .agents/scripts/extract-strings.mjs src/content/guide/ > /tmp/after.json
diff /tmp/before.json /tmp/after.json
```
## Language switching
Today the toggle swaps text client-side and updates `<html lang>`. Two options
for Astro, decide in task 03:
- **Keep client-side swap.** Both languages ship in the payload. Zero routing
change, matches today exactly, no URL work. Recommended — the content is small
and the current behaviour is already what people link to.
- **Route-based (`/en/`, `/pt/`).** Better SEO, more correct, but it changes
every existing URL. Only do this with an explicit decision plus redirects.
Whichever you pick, `<html lang>` must track the active language.
## Rendered content
Markdown in `catalog.js` entries (the `improved` field) should become real
Markdown files in the collection, rendered at build time rather than by a
hand-rolled client-side renderer. That deletes code and improves fidelity.
Careful: `skill-reviews/improved/**/SKILL.md` is **generated** from those
entries by `scripts/build-skill-review.mjs`, and the generated files are
committed. Keep that generator working, or replace it and update every reference
to it.
-123
View File
@@ -1,123 +0,0 @@
# Rule: quality gates
Three tiers. Each is scoped so that **many agents committing in parallel
worktrees stay fast** — the whole point is that a gate you are tempted to skip
is not a gate.
| Tier | Hook | Scope | Budget | Runs |
| ---- | ------------------ | ------------------------------------- | ------- | -------------------- |
| 1 | `pre-commit` | **staged files only** (lint-staged) | < 3s | every commit |
| 2 | `pre-push` | whole project: build + verify + audit | < 90s | every push |
| 3 | CI (Gitea Actions) | tier 2 + lint, on a clean install | minutes | every push to `main` |
Tier 1 must stay under a few seconds. If it creeps, move the check to tier 2. An
agent that waits 40s per commit will start passing `--no-verify`, and then you
have no gate at all.
## Tier 1 — pre-commit (lint-staged)
Formats and lints **only what you staged**. Scoped by construction, so ten
parallel worktrees do ten small jobs, not ten full-project sweeps.
- Prettier + ESLint on `*.{js,mjs,ts,astro}`
- Prettier + Stylelint on `*.css`
- `check-tokens.mjs` on changed `.astro`/`.css` — catches raw hex before it
lands
- Prettier on `*.{json,md}`
## Tier 2 — pre-push
The real gate:
```
astro check types
astro build it compiles
verify.mjs 42 content assertions — count must not fall
audit-ui.mjs no external runtime dependencies
check-tokens.mjs full sweep
```
## Tier 3 — CI
Tier 2 plus `pnpm run lint`, run against a clean
`pnpm install --frozen-lockfile` — which is the part a local worktree cannot
prove.
Screenshot comparison against `.agents/snapshots/` is **not** wired in yet.
`visual-regression.mjs` can only capture baselines, not diff them, so running it
in CI would overwrite the baselines and pass unconditionally. It also needs
Playwright, which is not a project dependency. Add the step back when the script
grows a compare mode.
## Bypassing
`--no-verify` is allowed exactly once: a work-in-progress commit **on your own
task branch that you will amend or squash away**. It is never allowed on a
commit you intend to merge, and the pre-push gate has no bypass.
If a gate is wrong, fix the gate in its own commit. Do not route around it.
## Never restructure code to slip past a checker
A checker is a proxy for a rule. Passing the proxy while breaking the rule is
worse than failing, because failure is visible and this is not.
`check-tokens.mjs` matches `font-size: Npx`. Writing the same value as the
`font:` shorthand passes it. Task 07 did exactly that, in **two** components,
with a comment saying so. Both hardcoded values survived into a "green" branch.
### Do not substitute a near-miss token either
The second way to break this is subtler, and both tasks 10 and 11 did it: keep
the gate happy by pointing a legacy value at the closest token that already
exists. `#e5eeeb` became `var(--paper)`. Diff-**added** green became
`var(--accent)` — purple. `12px` and `14px` both became `var(--step-1)`, 15px.
That is a silent redesign, and it is _worse_ than leaving the raw value in,
because a raw hex is at least honest about being unresolved.
### What to do instead: mark the gap
`tokens.css` has one owner (`design-system-keeper`) so that "add a token" is a
decision, not a side effect. You may not add one. You **can** keep the true
value and stay green — mark it:
```css
/* token-gap: no --step-* covers 12px; owner design-system-keeper */
font-size: 12px;
```
The marker waives that one finding. It needs a real reason after the colon; a
bare `token-gap:` is rejected. Every marked value is listed on each run, so the
debt stays visible rather than disappearing.
Write it in your task report as well: selector, legacy value, owning file.
Marking a gap is not resolving it — it keeps the site truthful until whoever
owns the token layer decides.
## Parallelism
- Hooks are **per-worktree**. Git's `index.lock` is per-worktree, so parallel
commits do not contend.
- The heavy tier-2 gate takes a **lock** (`.git/af-gate.lock`, shared across
worktrees) so ten agents pushing at once do not run ten concurrent builds and
thrash the machine. Waiters queue; they do not fail.
- `pnpm install --frozen-lockfile` in a fresh worktree is cheap: pnpm hardlinks
from the shared content-addressable store, so a second worktree costs seconds
and almost no disk instead of another 225 MB. No `--prefer-offline` needed.
## The silent-failure mode you must know about
Husky sets `core.hooksPath` to `.husky/_`, and **`.husky/_` is generated by
`pnpm install`, not committed**. A fresh `git worktree add` therefore has hooks
configured but the directory missing — so **hooks silently do not run**. Every
commit passes. Nothing is checked.
`.agents/scripts/worktree.sh start` runs the install and then verifies. Check
manually any time you did not use it:
```bash
.agents/scripts/verify-hooks.sh
```
Run this first in any worktree you did not create with the script.
-85
View File
@@ -1,85 +0,0 @@
# Rule: git worktrees
This project teaches worktrees. It should use them properly.
## One task, one worktree, one agent
```bash
# from the main checkout
git worktree add ../af-task-07 -b refactor/task-07-route-cards
cd ../af-task-07
pnpm install --frozen-lockfile
```
Naming: directory `../af-task-NN`, branch `refactor/task-NN-<slug>`. Both
derived from the task file so the mapping is never ambiguous.
## Why isolation matters here
The migration runs many agents in parallel over the same small set of files
(`tokens.css`, `verify.mjs`, `astro.config.mjs` are contended). Worktrees give
each agent its own working directory over one object store — cheap, and no agent
can see another's half-finished state.
The failure mode without them: two agents both "fix" `verify.mjs`, and the
second overwrites the first's assertions.
## Contended files
These are touched by many tasks. Whoever owns them per the plan is the **only**
writer; everyone else opens an issue in their task report instead of editing:
| File | Owner |
| ---------------------------------- | ----------------------- |
| `src/styles/tokens.css` | `design-system-keeper` |
| `scripts/verify.mjs` | `verification-engineer` |
| `astro.config.mjs`, `package.json` | `astro-architect` |
| `src/content/config.ts` | `content-i18n-migrator` |
## Never rebase a task branch
Not onto `main`, not onto another task branch, not "just to tidy up". A rebase
rewrites every commit with a new SHA, so a branch that others branched from — or
that has already been merged — turns into a duplicate history that no longer
shares an ancestor with the original. Task 03 did this and flattened task 01's
merge into four look-alike commits; it is the same divergent-history trap that
broke the `pages` branch.
To pick up new work from `main`, **merge it in**: `git merge origin/main`. The
extra merge commit is the price of a history that stays true, and it is cheap.
## Before you start
1. `git fetch origin` — then branch from the ref your task file names as its
base. Do not assume that base is `main`.
2. Read your task file end to end before writing anything.
3. Confirm your task's dependencies are merged. Task files list them.
## Before you finish
1. `pnpm run verify` green — without deleting assertions.
2. The relevant checklist in [`../checklists/`](../checklists/) complete.
3. `git fetch origin && git merge origin/main`, resolve conflicts in your
worktree. **Merge — never rebase.**
4. Task report: what changed, what you verified, **what you did not do**.
## Cleanup
```bash
cd - # back to the main checkout
git worktree remove ../af-task-07
git branch -d refactor/task-07-route-cards
```
Stale worktrees hold locks and confuse the next agent. `git worktree list`
should be short.
## Never
- Never work directly on `main`.
- Never force-push a shared branch. The `pages` branch is the sole exception,
and only if CI owns it (see
[`../context/publishing.md`](../context/publishing.md)).
- Never `git add -A` from the repository root. This repo has untracked local
scratch (`.serena/`, `scripts/inspect.py`) that must not be swept into a
commit.
-81
View File
@@ -1,81 +0,0 @@
# Rule: theming
Binding for every colour, font size, spacing value, and breakpoint.
**Read [`../context/design-system.md`](../context/design-system.md) first.** The
current CSS has three drifting palettes and a broken `@font-face`. This rule
describes the target; that file describes what you are migrating from.
## One token layer
Every value comes from `src/styles/tokens.css`. If a component needs a value
that is not a token, either it is a genuine one-off (justify it in a comment) or
the token layer is missing something (add it there, not inline).
```css
/* forbidden */
color: #172f42;
color: rgba(23, 47, 66, 0.6);
/* required */
color: var(--ink);
color: var(--muted);
```
No raw hex outside `tokens.css`. `.agents/scripts/check-tokens.mjs` enforces it;
wire it into `pnpm run verify`.
## Semantic names, not literal ones
`--ink`, `--paper`, `--muted`, `--line`, `--accent`, `--gold`, `--blue`,
`--deep` are the existing vocabulary. Keep it — it is already semantic and the
team reads it fluently. Do not rename to `--color-neutral-900`.
If a genuine second surface is needed, extend semantically (`--surface-lab`,
`--ink-inverse`), never numerically.
## Type scale
Replace the 14 ad-hoc `clamp()` triples with named steps:
```css
--step-display: clamp(56px, 9vw, 126px); /* h1 */
--step-6: clamp(36px, 5vw, 65px); /* section h2 */
--step-5: clamp(24px, 3vw, 38px); /* sub-head */
--step-4: clamp(22px, 3vw, 36px); /* pull-quote */
--step-1: 15px; /* body */
--step-0: 11px; /* eyebrow / label */
```
The eyebrow treatment (`1011px` monospace, `letter-spacing:.08.1em`,
uppercase) is a signature of this design. Make it one class, not fifteen
repetitions.
## Breakpoints
Five named widths replace the current sixteen:
```css
--bp-sm: 560px;
--bp-md: 800px;
--bp-lg: 1100px;
--bp-xl: 1600px;
--bp-2xl: 2200px;
```
When collapsing a component's old breakpoint onto a named one, screenshot at the
**old** value. That is where the regression will be.
## Preserve the house style
- Flat colour blocks, hairline `1px` rules, near-zero border-radius.
- Tight negative tracking on display type (`-.06em``-.08em`).
- Grid separators built as `gap:1px` over a coloured parent background. This is
deliberate. Do not "fix" it into `border`.
- `Georgia, serif` for emphasis spans (`h1 em`). It renders today; keep it.
## Fonts
Do not add a webfont without an explicit decision recorded in the task. The
intended Manrope/DM Mono has never rendered; introducing it is a visual
redesign, not a refactor. Default: match what renders today.
-149
View File
@@ -1,149 +0,0 @@
#!/usr/bin/env node
// Fails when a raw colour, px font-size, or ad-hoc breakpoint appears outside
// the token layer. A rule nobody checks is a suggestion — wire this into
// `pnpm run verify`.
//
// ESCAPE HATCH — `token-gap:`. Some legacy values have no token yet, and only
// `design-system-keeper` may add one. Without an escape, an agent told both
// "keep the site identical" and "get the gate green" has to break one of them,
// and tasks 10 and 11 both broke the first: `#e5eeeb` became `var(--paper)`,
// diff-added green became `var(--accent)` purple. Substituting a near-miss
// token is a silent redesign; it is worse than a raw value, because the raw
// value is at least honest about what it is.
//
// So: mark the line, keep the true value, stay green.
//
// /* token-gap: no --step-* covers 12px; owner design-system-keeper */
// font-size: 12px;
//
// Marked values are counted and listed on every run — they are a visible debt
// queue, not a way to make the finding disappear. The marker needs a reason;
// a bare `token-gap:` does not count.
//
// Usage: node .agents/scripts/check-tokens.mjs [srcDir]
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
// lint-staged appends staged file paths; a bare run sweeps `src`.
const ARGS = process.argv.slice(2);
const TOKEN_FILES = ['tokens.css', 'base.css'];
const ALLOWED_BREAKPOINTS = ['560px', '800px', '1100px', '1600px', '2200px'];
const walk = (dir) =>
readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
return statSync(path).isDirectory() ? walk(path) : [path];
});
const targets = ARGS.length
? ARGS.flatMap((arg) => (statSync(arg).isDirectory() ? walk(arg) : [arg]))
: walk('src');
const findings = [];
const gaps = [];
// A finding is waived when its own line, or the line above it, carries a
// `token-gap:` marker with a reason after the colon.
const MARKER = /token-gap:([^\n]*)/;
// The reason is what is left after the marker once the comment terminator and
// punctuation are stripped. `/* token-gap: */` is not a reason.
const reason = (line) => {
const found = MARKER.exec(line ?? '');
if (!found) return null;
const text = found[1]
.replace(/\*\/\s*$/, '')
.replace(/[\s*/]+$/, '')
.trim();
return /[a-z0-9]/i.test(text) ? [null, text] : null;
};
const waiver = (lines, index) =>
reason(lines[index]) || (index > 0 ? reason(lines[index - 1]) : null);
for (const path of targets) {
if (!['.astro', '.css'].includes(extname(path))) continue;
if (TOKEN_FILES.some((allowed) => path.endsWith(allowed))) continue;
const lines = readFileSync(path, 'utf8').split('\n');
lines.forEach((line, index) => {
const at = `${path}:${index + 1}`;
const waived = waiver(lines, index);
const record = (finding) => {
if (waived) gaps.push(`${at}: ${finding.slice(at.length + 2)} [${waived[1]}]`);
else findings.push(finding);
};
// Raw hex — the drifted-palette failure mode this whole layer exists to stop.
const hex = line.match(/#[0-9a-fA-F]{3,8}\b/g);
if (hex) record(`${at}: raw hex ${hex.join(', ')} — use a token from tokens.css`);
// rgb()/hsl() literals are the same problem wearing a different hat.
if (/\b(rgba?|hsla?)\(\s*\d/.test(line)) record(`${at}: raw colour function — use a token`);
// Hard-coded font sizes bypass the type scale.
const fontSize = line.match(/font-size:\s*\d+(\.\d+)?px/);
if (fontSize) record(`${at}: hard-coded ${fontSize[0]} — use var(--step-*)`);
// Ad-hoc breakpoints are how sixteen of them accumulated last time.
const media = line.match(/@media[^{]*?\(\s*(?:max|min)-width:\s*(\d+px)/);
if (media && !ALLOWED_BREAKPOINTS.includes(media[1]))
record(
`${at}: breakpoint ${media[1]} is not a named one (${ALLOWED_BREAKPOINTS.join(', ')})`,
);
});
}
if (gaps.length) {
console.log(`token check: ${gaps.length} marked token-gap(s) awaiting design-system-keeper:\n`);
gaps.forEach((gap) => console.log(` ${gap}`));
console.log('');
}
if (findings.length) {
console.error(`token check failed — ${findings.length} finding(s):\n`);
findings.forEach((finding) => console.error(` ${finding}`));
process.exit(1);
}
import { existsSync } from 'node:fs';
import { basename } from 'node:path';
if (existsSync('dist')) {
const builtCss = walk('dist').filter((p) => p.endsWith('.css'));
const tokensBuilt = builtCss.find((p) => /[\\/]tokens\.[^\\/]+\.css$/.test(p));
if (!tokensBuilt) {
console.error('token check failed — tokens.css was not built into dist/');
process.exit(1);
}
const tokensContent = readFileSync(tokensBuilt, 'utf8');
if (!tokensContent.includes('#527f9f')) {
console.error(
'token check failed — built tokens.css does not contain the canonical --blue value #527f9f',
);
process.exit(1);
}
const htmlFiles = walk('dist').filter(
(p) =>
p.endsWith('.html') &&
!p.includes('/hands-on/') &&
!p.includes('\\hands-on\\') &&
!p.includes('/submitted-skills/') &&
!p.includes('\\submitted-skills\\'),
);
const tokenChunkName = basename(tokensBuilt);
for (const html of htmlFiles) {
const content = readFileSync(html, 'utf8');
if (!content.includes(tokenChunkName)) {
console.error(
`token check failed — ${html} does not load the token layer (${tokenChunkName})`,
);
process.exit(1);
}
}
}
console.log('token check passed');
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env node
// Mechanically extract every { en, pt } string pair from a file or directory,
// sorted and normalised, so a content migration can be proven lossless:
//
// node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
// node .agents/scripts/extract-strings.mjs src/content/ > /tmp/after.json
// diff /tmp/before.json /tmp/after.json
//
// A non-empty diff means you altered content. These are hand-written
// translations with deliberate tone — copy them, never retype them.
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
const target = process.argv[2];
if (!target) {
console.error('usage: extract-strings.mjs <file|dir>');
process.exit(2);
}
const walk = (dir) =>
readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
return statSync(path).isDirectory() ? walk(path) : [path];
});
const files = statSync(target).isDirectory() ? walk(target) : [target];
// Matches `en: '…'` / "en": "…" and the pt counterpart, single or double quoted,
// tolerating escaped quotes inside.
const PAIR = /["']?\b(en|pt)\b["']?\s*:\s*(['"])((?:\\.|(?!\2)[\s\S])*)\2/g;
const strings = [];
for (const file of files) {
const source = readFileSync(file, 'utf8');
for (const match of source.matchAll(PAIR)) {
strings.push({ lang: match[1], value: match[3] });
}
}
// Sort so file ordering and structure changes do not show up as content changes.
strings.sort((a, b) => (a.lang + a.value).localeCompare(b.lang + b.value));
console.log(JSON.stringify(strings, null, 2));
console.error(`extracted ${strings.length} strings from ${files.length} file(s)`);
-70
View File
@@ -1,70 +0,0 @@
#!/usr/bin/env bash
# Tier 2 gate: types, build, content contracts, dependency audit, tokens.
# Invoked by .husky/pre-push, and safe to run by hand at any time.
#
# Parallel-safe: takes a lock in the SHARED git dir, so ten agents pushing from
# ten worktrees queue instead of running ten concurrent Astro builds and
# thrashing the machine. Waiters block; they do not fail.
set -euo pipefail
root=$(git rev-parse --show-toplevel)
cd "$root"
# --git-common-dir resolves to the ONE shared .git across all worktrees, which
# is exactly the scope we want the lock to cover.
common=$(git rev-parse --git-common-dir)
lock="$common/af-gate.lock"
exec 9>"$lock"
if ! flock -n 9; then
echo "gate: another worktree is running the gate — waiting for it…"
flock 9
fi
started=$(date +%s)
step() { printf '\n\033[1m▸ %s\033[0m\n' "$1"; }
# Fail loudly rather than passing vacuously when the toolchain is not installed.
if [ ! -d node_modules ]; then
echo "gate: node_modules missing — run 'pnpm install --frozen-lockfile' first" >&2
exit 1
fi
# This project is pnpm-only. An agent that runs `npm install` out of habit gets
# a second, divergent dependency tree and a lockfile nobody reads — the same
# class of failure that cost two sessions during phase 0. Catch it here.
if [ -f package-lock.json ] || [ -f yarn.lock ] || [ -f bun.lock ] || [ -f bun.lockb ]; then
echo "gate: a non-pnpm lockfile is present. This project uses pnpm only." >&2
echo " Delete it, then run 'pnpm install --frozen-lockfile'." >&2
exit 1
fi
step "types"
pnpm exec astro check
step "build"
pnpm run build
step "content contracts"
pnpm run verify
# The assertion count is the thing agents are most tempted to "fix" downward.
# Compare against origin/main and refuse a silent reduction.
step "assertion coverage"
current=$(grep -c 'throw new Error' scripts/verify.mjs)
baseline=$(git show origin/main:scripts/verify.mjs 2>/dev/null | grep -c 'throw new Error' || echo "$current")
if [ "$current" -lt "$baseline" ]; then
echo "gate: verify.mjs coverage fell from $baseline to $current assertions." >&2
echo " Only verification-engineer may reduce it, with a reason per removal." >&2
echo " See .agents/context/verification.md" >&2
exit 1
fi
echo " $current assertions (baseline $baseline)"
step "runtime dependency audit"
node scripts/audit-ui.mjs
step "design tokens"
node .agents/scripts/check-tokens.mjs
printf '\n\033[32mgate passed\033[0m in %ss\n' "$(( $(date +%s) - started ))"
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# `.agents/` is the vendor-neutral home for this project's rules, skills, and
# agent definitions — MiniMax, Gemini, and Codex all read plain files from it.
#
# Claude Code, however, discovers subagents and skills at fixed paths. This
# symlinks them so there is exactly ONE copy of every definition and no drift.
#
# .agents/scripts/install-claude-agents.sh
set -euo pipefail
root="$(git rev-parse --show-toplevel)"
cd "$root"
mkdir -p .claude
for target in agents skills; do
link=".claude/$target"
if [ -e "$link" ] && [ ! -L "$link" ]; then
echo "refusing: $link exists and is not a symlink — move it aside first" >&2
exit 1
fi
ln -sfn "../.agents/$target" "$link"
echo "linked $link -> .agents/$target"
done
echo
echo "Claude Code will now load:"
ls -1 .agents/agents/*.md | sed 's|.*/| agent: |;s|\.md$||'
ls -1d .agents/skills/*/ | sed 's|.*/skills/| skill: |;s|/$||'
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env bash
# Launch one refactor task in its own worktree, on the CLI its brief routes it to.
#
# .agents/scripts/launch.sh 01 scaffold
# .agents/scripts/launch.sh 02 tokens --base refactor/task-01-scaffold
# .agents/scripts/launch.sh 07 primitives --cli mm --fg
#
# Routing comes from plans/astro-refactor/MODEL-ROUTING.md. Override with --cli.
# All four CLIs are launched with their permission prompts disabled: these run
# unattended inside a worktree, and a blocked edit or bash call just hangs.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
number=${1:?usage: launch.sh <task-number> <slug> [--base ref] [--cli codex|agy|mm|oc] [--fg]}
slug=${2:?slug, e.g. scaffold}
shift 2
base="main"
cli=""
foreground=0
while [ $# -gt 0 ]; do
case "$1" in
--base) base=$2; shift 2 ;;
--cli) cli=$2; shift 2 ;;
--fg) foreground=1; shift ;;
*) echo "unknown flag: $1" >&2; exit 2 ;;
esac
done
# Model routing. Codex takes the long iterate-until-green loops, Gemini the two
# tasks that need whole-corpus context plus visual judgement, MiniMax the rest.
if [ -z "$cli" ]; then
case "$number" in
01|03|15|16|19) cli=codex ;;
02|18) cli=agy ;;
*) cli=mm ;;
esac
fi
dir="../af-task-${number}"
branch="refactor/task-${number}-${slug}"
plan="plans/astro-refactor/task-${number}-${slug}.md"
log="$(pwd)/.agents/logs/task-${number}-${slug}.log"
[ -f "$plan" ] || { echo "no plan at $plan — check the number and slug" >&2; exit 1; }
mkdir -p .agents/logs
# The brief names its own agent; pull it out so the prompt can point at the file.
# [a-z0-9-] not [a-z-]: content-i18n-migrator has digits in it.
agent=$(sed -n 's/.*\*\*Agent\*\*: `\([a-z0-9-]*\)`.*/\1/p' "$plan" | head -1)
[ -n "$agent" ] || { echo "could not read the agent name out of $plan" >&2; exit 1; }
[ -f ".agents/agents/${agent}.md" ] || { echo "no such agent: .agents/agents/${agent}.md" >&2; exit 1; }
if [ -d "$dir" ]; then
echo "worktree $dir already exists — reusing it"
else
git worktree add "$dir" -b "$branch" "$base"
fi
# pnpm install works only once task 01 has produced a lockfile. The pre-existing root
# package.json carries two scripts and no dependencies, so before task 01 there
# is no toolchain to install and no hooks to verify.
if [ -f "$dir/pnpm-lock.yaml" ]; then
( cd "$dir" && pnpm install --frozen-lockfile && .agents/scripts/verify-hooks.sh )
fi
prompt=$(cat <<PROMPT
You are the \`${agent}\` specialist on the ai-for-dummies Astro refactor.
You are working alone in the git worktree at $(cd "$dir" && pwd), on branch ${branch}.
Read these, in this order, before you write anything:
1. AGENTS.md — project entry point and the "never touch" list
2. .agents/agents/${agent}.md — your role, what you own, what you must not do
3. ${plan} — your task brief: scope, steps, done-when, do-not
4. every file the brief and your agent definition tell you to read
(.agents/context/*, .agents/rules/*, .agents/skills/*, .agents/checklists/*)
Then do the task. Rules that override your own judgement:
- Stay inside the scope in the brief. Do not do work belonging to another task.
- Honour every "Do not" line in the brief and in your agent definition.
- The site must look and read exactly as it does today. This is a refactor,
not a redesign. If you find something that looks like a bug in the current
design, write it down in the final report — do not fix it.
- Never delete or weaken an assertion in scripts/verify.mjs to make a suite
green. Only the verification-engineer may reduce assertion count, with a
written reason per removal.
- Commit as you go with conventional-commit messages. The commit-msg and
pre-commit hooks are live; if a hook rejects you, fix the cause, never
bypass with --no-verify.
- Work through the done-when checklist at the end and actually run each check.
Finish by printing: what you changed, which done-when boxes are genuinely
ticked, which are not and why, and anything the next task needs to know.
PROMPT
)
echo "task : ${number} ${slug}"
echo "agent : ${agent}"
echo "cli : ${cli}"
echo "worktree: ${dir} (branch ${branch}, from ${base})"
echo "log : ${log}"
echo
run() {
case "$cli" in
codex)
codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check \
-C "$dir" "$prompt"
;;
agy)
( cd "$dir" && agy --dangerously-skip-permissions \
--model gemini-3.1-pro-high --print-timeout 4h -p "$prompt" )
;;
mm)
( cd "$dir" && mm --dangerously-skip-permissions \
--model opus -p "$prompt" )
;;
oc)
# Claude Code against a local-Ollama-backed model, through the headroom
# hub. Unproven on this repo — give it the task whose failure is cheapest.
( cd "$dir" && OLLAMA_CLAUDE_MODEL="${OC_MODEL:-glm-5.3:cloud}" \
ollama-claude --dangerously-skip-permissions -p "$prompt" )
;;
*) echo "unknown cli: $cli" >&2; exit 2 ;;
esac
}
if [ "$foreground" = 1 ]; then
run 2>&1 | tee "$log"
else
export cli dir prompt
nohup bash -c "$(declare -f run); run" >"$log" 2>&1 &
echo "launched in background, pid $!"
echo "follow: tail -f $log"
fi
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env node
// Compare the *rendered* text of a legacy page against its Astro replacement.
//
// node .agents/scripts/rendered-text-diff.mjs full-guide
//
// Why this exists: scripts/verify.mjs reads the legacy files, so a migrated
// page can drop half its content and still pass the gate. Task 15d shipped
// /full-guide/ missing 86 rendered spans -- the entire verification section,
// the hands-on exercise brief, and both "Clone from Gitea" links -- and every
// check was green.
//
// Static HTML comparison is useless here: the guide's tab panels are injected
// by an island at runtime, so half the legacy page's markup has no static
// counterpart. This walks the live DOM instead and skips anything the browser
// is not painting -- which also drops the hidden Portuguese half of each
// bilingual pair, so the two sides line up.
//
// Requires playwright (devDependency) and two static servers; it starts both.
import { spawn } from 'node:child_process';
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { chromium } from 'playwright';
const route = process.argv[2];
if (!route) {
console.error('usage: rendered-text-diff.mjs <route> e.g. full-guide');
process.exit(2);
}
// The built site expects to be served under the configured base path.
const staging = mkdtempSync(join(tmpdir(), 'af-rtd-'));
cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true });
const serve = (dir, port) =>
spawn('python3', ['-m', 'http.server', String(port), '-d', dir], { stdio: 'ignore' });
const servers = [serve('.', 4197), serve(staging, 4196)];
const stop = () => {
servers.forEach((s) => s.kill());
rmSync(staging, { recursive: true, force: true });
};
// Visible text nodes, in document order, whitespace collapsed.
const visibleText = () => {
const out = [];
const walk = (node) => {
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent.replace(/\s+/g, ' ').trim();
if (text) out.push(text);
continue;
}
if (child.nodeType !== Node.ELEMENT_NODE) continue;
if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE') continue;
const style = getComputedStyle(child);
if (child.hidden || style.display === 'none' || style.visibility === 'hidden') continue;
walk(child);
}
};
walk(document.body);
return out;
};
try {
await new Promise((r) => setTimeout(r, 1500));
const browser = await chromium.launch();
const grab = async (url) => {
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
await page.goto(url, { waitUntil: 'load' });
// The islands hydrate and render their initial panel on load; without this
// every panel's copy reads as missing.
await page.waitForTimeout(1200);
const spans = await page.evaluate(visibleText);
await page.close();
return spans;
};
const legacy = await grab(`http://localhost:4197/${route}/index.html`);
const astro = await grab(`http://localhost:4196/ai-for-dummies/${route}/`);
await browser.close();
const rendered = new Set(astro);
const missing = legacy.filter((span) => !rendered.has(span));
console.log(
`legacy ${legacy.length} spans · astro ${astro.length} spans · missing ${missing.length}`,
);
for (const span of missing) console.log(` - ${span}`);
process.exitCode = missing.length === 0 ? 0 : 1;
} finally {
stop();
}
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env node
// Rendered-text snapshot of one route. This is the migration's regression net:
// token matching in verify.mjs cannot catch a dropped paragraph, this can.
//
// Usage:
// node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/
// node .agents/scripts/snapshot-route.mjs dist/models/index.html
//
// Take a snapshot from the vanilla site BEFORE migrating, then diff the built
// output against it. An empty diff is the proof that nothing was lost.
import { readFileSync } from 'node:fs';
const target = process.argv[2];
if (!target) {
console.error('usage: snapshot-route.mjs <url|path>');
process.exit(2);
}
const html = target.startsWith('http')
? await (await fetch(target)).text()
: readFileSync(target, 'utf8');
const text = html
// Drop anything that is not user-visible prose.
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<[^>]+>/g, '\n')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#0?39;/g, "'")
.replace(/&nbsp;/g, ' ')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.join('\n');
console.log(text);
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Guards the silent-failure mode described in .agents/rules/gates.md.
#
# Husky points core.hooksPath at `.husky/_`, but that directory is GENERATED by
# `pnpm install` and is NOT committed. A fresh `git worktree add` therefore has
# hooks configured and the directory missing — so every hook silently does
# nothing and every commit passes unchecked.
#
# Run this in any worktree you did not create with worktree.sh.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
fail=0
path=$(git config --get core.hooksPath || true)
if [ -z "$path" ]; then
echo "✗ core.hooksPath is unset — husky was never installed here"
fail=1
else
echo "✓ core.hooksPath = $path"
fi
if [ ! -d "${path:-.husky/_}" ]; then
echo "${path:-.husky/_}/ does not exist — HOOKS ARE NOT RUNNING"
fail=1
else
echo "${path} exists"
fi
for hook in pre-commit commit-msg pre-push; do
if [ -f ".husky/$hook" ]; then
echo "✓ .husky/$hook present"
else
echo "✗ .husky/$hook missing"
fail=1
fi
done
if [ ! -d node_modules ]; then
echo "✗ node_modules missing — lint-staged and astro check cannot run"
fail=1
fi
if [ "$fail" -ne 0 ]; then
echo
echo "Fix: pnpm install --frozen-lockfile (its prepare script regenerates .husky/_)"
exit 1
fi
echo
echo "hooks are live in this worktree"
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env node
// Keep vanilla screenshots before a page changes; compare them against the
// migrated preview when a visual diff needs investigation.
import { mkdirSync } from 'node:fs';
const routes = [
'/',
'/full-guide/',
'/summary/',
'/models/',
'/agents/',
'/skills/',
'/rules/',
'/skills-review/',
'/hands-on/starter/',
'/hands-on/rules/',
];
const widths = [560, 800, 1100, 1600];
const hasReducedMotion = process.argv.includes('--reduced-motion');
const base = process.env.VISUAL_BASE_URL ?? 'http://localhost:4173';
const output =
process.env.VISUAL_OUTPUT_DIR ??
`.agents/snapshots/${hasReducedMotion ? 'before-reduced-motion' : 'before'}`;
mkdirSync(output, { recursive: true });
let chromium;
try {
({ chromium } = await import('playwright'));
} catch {
throw new Error(
'visual regression requires Playwright; install its project dependency and run pnpm exec playwright install chromium',
);
}
const browser = await chromium.launch({
headless: true,
...(process.env.VISUAL_BROWSER_PATH ? { executablePath: process.env.VISUAL_BROWSER_PATH } : {}),
});
try {
for (const route of routes) {
for (const width of widths) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
if (hasReducedMotion) await page.emulateMedia({ reducedMotion: 'reduce' });
// The legacy site intentionally calls optional services that may be down;
// visual capture is about the rendered page, not their network lifetime.
await page.goto(new URL(route, base).toString(), { waitUntil: 'domcontentloaded' });
const name = route.replace(/^\/+|\/+$/g, '').replaceAll('/', '_') || 'index';
await page.screenshot({ path: `${output}/${name}-${width}.png`, fullPage: true });
await page.close();
}
}
} finally {
await browser.close();
}
console.log(`captured ${routes.length * widths.length} screenshots in ${output}`);
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/env bash
# Spin up (or tear down) an isolated worktree for one refactor task.
#
# .agents/scripts/worktree.sh start 07 route-cards
# .agents/scripts/worktree.sh finish 07 route-cards
#
# One task, one worktree, one agent. See .agents/rules/git-worktrees.md
set -euo pipefail
action=${1:?usage: worktree.sh <start|finish> <task-number> <slug>}
number=${2:?task number, e.g. 07}
slug=${3:?slug, e.g. route-cards}
dir="../af-task-${number}"
branch="refactor/task-${number}-${slug}"
plan="plans/astro-refactor/task-${number}-${slug}.md"
case "$action" in
start)
[ -f "$plan" ] || echo "warning: no plan at $plan — check the task number" >&2
git fetch origin
git worktree add "$dir" -b "$branch" origin/main
# Not optional. `.husky/_` is generated by install and is NOT committed, so
# a fresh worktree has hooks configured but absent — every commit would pass
# unchecked. --prefer-offline keeps ten parallel spin-ups off the registry.
( cd "$dir" && pnpm install --frozen-lockfile && .agents/scripts/verify-hooks.sh )
echo
echo "worktree : $dir"
echo "branch : $branch"
echo "brief : $plan"
echo
echo "next: cd $dir && read the brief end to end before writing anything"
;;
finish)
# Never remove a worktree with uncommitted work in it.
if [ -n "$(git -C "$dir" status --porcelain)" ]; then
echo "refusing: $dir has uncommitted changes" >&2
git -C "$dir" status --short >&2
exit 1
fi
git worktree remove "$dir"
echo "removed $dir — branch $branch kept; delete it after the merge lands"
;;
*)
echo "unknown action: $action (expected start|finish)" >&2
exit 2
;;
esac
-81
View File
@@ -1,81 +0,0 @@
---
name: astro-component
description:
Build one Astro component for the ai-for-dummies site from the project
templates. Use when creating a primitive, block, or island, or when splitting
existing markup into a component.
---
# Building an Astro component
## Decide it should exist
Three occurrences, or a name a person says out loud. Two is not enough — see
[`../../rules/componentization.md`](../../rules/componentization.md).
Then place it:
- `primitives/` — no domain knowledge (Eyebrow, Rule, Callout, CodeBlock)
- `blocks/` — composed, page-agnostic, takes props (RouteCard, PhasePanel)
- `islands/` — interactive, justified, a leaf
## Start from a template
```bash
cp .agents/templates/components/static-block.astro src/components/blocks/RouteCard.astro
# interactive? use island.astro instead
```
Templates exist so ten parallel agents produce one house style. Do not start
from a blank file.
## Write it
```astro
---
interface Props {
number: string;
title: string;
summary: string;
href: string;
}
const { number, title, summary, href } = Astro.props;
---
<article class="card">
<b>{number}</b>
<h2>{title}</h2>
<p>{summary}</p>
<a href={href}>Open chapter →</a>
</article>
<style>
.card {
display: grid;
gap: 10px;
padding: 22px;
background: var(--paper);
}
b {
color: var(--accent);
font: var(--font-eyebrow);
}
</style>
```
Rules that bite most often here:
- **No raw hex.** Tokens only. `check-tokens.mjs` will fail you.
- **No `client:*`** unless it is genuinely interactive, and say why in the PR.
- Keep the `gap:1px` over a coloured parent trick where the original used it —
it is the house style, not a bug.
- Preserve every ARIA attribute from the markup you are replacing.
## Prove it
1. Render it at 560 / 800 / 1100 / 1600 px.
2. Tab to it. Focus ring visible (`outline: 3px solid #a7483f`).
3. Diff its rendered text against the markup it replaced.
4. Run
[`../../checklists/before-component.md`](../../checklists/before-component.md).
5. `pnpm run verify` — green, with no assertion deleted.
-69
View File
@@ -1,69 +0,0 @@
---
name: astro-page
description:
Migrate one hand-written HTML page of the ai-for-dummies site to an Astro
route without changing its URL, content, or JS budget. Use for any page-level
migration task.
---
# Migrating a page to Astro
## Snapshot first, migrate second
The snapshot is the only objective evidence that no content was lost.
```bash
pnpm run serve & # vanilla site on :4173
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ \
> .agents/snapshots/models.txt
```
Screenshot the same route at 560 / 800 / 1100 / 1600 px as well.
## Migrate
```bash
cp .agents/templates/pages/chapter.astro src/pages/models.astro
```
Then, in order:
1. Move markup into the layout + components. Reuse existing components before
creating new ones.
2. Move copy into `src/content/`. Both `en` and `pt`, copied literally — never
retyped.
3. Move page CSS into component `<style>` blocks. Do **not** port
`responsive.css` wholesale; take what this page needs and prove the rest
dead.
4. Keep every `data-*` hook. `verify.mjs` asserts many of them by name
(`data-phase`, `data-tree`, `data-route`, `data-model-provider`, …).
5. Keep every ARIA attribute and the `<title>` / `<meta name="description">`.
## The URL must not change
Served from `/ai-for-dummies/`, so `base` is set in `astro.config.mjs`. Never
hand-write an internal absolute path; use `import.meta.env.BASE_URL`.
Trailing slashes matter. `/models/` must not become `/models`.
If the page honours query params (the review desk uses `?author=`, `?skill=`,
`?view=`, `?file=`, `?compare=`, `?render=`), they must still work — they are
documented in the page footer and shared externally.
## Prove it
```bash
pnpm run build
node .agents/scripts/snapshot-route.mjs dist/models/index.html > /tmp/after.txt
diff .agents/snapshots/models.txt /tmp/after.txt # empty, or justify every line
node scripts/audit-ui.mjs
pnpm run verify
```
Then screenshots at the same four widths, and
[`../../checklists/before-page.md`](../../checklists/before-page.md).
## JS budget
A page that shipped zero JS must still ship zero. Check the build output. If
your migration added a `client:load` to a static page, you did it wrong.
-76
View File
@@ -1,76 +0,0 @@
---
name: content-migration
description:
Move bilingual copy out of app.js and catalog.js into typed Astro content
collections without losing or altering a single string. Use for any task that
relocates user-visible text.
---
# Content migration
## What you are moving
- `app.js` — ~50 `{ en, pt }` keys across `phases`, `handsOnPrompts`,
`modelGuide`, `skillSources`, `skillInstallPrompts`
- `skills-review/catalog.js` + `submitted-catalog.js` — 24 entries with `id`,
`author`, `title`, `status`, `focus`, `wins[]`, `improve[]`, `extras`,
`improved` (full markdown)
These are hand-written translations with deliberate tone. **Copy them. Never
retype them.** Retyping introduces drift you will not notice.
## Schema
```ts
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const localized = z.object({ en: z.string(), pt: z.string() });
const guide = defineCollection({
type: 'data',
schema: z.object({
id: z.string(),
model: localized,
title: localized,
copy: localized,
code: localized,
}),
});
```
Both locales **required**. A missing `pt` must be a build error — silent English
fallback is how a bilingual site quietly becomes monolingual.
## Procedure
1. Extract the literals mechanically (script, not by hand).
2. Write them into the collection.
3. Diff extracted-before against extracted-after. Must be empty.
```bash
node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
node .agents/scripts/extract-strings.mjs src/content/ > /tmp/after.json
diff /tmp/before.json /tmp/after.json
```
4. Only then delete the source literals.
## The generator trap
`skill-reviews/improved/**/SKILL.md` is **generated** from `catalog.js`
`improved` fields by `scripts/build-skill-review.mjs`, and the output is
committed to the repo. If you move `catalog.js`, that generator breaks silently
— it will still run, just against nothing.
Either keep the generator pointed at the new collection, or replace it and
update `package.json`, `README.md`, `docs/operations-guide.md`, and the review
desk footer, all of which reference it.
## Markdown
The `improved` fields are full Markdown rendered client-side today. Move them to
real `.md` files in the collection and let Astro render at build time. That
deletes the hand-rolled renderer and improves fidelity — but re-check the review
desk's diff view, which compares original and improved source text and needs the
raw string, not just rendered HTML.
-74
View File
@@ -1,74 +0,0 @@
---
name: design-tokens
description:
Extract, name, and enforce the design token layer for the ai-for-dummies site.
Use when touching any colour, font size, spacing value, or breakpoint, when
consolidating the three drifted palettes, or when a check-tokens failure needs
resolving.
---
# Design tokens
## Before anything
Read [`../../context/design-system.md`](../../context/design-system.md). This
site has **three drifting palettes** and a **broken `@font-face`**. Both are
traps. If you have not read that file, you will "preserve the styles" by copying
a bug.
## Extracting
```bash
python3 - <<'PY'
import re
files=['styles.css','chapters.css','landing.css','rules/styles.css','skills/styles.css',
'skills-review/styles.css','hands-on/starter/styles.css','hands-on/rules/styles.css']
seen={}
for f in files:
for m in re.finditer(r'--([a-z-]+):\s*([^;}]+)', open(f).read()):
seen.setdefault(m.group(1),{}).setdefault(m.group(2).strip(),[]).append(f)
for k,v in sorted(seen.items()):
print(f"--{k}:")
for val,fs in v.items(): print(f" {val:12} <- {', '.join(fs)}")
PY
```
Re-run this after any consolidation. Every token should print exactly one value.
## Consolidating a drifted token
1. List every value and where it is used (above).
2. Compute the perceptual delta. Sub-perceptual (a few units per channel) →
canonicalize freely. Visible (`--blue`: `#527f9f` vs `#215675`) → screenshot
both, get a human decision, record it in the task file.
3. Pick the canonical value. Prefer the one used on the most-visited surface.
4. Replace, then screenshot every affected page at 560/800/1100/1600 px.
5. Attach before/after images to the task report. "It looked fine to me" is not
evidence.
## Naming
Semantic, matching the existing vocabulary — `--ink`, `--paper`, `--muted`,
`--line`, `--accent`, `--gold`, `--blue`, `--deep`. Never numeric scales. New
surfaces extend semantically: `--surface-lab`, `--ink-inverse`.
## Type scale and breakpoints
Collapse the 14 ad-hoc `clamp()` triples to named steps and the 16 breakpoints
to five, per [`../../rules/theming.md`](../../rules/theming.md). When collapsing
a breakpoint, **screenshot at the old value** — that is where the regression is.
## Enforcing
```bash
node .agents/scripts/check-tokens.mjs # fails on raw hex outside tokens.css
```
Wire it into `pnpm run verify`. A rule nobody checks is a suggestion.
## The font decision
Do not self-host Manrope/DM Mono as part of a refactor task. The intended fonts
have never rendered, so adopting them is a visual redesign. Default to matching
what renders today (Arial / generic monospace) and delete the dead `@font-face`.
If a human wants the real fonts, that is its own task with its own screenshots.
-79
View File
@@ -1,79 +0,0 @@
---
name: motion
description:
Add or review animation on the ai-for-dummies site — transitions, state
changes, view transitions. Use when any element moves, fades, or transforms,
or when auditing existing motion for performance and reduced-motion support.
---
# Motion
This is an editorial-print design. Motion is punctuation. Read
[`../../rules/animation.md`](../../rules/animation.md) — it is binding; this is
the procedure.
## Decide it should move
Answer out loud: **what does this motion tell the user that the static state
does not?** Valid answers: something changed, attention needs directing to what
changed, a layout shift needs smoothing. "It feels more polished" is not an
answer — on this design it reads as generic.
If there is no answer, ship it static. That is a legitimate, common outcome.
## Build it
```css
.panel {
transition:
opacity 180ms cubic-bezier(0.2, 0, 0, 1),
transform 180ms cubic-bezier(0.2, 0, 0, 1);
}
.panel[data-state='entering'] {
opacity: 0;
transform: translateY(6px);
}
```
- **`transform` and `opacity` only.** Animating `width`/`height`/`top`/`left`
forces layout every frame and shows up as a failed INP (budget: 200ms).
- 150250ms for UI feedback; up to 400ms for a page transition.
- One thing moves at a time. No staggered card cascades.
- `will-change` only immediately before animating, removed after.
- No animation library. This site's thesis is having no runtime dependencies.
## Reduced motion is not optional
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
Then **test it**: DevTools → Rendering → Emulate
`prefers-reduced-motion: reduce`. The end state must still be correct and the UI
still usable. Reduced, not broken.
## Page transitions
Astro's `<ClientRouter />` is the only sanctioned motion dependency. Before
enabling it, verify:
- JS disabled → full navigation still works
- browser back/forward still works
- the review desk's query-param deep links survive
- `prefers-reduced-motion` is honoured
## Audit an existing animation
```bash
grep -rn "transition\|animation\|@keyframes\|transform" src/ --include="*.astro" --include="*.css"
```
For each hit ask: does it animate a layout property? does it respect reduced
motion? is there a stated purpose? Three questions, three possible fixes.
-67
View File
@@ -1,67 +0,0 @@
---
name: verify-contract
description:
Evolve scripts/verify.mjs across the Astro migration without losing coverage.
Use whenever a verify assertion fails because of a refactor, or when adding
checks for new architecture.
---
# The verification contract
Read [`../../context/verification.md`](../../context/verification.md) first.
`verify.mjs` has 42 assertions pinning real content and interactions. They are
the only thing preventing silent content loss during this migration, and they
will all break, because they assert against files that stop existing.
## The rule
**A failing assertion is a question, not a bug to delete.**
```
assertion fails
does the user-visible fact it pins still exist?
├── yes → re-point the assertion at the new location
└── no → you deleted content. Put it back, or get sign-off.
```
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease. If it must,
the `verification-engineer` writes a one-line reason per removal in the task
report. Nobody else may reduce coverage.
## Translating assertions
| Kind | Old | New |
| --------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
| Content presence | `html.includes('data-phase="plan"')` | same token, read from `dist/full-guide/index.html` |
| Implementation detail | `js.includes('renderTree')` | assert the rendered output has the tree UI, not that a function is named that |
| Asset version | `'app.js?v=20260904-vote-widget'` | assert the built HTML references a hashed asset |
Implementation-detail assertions are the dangerous ones: they _look_ deletable.
They are pinning a feature. Replace with an output-level assertion of the same
feature; never drop.
## Add the stronger check
Token matching cannot catch a dropped paragraph. Add rendered-text snapshots:
```bash
# before migrating
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ > .agents/snapshots/models.txt
# after
node .agents/scripts/snapshot-route.mjs dist/models/index.html | diff .agents/snapshots/models.txt -
```
Commit the snapshots. They are the migration's regression net.
## Extend audit-ui.mjs
It rejects external `<script>`/`<link>` but **misses external URLs inside CSS**
— which is exactly how the broken Google Fonts `@font-face` in `styles.css:1`
got into a "dependency-free" site. Add:
```js
if (/@import|src:\s*url\(['"]?https?:|url\(['"]?https?:/i.test(css))
throw new Error(`${file} has an external CSS dependency`);
```
-73
View File
@@ -1,73 +0,0 @@
---
name: visual-regression
description:
Prove a refactor did not change how the site looks. Use before and after any
page migration, token consolidation, or breakpoint change on ai-for-dummies.
---
# Visual regression
"Maintain the same styles" is a testable claim. Test it.
## Capture
The repo already has a Playwright pattern (`scripts/inspect.py`). Extend it
rather than inventing one.
```python
from playwright.sync_api import sync_playwright
ROUTES = ['/', '/full-guide/', '/summary/', '/models/', '/agents/',
'/skills/', '/rules/', '/skills-review/',
'/hands-on/starter/', '/hands-on/rules/']
WIDTHS = [560, 800, 1100, 1600]
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
for route in ROUTES:
for w in WIDTHS:
page = browser.new_page(viewport={'width': w, 'height': 900})
page.goto(f'{BASE}{route}', wait_until='networkidle')
page.screenshot(path=f'{OUT}/{route.strip("/").replace("/","_") or "index"}-{w}.png',
full_page=True)
page.close()
browser.close()
```
Run once against the vanilla site (`pnpm run serve`), once against
`pnpm run preview`. Keep both sets.
## Compare
```bash
for f in before/*.png; do
compare -metric AE "$f" "after/$(basename $f)" null: 2>&1 # ImageMagick
echo " <- $(basename $f)"
done
```
Pixel-exact is not the bar — antialiasing differs. Judge by eye where the metric
is non-trivial, and attach the pair to the task report.
## The three widths that catch the most
- **560px** — where the 16 ad-hoc breakpoints collapse to `--bp-sm`. Highest
risk in the whole migration.
- **800px** — the most common existing breakpoint; layout flips here.
- **1600px** — `min-width` rules that only fire on large screens are the ones
nobody notices are broken.
Also screenshot at the **old** breakpoint values you removed (520, 530, 600,
620, 720, 850, 880, 900), not just the new ones. Regressions hide exactly there.
## What a real difference looks like
Expect and accept: sub-pixel text shifts, antialiasing.
Investigate: anything that moves by more than ~2px, any colour change (that is a
token bug), any element that appears or disappears (that is content loss — stop
and check the snapshot diff).
## Reduced motion
Capture one pass with `prefers_reduced_motion='reduce'`. Animations must land in
their correct end state, not vanish.
-40
View File
@@ -1,40 +0,0 @@
AI For Dummies — Agents and trees
← ROUTE MAP
02 / AGENTS & TREES
field guide ↗
Subagent workflow
One branch
per
hand.
Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent.
The tree
Split at
the
seam.
MAIN / ORCHESTRATOR
├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence
FRAME
Orchestrator
Owns scope, task graph, boundaries, and integration.
HAND OFF
Worker
Owns one coherent slice and one worktree.
PROVE
Verifier
Re-runs gates and reports remaining gaps.
Handoff
Context that
can
travel.
01
Brief
Goal, owned files, dependencies, non-goals, acceptance.
02
Isolation
One branch and worktree per independent change.
03
Evidence
Commands, result, changed files, screenshots, gaps.
Previous: models →
Rules case study →
Try the rules lab →
Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Some files were not shown because too many files have changed in this diff Show More