docs: add .agents workspace and the Astro refactor plan

Adds the agent-facing workspace and a 20-task plan for migrating the site
to Astro. Nothing here implements the refactor; these are briefs, rules and
templates that the task agents read.

- .agents/ holds context, rules, checklists, skills, specialist agents,
  component/page/config templates and gate scripts. It is vendor-neutral so
  MiniMax, Gemini and Codex can all read it; CLAUDE.md just points at
  AGENTS.md.
- .husky/ plus .lintstagedrc.json wire the three gate tiers. gate.sh locks on
  the shared git-common-dir so parallel worktrees serialise, and guards the
  assertion count in scripts/verify.mjs against a coverage drop.
- plans/astro-refactor/ carries the phase graph, per-task briefs and the
  model-routing recommendation.

These files must be tracked before fanning out: a worktree only checks out
tracked files, so an untracked plan is invisible to every agent working in one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Marcos Paulo
2026-09-05 01:18:27 +00:00
parent aa85c1d0b7
commit aae4d42229
79 changed files with 3805 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# 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 `npm 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
npm run verify ← hard gate
reviewer agent on the diff ← merge gate
```
+44
View File
@@ -0,0 +1,44 @@
---
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
`npm 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
`npm run build` succeeds, one migrated page serves correctly from the real host
under `/ai-for-dummies/`, `npm run verify` and `node scripts/audit-ui.mjs` are
green, and the operations guide matches reality.
+37
View File
@@ -0,0 +1,37 @@
---
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.
- 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 `npm run verify` is green **with no assertion deleted**.
+53
View File
@@ -0,0 +1,53 @@
---
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 `npm run verify` is green.
+46
View File
@@ -0,0 +1,46 @@
---
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.** The `@font-face` in `styles.css:1` points
`src:` at a Google Fonts *stylesheet*, so Manrope and DM Mono have always
fallen back to Arial and generic monospace. Self-hosting them is a redesign,
not a refactor. Default: delete the dead rule, declare the stacks that
actually render. Escalate if someone wants the real fonts.
## 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 `npm 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.
+48
View File
@@ -0,0 +1,48 @@
---
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.
+42
View File
@@ -0,0 +1,42 @@
---
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 `npm run verify` green, screenshots
compared, and your task report lists what you deliberately left alone.
+50
View File
@@ -0,0 +1,50 @@
---
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.
+50
View File
@@ -0,0 +1,50 @@
---
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
`npm run verify`, and the suite runs green on the migrated site.
+15
View File
@@ -0,0 +1,15 @@
# 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
- [ ] `npm run verify` green with no assertion deleted
+12
View File
@@ -0,0 +1,12 @@
# Checklist: before you merge a task branch
- [ ] Rebased on current `origin/main`, conflicts resolved in the worktree
- [ ] `npm 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
+14
View File
@@ -0,0 +1,14 @@
# 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
- [ ] `npm run verify` green with no assertion deleted
+75
View File
@@ -0,0 +1,75 @@
# 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.
+107
View File
@@ -0,0 +1,107 @@
# 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 you see is not the typography that was written
`styles.css` line 1:
```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:` points at a **CSS stylesheet**, not a font file. No browser can load a
font from that, so:
- every `font-family:Manrope,Arial,sans-serif` renders as **Arial**
- every `font:… 'DM Mono',monospace` renders as the **generic monospace** face
- there are no `@font-face` blocks anywhere else and zero font files in the repo
- `scripts/audit-ui.mjs` only rejects external `<link>`/`<script>` tags, so this
slipped through the "dependency-free" audit
**This is a trap for the refactor.** Self-hosting Manrope and DM Mono in Astro
is the obvious "fix" — and it would change how every page looks, violating
"maintain the same styles". Treat it as an explicit product decision:
- **Keep current rendering**: delete the dead `@font-face`, replace the font
stacks with what actually renders today (`Arial, sans-serif` /
`ui-monospace, monospace`). Zero visual change. Honest CSS.
- **Adopt the intended fonts**: self-host the woff2 files in `public/fonts/`,
add real `@font-face` with `font-display:swap`. Better-looking, but it is a
redesign and needs sign-off plus fresh screenshots.
Default to the first unless a human says otherwise.
## 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`.
+54
View File
@@ -0,0 +1,54 @@
# 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`** | `npm 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``npm ci && npm 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 `npm 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
@@ -0,0 +1,54 @@
# 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 `npm 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.
+52
View File
@@ -0,0 +1,52 @@
# 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.
+59
View File
@@ -0,0 +1,59 @@
# 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: .01ms !important; animation-iteration-count: 1 !important;
transition-duration: .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.
+72
View File
@@ -0,0 +1,72 @@
# 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
@@ -0,0 +1,48 @@
# 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.
+53
View File
@@ -0,0 +1,53 @@
# 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
@@ -0,0 +1,59 @@
# 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.
+76
View File
@@ -0,0 +1,76 @@
# 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 + visual regression | 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 screenshot comparison against `.agents/snapshots/`. Only CI has the
budget for it.
## Bypassing
`--no-verify` is allowed exactly once: a work-in-progress commit **on your own
task branch that you will rebase 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.
## 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.
- `npm ci` in a fresh worktree should use `--prefer-offline` to avoid registry
contention when several spin up at once.
## The silent-failure mode you must know about
Husky sets `core.hooksPath` to `.husky/_`, and **`.husky/_` is generated by
`npm 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.
+69
View File
@@ -0,0 +1,69 @@
# 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
npm ci
```
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` |
## Before you start
1. `git fetch origin && git rebase origin/main` — start from current `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. `npm run verify` green — without deleting assertions.
2. The relevant checklist in [`../checklists/`](../checklists/) complete.
3. `git rebase origin/main` again, resolve conflicts in your worktree.
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.
+78
View File
@@ -0,0 +1,78 @@
# 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,.6);
/* required */
color: var(--ink);
color: var(--muted);
```
No raw hex outside `tokens.css`. `.agents/scripts/check-tokens.mjs` enforces it;
wire it into `npm 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.
+59
View File
@@ -0,0 +1,59 @@
#!/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
// `npm run verify`.
//
// 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 = [];
for (const path of targets) {
if (!['.astro', '.css'].includes(extname(path))) continue;
if (TOKEN_FILES.some((allowed) => path.endsWith(allowed))) continue;
readFileSync(path, 'utf8').split('\n').forEach((line, index) => {
const at = `${path}:${index + 1}`;
// 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) findings.push(`${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))
findings.push(`${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) findings.push(`${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]))
findings.push(`${at}: breakpoint ${media[1]} is not a named one (${ALLOWED_BREAKPOINTS.join(', ')})`);
});
}
if (findings.length) {
console.error(`token check failed — ${findings.length} finding(s):\n`);
findings.forEach((finding) => console.error(` ${finding}`));
process.exit(1);
}
console.log('token check passed');
+45
View File
@@ -0,0 +1,45 @@
#!/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)`);
+61
View File
@@ -0,0 +1,61 @@
#!/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 'npm ci --prefer-offline' first" >&2
exit 1
fi
step "types"
npx --no-install astro check
step "build"
npm run build
step "content contracts"
npm 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
@@ -0,0 +1,29 @@
#!/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|/$||'
+37
View File
@@ -0,0 +1,37 @@
#!/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
@@ -0,0 +1,51 @@
#!/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
# `npm 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: npm ci --prefer-offline (its prepare script regenerates .husky/_)"
exit 1
fi
echo
echo "hooks are live in this worktree"
+50
View File
@@ -0,0 +1,50 @@
#!/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" && npm ci --prefer-offline && .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
+68
View File
@@ -0,0 +1,68 @@
---
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. `npm run verify` — green, with no assertion deleted.
+65
View File
@@ -0,0 +1,65 @@
---
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
npm 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
npm 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
npm 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.
+73
View File
@@ -0,0 +1,73 @@
---
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.
+70
View File
@@ -0,0 +1,70 @@
---
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 `npm 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.
+68
View File
@@ -0,0 +1,68 @@
---
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(.2,0,0,1),
transform 180ms cubic-bezier(.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: .01ms !important; animation-iteration-count: 1 !important;
transition-duration: .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.
+64
View File
@@ -0,0 +1,64 @@
---
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`);
```
+71
View File
@@ -0,0 +1,71 @@
---
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 (`npm run serve`), once against
`npm 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.
@@ -0,0 +1,39 @@
---
// Grid-group template — for a set of sibling cards separated by hairlines.
//
// Note the separator technique: `gap: 1px` over a coloured parent background.
// That is DELIBERATE house style throughout this site, not a workaround.
// Do not "fix" it into `border`.
interface Props {
label: string;
/** Number of columns at the widest breakpoint. */
columns?: number;
}
const { label, columns = 3 } = Astro.props;
---
<section class="group" aria-label={label}>
<div class="grid" style={`--columns: ${columns}`}>
<slot />
</div>
</section>
<style>
.grid {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: 1px; /* hairline separators, drawn by the parent background */
background: var(--line);
}
/* Children paint their own background, which is what makes the 1px show. */
.grid > :global(*) {
background: var(--paper);
}
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
}
</style>
+97
View File
@@ -0,0 +1,97 @@
---
// Interactive island template. Use ONLY when the component genuinely needs
// client-side behaviour, and write the justification in your PR description.
//
// Islands are LEAVES. Do not wrap static children that could have been
// server-rendered — hydrate the tab panel, not the page.
//
// Hydration preference, in order:
// (none) → client:visible → client:idle → client:load
//
// Usage: <Island client:visible items={items} />
interface Props {
items: { id: string; label: string; body: string }[];
initialId?: string;
}
const { items, initialId = items[0]?.id } = Astro.props;
---
<div class="island" data-initial={initialId}>
<div class="tabs" role="tablist" aria-label="Sections">
{items.map((item) => (
<button
role="tab"
id={`tab-${item.id}`}
aria-controls={`panel-${item.id}`}
aria-selected={item.id === initialId}
data-tab={item.id}
>{item.label}</button>
))}
</div>
{items.map((item) => (
<div
role="tabpanel"
id={`panel-${item.id}`}
aria-labelledby={`tab-${item.id}`}
data-panel={item.id}
hidden={item.id !== initialId}
>{item.body}</div>
))}
</div>
<script>
// Scoped to this island's own root so multiple instances never collide.
document.querySelectorAll<HTMLElement>('.island').forEach((root) => {
const tabs = root.querySelectorAll<HTMLButtonElement>('[data-tab]');
const select = (id: string) => {
tabs.forEach((tab) => tab.setAttribute('aria-selected', String(tab.dataset.tab === id)));
root.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {
panel.hidden = panel.dataset.panel !== id;
});
};
tabs.forEach((tab) => tab.addEventListener('click', () => select(tab.dataset.tab!)));
// Arrow-key navigation is required for role="tablist" — see
// .agents/rules/accessibility.md
root.querySelector('[role="tablist"]')?.addEventListener('keydown', (event) => {
const key = (event as KeyboardEvent).key;
if (key !== 'ArrowRight' && key !== 'ArrowLeft') return;
const list = [...tabs];
const current = list.findIndex((tab) => tab.getAttribute('aria-selected') === 'true');
const next = list[(current + (key === 'ArrowRight' ? 1 : -1) + list.length) % list.length];
select(next.dataset.tab!);
next.focus();
});
});
</script>
<style>
.tabs { display: grid; gap: 8px; }
button {
padding: 12px;
color: var(--ink);
background: transparent;
border: 1px solid var(--line);
text-align: left;
cursor: pointer;
/* transform/opacity only — never animate layout properties */
transition: background 180ms cubic-bezier(.2, 0, 0, 1);
}
button[aria-selected='true'] {
color: var(--paper);
background: var(--ink);
}
button:focus-visible {
outline: 3px solid var(--red);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
button { transition-duration: .01ms; }
}
</style>
@@ -0,0 +1,74 @@
---
// Static block template — the default. Ships zero JavaScript.
// Copy to src/components/blocks/<Name>.astro and replace everything marked TODO.
//
// Before using this, confirm the block earns extraction: it appears three times,
// or it has a name a person says out loud. See .agents/rules/componentization.md
interface Props {
/** TODO: describe each prop. Required by default; optional needs a reason. */
eyebrow: string;
title: string;
body: string;
href?: string;
}
const { eyebrow, title, body, href } = Astro.props;
---
<article class="block">
<span class="eyebrow">{eyebrow}</span>
<h2>{title}</h2>
<p>{body}</p>
{href && <a href={href}>Open </a>}
<slot />
</article>
<style>
/* Tokens only. No raw hex, no px font sizes, no ad-hoc breakpoints.
.agents/scripts/check-tokens.mjs enforces this. */
.block {
display: grid;
gap: 10px;
padding: 22px;
background: var(--paper);
color: var(--ink);
}
/* The house eyebrow: uppercase monospace, wide tracking. One class, not
fifteen repetitions. */
.eyebrow {
color: var(--accent);
font: var(--font-eyebrow);
letter-spacing: .1em;
text-transform: uppercase;
}
h2 {
margin: 0;
font-size: var(--step-5);
line-height: 1.05;
letter-spacing: -.06em; /* tight display tracking is a signature of this design */
}
p {
margin: 0;
color: var(--muted);
line-height: 1.65;
}
a {
color: var(--blue);
font-weight: 700;
text-decoration: none;
}
a:focus-visible {
outline: 3px solid var(--red);
outline-offset: 2px;
}
@media (max-width: 800px) {
.block { padding: 18px; }
}
</style>
+7
View File
@@ -0,0 +1,7 @@
dist
node_modules
public/hands-on
submitted-skills
skill-reviews
vote-service
.agents/snapshots
+11
View File
@@ -0,0 +1,11 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"plugins": ["prettier-plugin-astro"],
"overrides": [
{ "files": "*.astro", "options": { "parser": "astro" } },
{ "files": "*.md", "options": { "proseWrap": "always", "printWidth": 80 } }
]
}
@@ -0,0 +1,22 @@
{
"extends": ["stylelint-config-standard"],
"ignoreFiles": [
"dist/**",
"public/hands-on/**",
"submitted-skills/**"
],
"rules": {
"custom-property-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
"declaration-property-value-disallowed-list": {
"/^transition/": ["/width/", "/height/", "/^top/", "/^left/", "/margin/"],
"/^animation/": ["/width/", "/height/"]
},
"media-feature-name-no-unknown": true,
"no-descending-specificity": null,
"selector-class-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
},
"_comments": {
"declaration-property-value-disallowed-list": "Animating layout properties forces reflow every frame and fails the 200ms INP budget. transform and opacity only — see .agents/rules/animation.md",
"ignoreFiles": "hands-on/ is a lab fixture and submitted-skills/ is other people's work; neither is ours to restyle"
}
}
+26
View File
@@ -0,0 +1,26 @@
// Flat config (ESLint 9+). Copy to the repository root in task 01.
import js from '@eslint/js';
import astro from 'eslint-plugin-astro';
export default [
js.configs.recommended,
...astro.configs.recommended,
{
ignores: [
'dist/**',
'public/hands-on/**', // lab fixtures ship verbatim — linting them would
// invite "fixes" that break the exercise
'submitted-skills/**', // other people's work, reproduced as submitted
'skill-reviews/**', // generated from skills-review/catalog.js
'vote-service/**', // Go service, separate lifecycle
],
},
{
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
eqeqeq: ['error', 'always'],
'no-var': 'error',
'prefer-const': 'error',
},
},
];
+39
View File
@@ -0,0 +1,39 @@
# Tier 3 gate. Copy to .gitea/workflows/verify.yml in task 01.
#
# Known trap: this Gitea's act-runner registration lives in an emptyDir, so a
# pod restart silently kills CI. If the site stops updating, check the runner
# BEFORE debugging the workflow.
name: verify
on:
push:
branches: [main]
pull_request:
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # gate.sh compares assertion counts against origin/main
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci --prefer-offline
- run: npm run lint
- run: ./.agents/scripts/gate.sh
# Tier 3 only: too slow for pre-push, essential before publishing.
- name: visual regression
run: |
npx playwright install --with-deps chromium
node .agents/scripts/visual-regression.mjs
- uses: actions/upload-artifact@v4
if: failure()
with:
name: screenshots
path: .agents/snapshots/diff/
@@ -0,0 +1,27 @@
{
"_note": "Merge these into package.json in task 01. `prepare` is what installs husky; without it every hook is inert.",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"check": "astro check",
"lint": "eslint . --max-warnings=0 && stylelint '**/*.css' --max-warnings=0",
"format": "prettier --write .",
"verify": "node scripts/verify.mjs && node scripts/audit-ui.mjs && node .agents/scripts/check-tokens.mjs",
"gate": "./.agents/scripts/gate.sh",
"snapshot": "node .agents/scripts/snapshot-route.mjs",
"prepare": "husky"
},
"devDependencies": {
"astro": "^5",
"@eslint/js": "^9",
"eslint": "^9",
"eslint-plugin-astro": "^1",
"husky": "^9",
"lint-staged": "^16",
"prettier": "^3",
"prettier-plugin-astro": "^0.14",
"stylelint": "^16",
"stylelint-config-standard": "^39"
}
}
+61
View File
@@ -0,0 +1,61 @@
---
// Chapter page template — for the numbered chapters (models, agents, skills,
// rules). These ship ZERO JavaScript today and must continue to.
//
// Copy to src/pages/<slug>.astro. The route must match the existing URL
// exactly, trailing slash included.
import ChapterLayout from '../layouts/ChapterLayout.astro';
import GridGroup from '../components/blocks/GridGroup.astro';
import StaticBlock from '../components/blocks/StaticBlock.astro';
import { getEntry } from 'astro:content';
// Content comes from a collection, never hard-coded in the page.
// Both `en` and `pt` are required by the schema.
const chapter = await getEntry('chapters', 'models');
const lang = 'en'; // TODO: wire to the language toggle decision (task 03)
---
<ChapterLayout
number={chapter.data.number}
title={chapter.data.title[lang]}
description={chapter.data.description[lang]}
>
<section class="hero">
<p class="eyebrow">{chapter.data.eyebrow[lang]}</p>
<h1 set:html={chapter.data.heading[lang]} />
<p class="lede">{chapter.data.lede[lang]}</p>
</section>
<GridGroup label="Chapter sections" columns={3}>
{chapter.data.sections.map((section) => (
<StaticBlock
eyebrow={section.eyebrow[lang]}
title={section.title[lang]}
body={section.body[lang]}
/>
))}
</GridGroup>
</ChapterLayout>
<style>
/* Page-level layout only. Anything reusable belongs in a component. */
.hero { max-width: 780px; padding: clamp(75px, 12vh, 145px) 0 85px; }
h1 {
margin: 16px 0 24px;
font-size: var(--step-display);
line-height: .86;
letter-spacing: -.08em;
}
/* Georgia is a real system font and DOES render — unlike Manrope/DM Mono.
See .agents/context/design-system.md */
h1 :global(em) {
color: var(--blue);
font-family: Georgia, serif;
font-weight: 400;
}
.lede { max-width: 570px; color: var(--muted); line-height: 1.65; }
</style>
+45
View File
@@ -0,0 +1,45 @@
---
// Interactive page template — for pages that genuinely need client-side
// behaviour (the full guide, the review desk).
//
// The page itself is still server-rendered. Only the islands hydrate.
// If you are copying this for a page that has no interaction, use
// chapter.astro instead.
import BaseLayout from '../layouts/BaseLayout.astro';
import Island from '../components/islands/Island.astro';
import { getCollection } from 'astro:content';
const entries = await getCollection('guide');
const lang = 'en'; // TODO: wire to the language toggle decision (task 03)
const items = entries.map((entry) => ({
id: entry.id,
label: entry.data.title[lang],
body: entry.data.copy[lang],
}));
---
<BaseLayout title="Full field guide" description="TODO">
<!-- Static content is server-rendered. No hydration cost. -->
<section class="hero">
<p class="eyebrow">The full guide</p>
<h1>Ship the <em>system.</em></h1>
</section>
<!--
client:visible, not client:load — this is below the fold and the page must
stay interactive-free until it matters. Justification belongs in the PR:
"tab panel requires click-driven state; no server equivalent."
-->
<Island client:visible items={items} />
<!-- More static content after the island. Islands are leaves, not wrappers. -->
<slot />
</BaseLayout>
<style>
.hero { max-width: 780px; padding: clamp(75px, 12vh, 145px) 0 85px; }
h1 { font-size: var(--step-display); line-height: .86; letter-spacing: -.08em; }
h1 em { color: var(--blue); font-family: Georgia, serif; font-weight: 400; }
</style>
+11
View File
@@ -1,3 +1,14 @@
# Tooling caches, not part of the published site. # Tooling caches, not part of the published site.
.serena/ .serena/
__pycache__/ __pycache__/
# Build + toolchain (Astro migration).
node_modules/
dist/
.astro/
.husky/_/
# Visual-regression working output. Baselines in .agents/snapshots/ ARE tracked;
# the per-run captures and diffs are not.
.agents/snapshots/after/
.agents/snapshots/diff/
+26
View File
@@ -0,0 +1,26 @@
# Conventional commits, matching this repository's existing log
# (feat:, fix:, docs:, refactor:, chore:, test:, style:, ci:).
#
# Deliberately a plain regex rather than a commitlint dependency: this project's
# thesis is having no unnecessary dependencies, and one regex is legible to
# every agent that has to satisfy it.
message_file=$1
first_line=$(head -n1 "$message_file")
# Allow merge and revert commits through untouched.
case "$first_line" in
"Merge "*|"Revert "*) exit 0 ;;
esac
if ! printf '%s' "$first_line" | grep -qE '^(feat|fix|docs|refactor|chore|test|style|ci|perf)(\([a-z0-9 -]+\))?: .{1,}$'; then
echo "commit-msg: subject must be '<type>: <subject>' (lowercase type)." >&2
echo " types: feat fix docs refactor chore test style ci perf" >&2
echo " got: $first_line" >&2
exit 1
fi
if [ "${#first_line}" -gt 72 ]; then
echo "commit-msg: subject is ${#first_line} chars; keep it under 72." >&2
exit 1
fi
+10
View File
@@ -0,0 +1,10 @@
# Tier 1: staged files only. Budget < 3s — see .agents/rules/gates.md
#
# Husky v9+: no `husky.sh` sourcing (deprecated in v9, removed in v10).
# Guard the silent-failure mode: if hooks are configured but .husky/_ is
# missing (fresh worktree that never ran `npm install`), this file would not
# execute at all. Nothing we can do from inside it — so verify-hooks.sh exists
# and worktree.sh runs it at creation time.
npx --no-install lint-staged
+4
View File
@@ -0,0 +1,4 @@
# Tier 2: the real gate. Whole project. Budget < 90s.
# Takes a cross-worktree lock so parallel agents queue instead of thrashing.
exec .agents/scripts/gate.sh
+16
View File
@@ -0,0 +1,16 @@
{
"*.{js,mjs,ts,astro}": [
"prettier --write",
"eslint --fix --max-warnings=0"
],
"*.css": [
"prettier --write",
"stylelint --fix --max-warnings=0"
],
"*.{astro,css}": [
"node .agents/scripts/check-tokens.mjs"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
+77
View File
@@ -0,0 +1,77 @@
# AGENTS.md
Entry point for any AI agent or new engineer working in this repository. Read
this file, then follow the links you need. Full map:
[`.agents/ORCHESTRATOR.md`](.agents/ORCHESTRATOR.md).
## What is this project?
**"AI For Dummies" — a bilingual (EN/PT-BR) presentation and workshop about
working with coding agents: model routing, subagents, git worktrees, skills,
rules, and verification.** It is published as a static site on a self-hosted
Gitea Pages Server, and it doubles as its own teaching artifact: the hands-on
labs are dependency-free HTML/CSS/JS that workshop attendees point an agent at.
- **Current stack**: hand-written HTML + CSS + ES modules, no build step, no dependencies
- **Target stack**: Astro (see [`plans/astro-refactor/`](plans/astro-refactor/README.md)) — migration in progress
- **Languages**: English and Brazilian Portuguese, toggled client-side
- **Companion service**: `vote-service/` (Go + Kubernetes) — separate lifecycle, see its own README
## Essential commands
```bash
npm run verify # content + interaction contracts (scripts/verify.mjs) — the gate
node scripts/audit-ui.mjs # responsive / no-external-dependency audit
node scripts/build-skill-review.mjs # regenerate skill-reviews/improved/ from catalog.js
npm run serve # python3 -m http.server 4173
```
`npm run verify` is not a formality. It is a set of ~42 string-token assertions
that pin the site's real content and interactions. **A refactor that "passes"
by deleting assertions has failed.** See
[`.agents/context/verification.md`](.agents/context/verification.md).
## Publishing
`main` is the source of truth. The `pages` branch is what the Gitea Pages
Server actually serves, and its tree must end up identical to `main`'s. The
full procedure — including why `merge --ff-only` does *not* work here — is in
[`docs/operations-guide.md`](docs/operations-guide.md).
Adding a build step changes this contract. Read
[`.agents/context/publishing.md`](.agents/context/publishing.md) before doing so.
## Never touch
- `hands-on/starter/` and `hands-on/rules/`**lab fixtures.** The exercise *is*
that they are dependency-free vanilla HTML/CSS/JS an attendee can hand to an
agent. Componentizing them destroys the lesson. They ship as static assets.
- `submitted-skills/` — other people's submitted work, reproduced verbatim
- `skill-reviews/improved/` — generated; edit `skills-review/catalog.js` instead
- `vote-service/` — separate deploy lifecycle; do not fold into the site build
- `package-lock.json`, `dist/`, `node_modules/`
## Rules
Binding. Read the one that covers what you are about to do.
| Rule | When |
| --- | --- |
| [`astro.md`](.agents/rules/astro.md) | any `.astro` file |
| [`theming.md`](.agents/rules/theming.md) | any colour, font, or spacing value |
| [`componentization.md`](.agents/rules/componentization.md) | creating or splitting a component |
| [`animation.md`](.agents/rules/animation.md) | any motion, transition, or transform |
| [`accessibility.md`](.agents/rules/accessibility.md) | any interactive element |
| [`content-i18n.md`](.agents/rules/content-i18n.md) | any user-visible string |
| [`code-style.md`](.agents/rules/code-style.md) | always |
| [`git-worktrees.md`](.agents/rules/git-worktrees.md) | starting or finishing a task |
## The one thing to understand first
This site's styling is **not** a design system today. Three near-duplicate
palettes have drifted apart (`--ink` exists as `#172f42`, `#122534`, and
`#173044`), and the `@font-face` rule in `styles.css:1` is malformed, so the
intended Manrope/DM Mono typography has never actually rendered. "Maintain the
same styles" therefore needs a deliberate decision, not a copy-paste. Read
[`.agents/context/design-system.md`](.agents/context/design-system.md) before
touching any CSS.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+73
View File
@@ -0,0 +1,73 @@
# Which model runs which task
You have **MiniMax-M3** (primary), **Gemini**, and **Codex**. They are not
interchangeable here, and the split is not about which is "smartest" — it is
about which failure mode each task punishes.
**Honest caveat up front:** I have not benchmarked these three on this
repository. The routing below is reasoned from task shape and each model's
known strengths. Validate it cheaply on **task 07** (small, self-contained,
easy to judge) before fanning out across ten worktrees.
## Short answer
**Run the refactor on MiniMax-M3.** It is your primary, the work is mostly
bounded mechanical migration with a hard verification gate, and that is exactly
the shape M3 handles well at low cost. Reach for the other two at three specific
points where M3 is the wrong tool.
## Routing table
| Task | Model | Why this one |
| --- | --- | --- |
| 01 scaffold + gates | **Codex** | Config-heavy, many interacting tools (Astro + husky + lint-staged + CI), and success is binary — it builds and hooks fire, or not. Codex's long autonomous run-until-green loop suits it, and getting the foundation wrong is expensive later. |
| 02 design tokens | **Gemini** | Needs the whole CSS corpus in one context (8 stylesheets, ~90 KB) plus **visual judgement on screenshots**. Gemini's long context and multimodal comparison are the differentiator; the others would work file-by-file and miss cross-file drift. |
| 03 verification net | **Codex** | Writing test tooling with a tight feedback loop. Precision about what an assertion pins matters more than speed. |
| 04 content schema | **MiniMax-M3** | Small, well-specified, one file. |
| 0506 content migration | **MiniMax-M3** | High-volume mechanical string moves with a `diff` as the oracle. Cheap, parallel, verifiable. Exactly M3's sweet spot. |
| 07 primitives | **MiniMax-M3** | Small components from templates. Use this task to calibrate the whole routing decision. |
| 0811 component blocks | **MiniMax-M3 ×4 parallel** | Four bounded tasks, one template each, checklist-gated. Cost per task matters because there are many. |
| 1214, 17 pages | **MiniMax-M3** | Bounded, snapshot-diff verified. |
| 15 full guide | **Codex** | The hard one: 50 KB `app.js`, 12 render functions, tab state, bilingual swap. Long sustained reasoning over interacting pieces; the task most likely to need many iterations against a failing check. |
| 16 review desk | **Codex** | Same shape and worse — search, filtering, file fetching, six query params, markdown rendering, client-side diff. Highest defect risk in the plan. |
| 18 motion | **Gemini** | Judging whether motion looks right is perceptual. Feed it before/after captures. |
| 19 contract re-point | **Codex** | 42 assertions to translate without losing coverage. Meticulous, mechanical, verifiable. |
| 20 cutover | **you, with M3 assisting** | Touches production publishing. A human should be watching. |
| review of every task | **Gemini** | Fresh eyes, whole-diff-in-context, and it did not write the code. Never review with the model that wrote it. |
## The reasoning in one line each
- **MiniMax-M3** — cheapest per task and strong at bounded, tool-driven edits.
Use it for volume: 13 of the 20 tasks. Its weakness is long multi-file
reasoning where the spec is vague; every task above that it owns has a
template and a mechanical oracle.
- **Codex** — best at "keep iterating until the check passes" over a
complicated existing codebase. Use it where the loop is long and the answer is
not obvious: scaffold, the two hard pages, verification.
- **Gemini** — biggest context and genuinely useful multimodal comparison. Use
it where the input is *everything at once* or where the judgement is
**visual**: token consolidation, motion, screenshot diffing, and code review.
## Cross-checking rule
Whatever writes a task must not review it. Pair them: M3 writes → Gemini
reviews; Codex writes → Gemini reviews; Gemini writes → Codex reviews. The
`reviewer` agent definition is model-agnostic on purpose.
## Swap the routing if you see this
| Symptom | Move the task to |
| --- | --- |
| M3 spends more than ~3 iterations failing the same gate | Codex |
| M3 edits files outside its task scope | Codex, and tighten the brief |
| Codex "fixes" a red suite by deleting assertions | anything — but re-read `context/verification.md` to it first; `gate.sh` blocks the merge either way |
| Gemini gives confident visual sign-off with no screenshots attached | require the artifacts; do not accept prose |
| A task needs more than two models' worth of hand-holding | the task is too big — split it |
## Cost shape
Tasks 0514 are ~two-thirds of the work and are all M3-eligible. Running those
on Codex would work and cost several times more for no measurable quality gain,
because the checklist and `gate.sh` — not the model — are what guarantee those
outputs. Spend the expensive models where there is no mechanical oracle: 15, 16,
and the visual judgement calls.
+94
View File
@@ -0,0 +1,94 @@
# Astro refactor — plan
**Status: not started. These are briefs, not work.** Nothing in this plan has
been implemented.
Goal: move `ai-for-dummies` from ten hand-written HTML pages to Astro, so that
adding a chapter is a component and a content entry rather than a copy-pasted
file — **without changing how the site looks, what it says, or what it costs a
visitor to load.**
## Read before starting anything
| File | Why |
| --- | --- |
| [`../../AGENTS.md`](../../AGENTS.md) | entry point |
| [`../../.agents/context/design-system.md`](../../.agents/context/design-system.md) | three drifting palettes, a font that has never rendered |
| [`../../.agents/context/verification.md`](../../.agents/context/verification.md) | 42 assertions that will all break, and must not be deleted |
| [`../../.agents/context/publishing.md`](../../.agents/context/publishing.md) | Gitea Pages serves a branch and cannot build |
## The three things most likely to go wrong
1. **Content loss that nobody notices.** 50 KB of bilingual copy moves between
files. Snapshot every route *before* migrating it — task 03 exists to make
that possible and blocks all page work.
2. **Assertions deleted to make a red suite green.** That converts a content-loss
bug into a passing build. `gate.sh` refuses a coverage drop.
3. **Base-path bugs.** The site lives at `/ai-for-dummies/`. It will work
perfectly in `npm run preview` and 404 in production. Verify on the real host.
## Phases
```
Phase 0 foundation 01 → (02 ∥ 03 ∥ 04)
Phase 1 content 05 ∥ 06 after 04
Phase 2 components 07 → (08 ∥ 09 ∥ 10 ∥ 11) after 02
Phase 3 pages 12 ∥ 13 ∥ 14 ∥ 17, then 15 ∥ 16
Phase 4 polish 18 ∥ 19, then 20
```
| # | Task | Agent | Depends on | Parallel with |
| --- | --- | --- | --- | --- |
| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
| 04 | [content schema](task-04-content-schema.md) | content-i18n-migrator | 01 | 02, 03 |
| 05 | [guide content](task-05-content-guide.md) | content-i18n-migrator | 04 | 06 |
| 06 | [review-desk content](task-06-content-review.md) | content-i18n-migrator | 04 | 05 |
| 07 | [primitives](task-07-primitives.md) | component-builder | 02 | — |
| 08 | [route cards](task-08-route-cards.md) | component-builder | 07 | 09, 10, 11 |
| 09 | [chapter blocks](task-09-chapter-blocks.md) | component-builder | 07 | 08, 10, 11 |
| 10 | [guide blocks](task-10-guide-blocks.md) | component-builder | 07 | 08, 09, 11 |
| 11 | [review-desk blocks](task-11-review-blocks.md) | component-builder | 07 | 08, 09, 10 |
| 12 | [landing page](task-12-page-landing.md) | page-migrator | 03, 08 | 13, 14, 17 |
| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
| 15 | [full guide](task-15-page-full-guide.md) | page-migrator | 05, 10, 13 | 16 |
| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15 |
| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
| 18 | [motion pass](task-18-motion.md) | motion-designer | 15, 16 | 19 |
| 19 | [contract re-point](task-19-verify-repoint.md) | verification-engineer | 15, 16 | 18 |
| 20 | [cutover + cleanup](task-20-cutover.md) | astro-architect | all | — |
Widest parallelism: **four agents** (tasks 0811, then 12/13/14/17). More than
that and they start contending on review capacity, not on files.
## Running a task
```bash
.agents/scripts/worktree.sh start 08 route-cards
cd ../af-task-08
# agent reads: plans/astro-refactor/task-08-route-cards.md
# .agents/agents/component-builder.md (+ the skills it names)
```
The script runs `npm ci` and `verify-hooks.sh` for you. That matters: `.husky/_`
is generated, not committed, so a hand-made worktree has hooks configured but
**silently not running**.
Finishing:
```bash
npm run gate # tier 2, same as pre-push
# reviewer agent reads the diff against .agents/checklists/before-merge.md
.agents/scripts/worktree.sh finish 08 route-cards
```
## Which model to run each task
See [`MODEL-ROUTING.md`](MODEL-ROUTING.md).
## These files must be committed
Worktrees check out tracked files. If this plan stays untracked, every worktree
you create will be missing it. Commit `plans/` and `.agents/` before fanning out.
+59
View File
@@ -0,0 +1,59 @@
# Task 01 — Astro scaffold, gates, and the publishing decision
**Agent**: `astro-architect` · **Model**: Codex · **Depends on**: nothing
**Blocks**: everything · **Worktree**: `.agents/scripts/worktree.sh start 01 scaffold`
## Goal
A working Astro project that builds, serves one page correctly from the real
host under `/ai-for-dummies/`, and has all three gate tiers live.
## Scope
`astro.config.mjs`, `package.json`, `tsconfig.json`, `src/layouts/BaseLayout.astro`,
`.husky/`, `.lintstagedrc.json`, lint configs, `.gitea/workflows/verify.yml`,
`docs/operations-guide.md`.
Copy the configs from `.agents/templates/config/` — they are written for this
project (correct ignores for `hands-on/`, `submitted-skills/`, `vote-service/`).
## Steps
1. `npm create astro@latest` into the worktree — minimal template, TypeScript
strict, **no UI framework, no CSS framework**.
2. Set `base: '/ai-for-dummies'`. Every internal link goes through
`import.meta.env.BASE_URL` from here on.
3. `src/layouts/BaseLayout.astro`: `<html lang>`, viewport meta, title,
description, global styles slot. Nothing clever.
4. Merge `.agents/templates/config/package.scripts.json` into `package.json`.
**`"prepare": "husky"` is what makes hooks exist** — without it every hook is
inert.
5. `npm install husky lint-staged prettier eslint stylelint …`, then `npx husky init`.
6. Copy the three hooks and the lint configs into place. Verify with
`.agents/scripts/verify-hooks.sh`.
7. Migrate **one** page (`summary/` — smallest, zero JS) as a smoke test.
8. Copy `hands-on/` into `public/` (task 17 does this properly; a stub is fine here).
9. **Make the publishing decision** per `.agents/context/publishing.md`. Recommended:
Gitea Actions builds `dist/` and pushes `pages`. Copy
`.agents/templates/config/gitea-ci.yaml` to `.gitea/workflows/verify.yml`.
10. Rewrite the publishing section of `docs/operations-guide.md` to match.
## Done when
- [ ] `npm run build` succeeds; `npm run gate` passes
- [ ] `.agents/scripts/verify-hooks.sh` reports hooks live
- [ ] A bad commit message is rejected; a raw hex in a `.astro` file is rejected
- [ ] `/ai-for-dummies/summary/` serves correctly **from the real host**, not just preview
- [ ] `docs/operations-guide.md` describes the actual publishing path
## Do not
- Do not migrate more than one page. That is tasks 1216.
- Do not add a UI framework, CSS framework, or any runtime dependency.
- Do not touch `hands-on/` contents, `submitted-skills/`, or `vote-service/`.
## Watch for
The base path is the #1 production-only failure in this migration. `npm run
preview` will lie to you. Deploy the smoke-test page and curl it with a
`?v=<sha>` cache-buster.
+56
View File
@@ -0,0 +1,56 @@
# Task 02 — Design token layer
**Agent**: `design-system-keeper` · **Model**: Gemini (long context + visual judgement)
**Depends on**: 01 · **Parallel with**: 03, 04 · **Blocks**: 07
**Worktree**: `.agents/scripts/worktree.sh start 02 tokens`
## Goal
One value per token, a named type scale, five breakpoints — and photographic
proof the site looks the same.
## Read first
`.agents/context/design-system.md`. It documents two traps you will otherwise
walk into.
## Scope
`src/styles/tokens.css`, `src/styles/base.css`,
`.agents/scripts/check-tokens.mjs`. You are the only writer of these.
## The two decisions to surface
1. **Three palettes → one.** `--ink` is `#172f42` / `#122534` / `#173044`;
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
sub-perceptual — canonicalize. **`--blue` (`#527f9f` vs `#215675`) is visibly
different**: screenshot both, get a human decision, record it here.
2. **The fonts have never rendered.** `styles.css:1` has a malformed
`@font-face` whose `src:` points at a Google Fonts *stylesheet*. Manrope and
DM Mono have always fallen back to Arial and generic monospace. Default:
delete the dead rule, declare what actually renders. Self-hosting the real
fonts is a **redesign** and needs sign-off.
## Steps
1. Run the extraction script in `.agents/skills/design-tokens/SKILL.md`.
2. Capture baseline screenshots: 10 routes × {560, 800, 1100, 1600} **plus the
eight breakpoint widths you are removing** (520, 530, 600, 620, 720, 850,
880, 900).
3. Write `tokens.css`: one value per semantic name, `--step-*` type scale
replacing the 14 ad-hoc `clamp()` triples, five `--bp-*` widths.
4. Re-capture. Compare. Explain every visible difference.
## Done when
- [ ] Extraction prints exactly one value per token
- [ ] `node .agents/scripts/check-tokens.mjs` passes on `src/`
- [ ] Before/after screenshots attached at all twelve widths
- [ ] The `--blue` decision and the font decision are written down here
## Do not
- Do not add a webfont.
- Do not rename tokens to numeric scales (`--color-neutral-900`).
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
is deliberate house style and appears everywhere.
@@ -0,0 +1,49 @@
# Task 03 — Verification net
**Agent**: `verification-engineer` · **Model**: Codex
**Depends on**: 01 · **Parallel with**: 02, 04 · **Blocks**: 1216
**Worktree**: `.agents/scripts/worktree.sh start 03 verification-net`
## Goal
Rendered-text baselines for all ten routes, captured from the **vanilla site**,
before any page is migrated. Without this the page migrators have nothing to
diff against and "no content was lost" becomes an opinion.
This task is on the critical path. Do it early.
## Scope
`.agents/snapshots/`, `scripts/audit-ui.mjs`, `.agents/scripts/visual-regression.mjs`.
## Steps
1. `npm run serve` against the **current, unmigrated** site.
2. Snapshot all ten routes:
```bash
for r in "" full-guide summary models agents skills rules skills-review \
hands-on/starter hands-on/rules; do
node .agents/scripts/snapshot-route.mjs "http://localhost:4173/$r/" \
> ".agents/snapshots/${r:-index}.txt"
done
```
Commit them. They are the regression net.
3. Write `.agents/scripts/visual-regression.mjs` (Playwright). Extend the
existing `scripts/inspect.py` pattern rather than inventing one. Baselines to
`.agents/snapshots/before/`.
4. **Fix the audit gap**: `audit-ui.mjs` rejects external `<script>`/`<link>`
but misses external URLs in CSS — which is exactly how the broken Google
Fonts `@font-face` got into this "dependency-free" site. Add `@import`,
`src: url(https:…)`, `url(https:…)`.
## Done when
- [ ] Ten committed snapshots, each non-empty and containing that page's real prose
- [ ] `visual-regression.mjs` captures 10 routes × 4 widths
- [ ] Extended `audit-ui.mjs` **fails** on today's `styles.css` (prove it catches the real bug), then the dead rule is removed by task 02
- [ ] `npm run gate` green
## Do not
Do not change any assertion in `verify.mjs` yet. That is task 19, after the
pages exist.
@@ -0,0 +1,37 @@
# Task 04 — Content collection schema
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3
**Depends on**: 01 · **Parallel with**: 02, 03 · **Blocks**: 05, 06
**Worktree**: `.agents/scripts/worktree.sh start 04 content-schema`
## Goal
Typed collections that make a missing translation a build error.
## Scope
`src/content/config.ts` only. You own it.
## Steps
1. Define `const localized = z.object({ en: z.string(), pt: z.string() })`.
**Both required.** A missing `pt` must fail the build — silent English
fallback is how bilingual sites quietly become monolingual.
2. Collections:
- `guide``phases`, `modelGuide`, `skillSources`, `handsOnPrompts`, `skillInstallPrompts` from `app.js`
- `chapters` — copy for `/models/`, `/agents/`, `/skills/`, `/summary/`
- `reviews` — the 24 entries: `id`, `author`, `title`, `status`, `focus`, `wins[]`, `improve[]`, `extras`, `improved`
3. **Make the language-switching decision** and record it here:
- *client-side swap* — matches today, no URL change, both languages in the payload. **Recommended.**
- *route-based `/en/` `/pt/`* — better SEO, changes every existing URL, needs redirects.
## Done when
- [ ] `astro check` passes
- [ ] A deliberately missing `pt` field fails the build (prove it, then revert)
- [ ] The language decision is written down here with its reason
## Do not
Do not move any content yet. Schema only — tasks 05 and 06 fill it, and they
run in parallel against the shape you define.
@@ -0,0 +1,38 @@
# Task 05 — Guide content out of app.js
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3
**Depends on**: 04 · **Parallel with**: 06 · **Blocks**: 15
**Worktree**: `.agents/scripts/worktree.sh start 05 content-guide`
## Goal
Every `{ en, pt }` string in `app.js` lives in `src/content/guide/`, byte-identical.
## Scope
`src/content/guide/**`. Do **not** delete anything from `app.js` yet — task 15
removes it once the page consumes the collection.
## Steps
1. `node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json`
(~50 pairs across `phases`, `handsOnPrompts`, `modelGuide`, `skillSources`,
`skillInstallPrompts`).
2. Move them into the collection. **Copy mechanically — never retype.** These
are hand-written translations with deliberate tone (`'Transforme ambiguidade
em trabalho'`); retyping introduces drift nobody catches until a Portuguese
speaker reads it.
3. `node .agents/scripts/extract-strings.mjs src/content/guide/ > /tmp/after.json`
4. `diff /tmp/before.json /tmp/after.json`**must be empty**.
## Watch for
`handsOnPrompts` values are multi-line prompt strings built with `.join('\n')`.
Preserve the exact line breaks and leading `-` bullets: they are copy-pasted by
workshop attendees into an agent, and a mangled prompt breaks the exercise.
## Done when
- [ ] String diff empty
- [ ] `astro check` passes; both locales present on every entry
- [ ] `app.js` still untouched and the site still works
@@ -0,0 +1,43 @@
# Task 06 — Review-desk content out of catalog.js
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3
**Depends on**: 04 · **Parallel with**: 05 · **Blocks**: 16
**Worktree**: `.agents/scripts/worktree.sh start 06 content-review`
## Goal
24 review entries become a typed collection, with the `improved` markdown as
real `.md` files.
## Scope
`src/content/reviews/**`, `scripts/build-skill-review.mjs`.
## Steps
1. Move all 24 entries from `skills-review/catalog.js` (27 KB) and
`submitted-catalog.js` (18 KB) into the collection.
2. `improved` becomes a real Markdown file per entry, rendered at build time.
That deletes the hand-rolled client-side renderer.
3. **Re-point `scripts/build-skill-review.mjs`** at the collection.
## The two traps
1. **The generator writes committed files.**
`skill-reviews/improved/**/SKILL.md` is generated from the `improved` fields
and the output is in git. Move `catalog.js` and the generator keeps running
against nothing — **silently, exit code 0**. Prove it still produces
byte-identical output:
```bash
node scripts/build-skill-review.mjs && git diff --exit-code skill-reviews/
```
2. **The diff view needs raw source.** The review desk compares original and
improved as *text*. If `improved` only exists as rendered HTML, the change
lens breaks. Keep the raw string reachable.
## Done when
- [ ] 24 entries in the collection; `verify.mjs`'s `id:'` count assertion still passes
- [ ] `git diff --exit-code skill-reviews/` clean after regenerating
- [ ] `astro check` passes
- [ ] Every reference to the generator still accurate (`package.json`, `README.md`, `docs/operations-guide.md`, the review desk footer)
@@ -0,0 +1,45 @@
# Task 07 — Primitives
**Agent**: `component-builder` · **Model**: MiniMax-M3 — **use this task to
calibrate the model routing before fanning out**
**Depends on**: 02 · **Blocks**: 0811
**Worktree**: `.agents/scripts/worktree.sh start 07 primitives`
## Goal
The four smallest reusable pieces, so the four parallel block tasks compose
rather than reinvent.
## Scope
`src/components/primitives/``Eyebrow.astro`, `Rule.astro`, `Callout.astro`,
`CodeBlock.astro`. Nothing else.
## Why these four
Each appears 10+ times across the current stylesheets:
- **Eyebrow** — `1011px` monospace, `letter-spacing:.08.1em`, uppercase. The
single most repeated treatment on the site and a signature of the design.
- **Rule** — the `border-top: 4px solid var(--gold)` section divider.
- **Callout** — gold-background emphasis block (`.callout`, `.thesis`).
- **CodeBlock** — `<pre>` on `--ink` with gold text.
## Steps
Copy `.agents/templates/components/static-block.astro` for each. Typed props,
tokens only, zero JS. Then grep the current CSS for every place each treatment
appears and confirm the component covers them all — if it needs five variants,
you have found two components, not one.
## Done when
- [ ] `.agents/checklists/before-component.md` complete for all four
- [ ] Rendered output visually identical to the CSS classes they replace
- [ ] `npm run gate` green
- [ ] **Routing note written**: how many iterations, what the model got wrong.
This decides whether 0811 run on M3 or move to Codex.
## Do not
Do not build blocks or touch pages. Do not edit `tokens.css`.
@@ -0,0 +1,37 @@
# Task 08 — Route cards and grid group
**Agent**: `component-builder` · **Model**: MiniMax-M3
**Depends on**: 07 · **Parallel with**: 09, 10, 11 · **Blocks**: 12
**Worktree**: `.agents/scripts/worktree.sh start 08 route-cards`
## Goal
The landing page's six chapter cards become one component over data.
## Scope
`src/components/blocks/RouteCard.astro`, `src/components/blocks/GridGroup.astro`.
## Source
`index.html` — six `<article class="card">` blocks differing only in number,
title, summary, and href. Textbook extraction. Styles in `landing.css` +
`chapters.css`.
## Steps
1. `GridGroup` from `.agents/templates/components/grid-group.astro`. Preserve
the `gap:1px` over a coloured parent separator technique — it is deliberate.
2. `RouteCard` from `static-block.astro`: `number`, `title`, `summary`, `href`.
3. Card content moves to the `chapters` collection, both locales.
## Done when
- [ ] Six cards render identically to `index.html` today
- [ ] Screenshots at 560/800/1100/1600 match
- [ ] Zero JS
- [ ] `.agents/checklists/before-component.md` complete; `npm run gate` green
## Do not
Do not migrate `index.html` itself — that is task 12.
@@ -0,0 +1,38 @@
# Task 09 — Chapter blocks
**Agent**: `component-builder` · **Model**: MiniMax-M3
**Depends on**: 07 · **Parallel with**: 08, 10, 11 · **Blocks**: 13, 14
**Worktree**: `.agents/scripts/worktree.sh start 09 chapter-blocks`
## Goal
The shared furniture of `/models/`, `/agents/`, `/skills/`, `/summary/`,
`/rules/` — five pages that today duplicate the same markup five times.
## Scope
`src/components/blocks/``ChapterHero.astro`, `SectionGrid.astro`,
`ComparisonTable.astro`, `TopBar.astro`, `SiteFooter.astro`.
`src/layouts/ChapterLayout.astro`.
## Source
`chapters.css` is already the shared layer; the duplication is in the five HTML
files. Diff them against each other first — what differs is props, what matches
is the component.
## Watch for
- `TopBar` carries `aria-current="page"`. Preserve it; it is the only
indication of location for assistive tech.
- The tables in `/models/` and `/rules/` scroll horizontally on narrow screens
(`overflow-x:auto`, `min-width` on the inner element). Keep that — dropping it
makes the tables unreadable on a phone, and `audit-ui.mjs` asserts related
responsive tokens.
## Done when
- [ ] All five pages' markup expressible with these components
- [ ] Rendered text identical to current pages
- [ ] Zero JS
- [ ] Checklist complete; `npm run gate` green
@@ -0,0 +1,44 @@
# Task 10 — Full-guide blocks
**Agent**: `component-builder` · **Model**: MiniMax-M3
**Depends on**: 07 · **Parallel with**: 08, 09, 11 · **Blocks**: 15
**Worktree**: `.agents/scripts/worktree.sh start 10 guide-blocks`
## Goal
The full guide's distinctive sections as components — **static shells only**.
Task 15 wires the interactivity.
## Scope
`src/components/blocks/``PhasePanel.astro`, `FleetDiagram.astro`,
`HandoffTable.astro`, `WorktreeMap.astro`, `RouteTable.astro`,
`SkillPackage.astro`.
## Source
`full-guide/index.html` (22 KB) + `styles.css` + `responsive.css`.
## Critical: the `data-*` hooks are asserted by name
`verify.mjs` requires these survive: `data-phase="plan|build|review"`,
`data-tree="main|ui"`, `data-worker="ui"`, `data-route="plan"`,
`data-model-provider="openai|claude|gemini"`, `data-effort="low|medium|high"`,
`data-skill-file="skill"`, `data-skill-step="observe|validate"`,
`data-common-skill="ponytail|caveman|unlazy"`, `role="tablist"`, `id="hands-on"`.
Each is a real feature hook, not decoration. Losing one is losing a feature.
## Watch for
`responsive.css` is 30 KB of append-only overrides on top of `styles.css`. **Do
not port it wholesale.** Take what each component needs, then prove the rest is
dead. Expect to delete most of it — that is a win, not a risk, provided the
screenshots agree.
## Done when
- [ ] Every `data-*` hook above present in rendered output
- [ ] Components take props and render static markup; **no `client:*` yet**
- [ ] Screenshots match the current guide at four widths
- [ ] Checklist complete; `npm run gate` green
@@ -0,0 +1,49 @@
# Task 11 — Review-desk blocks
**Agent**: `component-builder` · **Model**: MiniMax-M3
**Depends on**: 07 · **Parallel with**: 08, 09, 10 · **Blocks**: 16
**Worktree**: `.agents/scripts/worktree.sh start 11 review-blocks`
## Goal
Review-desk furniture as static components. Interactivity is task 16.
## Scope
`src/components/blocks/``SkillList.astro`, `ReviewDetail.astro`,
`FileTabs.astro`, `PreviewPane.astro`, `ChangeLens.astro`, `VoteWidget.astro`
(markup only).
## Source
`skills-review/index.html`, `app.js` (20 KB), `styles.css`, `change-lens.css`,
`vote.js`.
## One component, 24 entries
The 24 reviews are **data**, not 24 components. One `SkillReviewCard` iterating
the collection. If you are writing the second near-identical component, stop.
## CSS tokens asserted by verify.mjs
`.change-lens`, `.change-rows`, `.skill-diff`, `.diff-lines`,
`.markdown-preview`, `max-height:540px`, `.markdown-table-wrap`,
`.markdown-frontmatter`, `.markdown-toc`, `.preview-title`, `.preview-markdown`,
`grid-template-columns:minmax(0,1fr)`, `height:120px`, `-webkit-line-clamp:2`,
`.vote-widget`, `.vote-buttons`, `[aria-pressed="true"]`.
Class names may move into scoped `<style>` blocks, but each must still exist and
task 19 must be able to assert it. Coordinate names with the verification
engineer rather than renaming freely.
## Watch for
`aria-pressed` on the vote buttons and preview switcher is how state reaches
assistive tech. Colour alone is not enough. It is also asserted.
## Done when
- [ ] All listed CSS hooks present
- [ ] `aria-pressed`, `role="group"`, `aria-label` preserved
- [ ] Static render matches current desk at four widths
- [ ] Checklist complete; `npm run gate` green
@@ -0,0 +1,30 @@
# Task 12 — Landing page
**Agent**: `page-migrator` · **Model**: MiniMax-M3
**Depends on**: 03, 08 · **Parallel with**: 13, 14, 17
**Worktree**: `.agents/scripts/worktree.sh start 12 page-landing`
## Goal
`index.html``src/pages/index.astro`. Smallest page migration; sets the
pattern the other page tasks follow.
## Steps
Snapshot first (task 03 baseline exists — diff against it). Then compose from
`RouteCard` + `GridGroup`, move copy to the `chapters` collection, port
`landing.css` into component styles.
## Hard constraints
- URL stays `/ai-for-dummies/` exactly
- **Zero JS** — this page has none today
- `<title>`, `<meta name="description">`, viewport preserved
- Internal links via `import.meta.env.BASE_URL`, never hand-written absolute
## Done when
- [ ] `diff .agents/snapshots/index.txt <(snapshot of dist)` empty
- [ ] Screenshots match at four widths
- [ ] Built page ships **0 bytes** of JS
- [ ] `.agents/checklists/before-page.md` complete; `npm run gate` green
@@ -0,0 +1,33 @@
# Task 13 — Chapter pages ×4
**Agent**: `page-migrator` · **Model**: MiniMax-M3
**Depends on**: 03, 09 · **Parallel with**: 12, 14, 17 · **Blocks**: 15, 16
**Worktree**: `.agents/scripts/worktree.sh start 13 page-chapters`
## Goal
`/models/`, `/agents/`, `/skills/`, `/summary/` as Astro routes. One task
because they share a layout — four near-identical migrations, and doing them
together is what proves `ChapterLayout` is right.
Blocks 15 and 16 because those pages link here and share the layout.
## Hard constraints
- Four URLs unchanged, **trailing slashes included**
- `/skills/` has an interactive package explorer (`skills/app.js`,
`data-skill-file`, `data-skill-step`) — that becomes an **island**, and it is
the only JS across these four pages. The other three ship zero.
- All `data-*` hooks preserved; `verify.mjs` asserts several
## Done when
- [ ] Four snapshot diffs empty
- [ ] Three pages ship 0 bytes JS; `/skills/` ships only its island
- [ ] Screenshots match at four widths
- [ ] Checklist complete for each page; `npm run gate` green
## Do not
Do not generalise `ChapterLayout` beyond these four. `/rules/` is task 14 and
has its own interaction model.
@@ -0,0 +1,33 @@
# Task 14 — Rules page
**Agent**: `page-migrator` · **Model**: MiniMax-M3
**Depends on**: 03, 09 · **Parallel with**: 12, 13, 17
**Worktree**: `.agents/scripts/worktree.sh start 14 page-rules`
## Goal
`/rules/` → Astro. Separate from task 13: it is a standalone bilingual case
study with its own stylesheet (`rules/styles.css`, 9.8 KB), its own script
(`rules/app.js`, 16 KB), and its own responsive contract.
## Asserted by verify.mjs
- `rulesHtml` must have **no external `<script>`/`<link>`** — it is explicitly
checked as a standalone page
- `rulesCss` must contain `@media(min-width:2200px)`, `@media(max-width:900px)`,
`@media(max-width:600px)`, `prefers-reduced-motion`
Task 02 collapses those breakpoints to named tokens. **Coordinate with the
verification engineer** — the assertion must be re-pointed at the token, not
deleted. Screenshot at the old widths (900, 600, 2200) to prove equivalence.
## Watch for
`rules/styles.css` shares `--accent` and `--deep` with `styles.css` — it is on
the *first* palette. Confirm task 02 canonicalized it the same way.
## Done when
- [ ] Snapshot diff empty; bilingual toggle works, `<html lang>` follows
- [ ] Responsive behaviour identical at 600/900/2200
- [ ] No external dependency; checklist complete; `npm run gate` green
@@ -0,0 +1,62 @@
# Task 15 — Full guide
**Agent**: `page-migrator` · **Model**: **Codex** — hardest task in the plan
**Depends on**: 05, 10, 13 · **Parallel with**: 16
**Worktree**: `.agents/scripts/worktree.sh start 15 page-full-guide`
## Goal
`/full-guide/` → Astro. 22 KB of HTML, a 50 KB script, 30 KB of CSS, twelve
render functions, bilingual throughout.
## What `app.js` actually is
Not application code — a **bilingual content database** (task 05 already moved
it) plus ~12 `render*` functions that swap `innerHTML` on tab clicks. Once
content is a collection, the remaining JS is small: tab state and a language
toggle.
## Islands
Only these hydrate. Everything else is server-rendered.
| Island | Directive | Why |
| --- | --- | --- |
| Phase tabs | `client:visible` | click-driven panel swap |
| Tree / worker / route / model / effort / skill selectors | `client:visible` | same pattern; consider one generic selector island |
| Language toggle | `client:idle` | page-wide, not urgent |
| Copy-prompt buttons | `client:visible` | clipboard |
If you end up with 12 separate islands you have missed the pattern — they are
one selector component with different data.
## Asserted by verify.mjs — all must survive
`const phases`, `const handsOnPrompts`, `const modelGuide`, `const skillSources`,
`const skillInstallPrompts`, `render('plan')`, `renderTree`, `renderWorker`,
`renderRoute`, `renderModelProvider`, `renderEffort`, `renderSkillFile`,
`renderSkillWorkflow`, `renderCommonSkill`, `renderHandsOn`, `copyPrompt`.
These are **implementation-detail assertions** — they look deletable and are
not. Each pins a feature. Coordinate with task 19 to replace each with an
output-level assertion of the same behaviour. **Never delete one yourself.**
Also: `data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`,
`hands-on/starter/`, `additional-reading.md`, `role="tablist"`, `<table>`.
## Watch for
- `copyPrompt` uses `navigator.clipboard` with a `document.execCommand`
fallback. Keep both — the fallback exists for non-secure contexts.
- The hands-on prompt strings are copy-pasted by attendees into an agent.
Exact whitespace and line breaks matter.
- `responsive.css` (30 KB) mostly serves this page. Port what is needed, prove
the rest dead, delete it. Screenshots are the proof.
## Done when
- [ ] Snapshot diff empty
- [ ] Every interaction works: all tabs, both languages, all copy buttons
- [ ] Keyboard: arrow keys move between tabs; focus visible throughout
- [ ] JS payload **smaller** than today's 50 KB (content is now static)
- [ ] Screenshots match at four widths; checklist complete; `npm run gate` green
@@ -0,0 +1,63 @@
# Task 16 — Review desk
**Agent**: `page-migrator` · **Model**: **Codex** — highest defect risk
**Depends on**: 06, 11, 13 · **Parallel with**: 15
**Worktree**: `.agents/scripts/worktree.sh start 16 page-review-desk`
## Goal
`/skills-review/` → Astro. The most interactive page: search, filtering,
lazy file fetching, markdown rendering, a client-side diff view, six URL params,
and a live API call to `vote-service/`.
## The six query params are a public contract
`?author=`, `?skill=`, `?view=`, `?file=`, `?compare=`, `?render=` — documented
in the page footer and shared externally. All six must round-trip, and browser
back/forward must restore state (`syncUrl` / `selectFromUrl` today).
**Test every one manually.** A snapshot diff cannot catch a broken deep link.
## Islands
The catalog + detail pane is genuinely interactive: `client:load` is justified
here. The vote widget is `client:visible`. Everything else server-renders.
## vote-service integration
`window.SKILLS_REVIEW_VOTE_API = 'https://ai-for-dummies-vote.marcospaulo.dev.br'`
is set inline in `index.html` today. Either keep the global or move it to
`PUBLIC_VOTE_API` — **if you move it, update `vote-service/README.md` in the
same change**, since it documents the coupling.
`ALLOWED_ORIGIN` on the deployed service is
`https://netcracker.pages.marcospaulo.dev.br`. If the site's origin changes, the
vote API breaks with a CORS error. It does not change in this plan — but verify
after cutover.
## Asserted by verify.mjs
Interaction tokens: `from './catalog.js'`, `from './files.js'`, `renderList`,
`renderDetail`, `selectSkill`, `packageSummary`, `markdownHeadings`,
`markdownToc`, `loadSelectedFile`, `schedulePackageSearch`, `fetchSource`,
`packageSearchText`, `diffMarkup`, `diffRows`, `searchParams.set('compare')`,
`markdownMarkup`, `syncUrl`, `selectFromUrl`, `URLSearchParams`,
`navigator.clipboard`, `document.execCommand`.
Plus the catalog count: `id:'` occurrences across both catalogs **must equal 24**.
Same rule as task 15 — re-point with task 19, never delete.
## Watch for
The markdown renderer is hand-rolled (`markdownMarkup`, `markdownHeadings`,
`markdownToc`). Task 06 moves rendering to build time — but the **diff view
needs raw source text**, not rendered HTML. Keep both available.
## Done when
- [ ] All six query params round-trip; back/forward restores state
- [ ] Search, filter, file tabs, preview, change lens, download, copy all work
- [ ] Vote widget reaches the live API; CORS preflight succeeds
- [ ] Snapshot diff empty; screenshots match; checklist complete
- [ ] `npm run gate` green
+38
View File
@@ -0,0 +1,38 @@
# Task 17 — hands-on passthrough
**Agent**: `astro-architect` · **Model**: MiniMax-M3
**Depends on**: 01 · **Parallel with**: 12, 13, 14
**Worktree**: `.agents/scripts/worktree.sh start 17 hands-on`
## Goal
`hands-on/starter/` and `hands-on/rules/` ship **byte-identical** from
`public/`, unprocessed.
## Why this is a task and not a footnote
These are **lab fixtures**. The workshop exercise is that an attendee points an
agent at dependency-free HTML/CSS/JS and watches it work. Componentizing them,
minifying them, or letting a bundler touch them destroys the lesson — and the
prompts in `app.js` reference these files by path and content.
The risk is an agent "helpfully" improving them. This task exists to say
explicitly: do not.
## Steps
1. Move both directories to `public/hands-on/`.
2. Confirm Astro copies `public/` verbatim with no processing.
3. Confirm URLs `/ai-for-dummies/hands-on/starter/` and `.../rules/` resolve.
4. Confirm every lint/format config **ignores** them (the templates already do).
## Done when
- [ ] `diff -r` between the original directories and `dist/hands-on/` is empty
- [ ] Both URLs serve; both labs work standalone
- [ ] The `hands-on/starter/` reference in the full guide still resolves
- [ ] `verify.mjs`'s `starterHtml` / `starterJs` assertions still pass
## Do not
Do not reformat, lint, componentize, or "modernise" a single line.
+47
View File
@@ -0,0 +1,47 @@
# Task 18 — Motion pass
**Agent**: `motion-designer` · **Model**: Gemini (perceptual judgement)
**Depends on**: 15, 16 · **Parallel with**: 19
**Worktree**: `.agents/scripts/worktree.sh start 18 motion`
## Goal
Audit the motion that exists, add only motion that earns its place, and make
`prefers-reduced-motion` correct everywhere.
## Order of work — audit first
The audit is worth more than the additions.
1. Inventory every `transition`, `animation`, `@keyframes`, `transform` in `src/`.
2. Flag anything animating a layout property (`width`, `height`, `top`, `left`,
`margin`). Those force reflow every frame and fail the 200ms INP budget. The
Stylelint config already blocks new ones; find the ported ones.
3. Confirm every animated element has a reduced-motion path, then **test it**
under DevTools emulation. Reduced means reduced, not broken — end states must
still be correct.
## Then, sparingly
Candidates, each needing a written purpose:
- tab panel change in the guide (state change — justified)
- review-desk detail swap on selection (state change — justified)
- vote widget tally update (feedback — justified)
`transform` and `opacity` only. 150250ms. `cubic-bezier(.2,0,0,1)`. One thing
moves at a time — no staggered card cascades; this design has a point of view
and staggering reads as a template.
## View transitions — optional, decide explicitly
Astro's `<ClientRouter />` is the only sanctioned motion dependency. If adopted,
verify: JS-disabled navigation, browser back/forward, the review desk's six
query params, and reduced motion. If any fails, do not ship it.
## Done when
- [ ] Zero animations on layout properties
- [ ] Every animation has a one-line written purpose
- [ ] Reduced-motion tested under emulation; end states correct
- [ ] Before/after captures attached; `npm run gate` green
@@ -0,0 +1,44 @@
# Task 19 — Re-point the verification contract
**Agent**: `verification-engineer` · **Model**: Codex
**Depends on**: 15, 16 · **Parallel with**: 18
**Worktree**: `.agents/scripts/worktree.sh start 19 verify-repoint`
## Goal
All 42 assertions pin the same user-visible facts against the new architecture.
Coverage does not fall.
You are the only role permitted to remove an assertion, and every removal needs
a written reason.
## The three kinds
| Kind | Example | What to do |
| --- | --- | --- |
| Content presence | `data-phase="plan"` | re-point at `dist/full-guide/index.html`; the token should survive rendering. If it does not, a component dropped content — **stop and report** |
| Implementation detail | `const phases`, `renderTree`, `from './catalog.js'` | obsolete as written, but each pins a **feature**. Replace with an output-level assertion of that feature. Never drop |
| Asset version | `app.js?v=20260904-vote-widget` | Astro hashes assets — assert the built HTML references a hashed asset |
## Steps
1. `npm run build`, then re-point `read()` calls at `dist/`.
2. Work through all 42 in order. For each: does the fact it pins still exist?
Yes → re-point. No → content was lost; escalate.
3. Add rendered-text snapshot assertions for all ten routes so this class of
regression is caught structurally, not by string luck.
4. Confirm `check-tokens.mjs` and the extended `audit-ui.mjs` are in
`npm run verify`.
## Done when
- [ ] `grep -c 'throw new Error' scripts/verify.mjs` ≥ the `origin/main` baseline
- [ ] Every removal has a one-line reason in this file
- [ ] Snapshot assertions cover all ten routes
- [ ] `npm run gate` green, and it **fails** when you deliberately delete a
paragraph from a component (prove the net works, then revert)
## Do not
Do not weaken an assertion to make it pass. If it cannot pass, something is
broken — that is the assertion doing its job.
+58
View File
@@ -0,0 +1,58 @@
# Task 20 — Cutover and cleanup
**Agent**: `astro-architect`, **with a human watching** · **Model**: MiniMax-M3 assisting
**Depends on**: all · **Worktree**: `.agents/scripts/worktree.sh start 20 cutover`
This task touches production publishing. Do not run it unattended.
## Goal
The Astro build is what the world sees, the old files are gone, and the docs
describe reality.
## Steps
1. **Full verification** on the built site: all 10 routes, all 6 query params,
both languages, vote widget against the live API, screenshots at four widths.
2. **Delete the superseded files** — only after their replacements are proven:
`app.js`, `styles.css`, `responsive.css`, `chapters.css`, `landing.css`,
`rules/app.js`, `rules/styles.css`, `skills/app.js`, `skills/styles.css`,
`skills-review/*.js`, `skills-review/*.css`, and the ten old `index.html`
files. `git rm`, one commit, reviewable.
**Keep**: `hands-on/**` (now under `public/`), `submitted-skills/**`,
`skill-reviews/**`, `docs/**`, `vote-service/**`.
3. **Publish** via the mechanism chosen in task 01.
4. **Verify on the real host** with a cache-buster:
```bash
for r in "" full-guide summary models agents skills rules skills-review \
hands-on/starter hands-on/rules; do
curl -sS -o /dev/null -w "%{http_code} $r\n" \
"https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/$r/?v=$(git rev-parse --short HEAD)"
done
```
All ten must be 200. A stale cached 200 looks identical to success — the
`?v=` is what distinguishes them.
5. **Update the docs**: `README.md`, `docs/operations-guide.md` (build + publish
path, and it must stop claiming `pages` is "the exact published source" if
that is no longer true), `GATES.md`, and `AGENTS.md` (stack is no longer
"migration in progress").
6. **Fast-forward `pages`** per the procedure in `docs/operations-guide.md`.
## Rollback
`pages` still holds the working vanilla site until you overwrite it. If cutover
fails, reset `pages` to its previous commit — the old site returns immediately.
Note the SHA before you start:
```bash
git rev-parse origin/pages # write it down
```
## Done when
- [ ] Ten routes 200 on the real host with a fresh cache-buster
- [ ] Vote widget works end-to-end from the published origin (CORS is
origin-sensitive — `ALLOWED_ORIGIN` must still match)
- [ ] Old files deleted; `npm run gate` green
- [ ] Docs match reality
- [ ] Previous `pages` SHA recorded for rollback