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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
- 150–250ms 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.
|
||||
@@ -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`);
|
||||
```
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user