Compare commits
105 Commits
pages
..
38b92bc66c
| Author | SHA1 | Date | |
|---|---|---|---|
| 38b92bc66c | |||
| db3ffdf6b0 | |||
| cac1115035 | |||
| a5d9630dd8 | |||
| f7985bfe2a | |||
| 98ab6db3de | |||
| 6d669d760a | |||
| 0b4f2dd403 | |||
| be2cf2d1c5 | |||
| 64b506aa32 | |||
| dc01460ee3 | |||
| 595006bcba | |||
| 24d0af4840 | |||
| 4b758c765d | |||
| 7f11b6e88e | |||
| fc063f606a | |||
| 421c84ff92 | |||
| d62c6bf958 | |||
| 99ccc8e249 | |||
| 966dfcb926 | |||
| 99d42f8fbc | |||
| 44f7fb8a01 | |||
| ea5f9356e7 | |||
| e8f6b8f488 | |||
| 7a1211ac5f | |||
| c6e6657086 | |||
| 9975757445 | |||
| 6ec1e31ec5 | |||
| 59bbff0ad9 | |||
| c19e77bcac | |||
| aa2653340a | |||
| 73d1b62989 | |||
| 47c50d43dd | |||
| 66c7c0b014 | |||
| 8388dd63fe | |||
| 9621ba44bf | |||
| d234f40134 | |||
| 0812a02219 | |||
| 3fdfbe3ae9 | |||
| 4305de9eb7 | |||
| 7d86c28c90 | |||
| 2a46cdc67d | |||
| 134bd37ec4 | |||
| 71e4775573 | |||
| 6119abfa32 | |||
| 2bd96d1f8c | |||
| d58e08b89e | |||
| 107e429fb9 | |||
| 34f6961264 | |||
| c00964132c | |||
| 1247c48ce4 | |||
| 4352745612 | |||
| 6bb01602b3 | |||
| f8c3394f3d | |||
| ba56f7a0c9 | |||
| fe25e053a9 | |||
| fa57cc156a | |||
| 723abeafb5 | |||
| da790de20d | |||
| 950dd3229c | |||
| d7820edb04 | |||
| ba460f5b8b | |||
| 9c013b056b | |||
| 29a5ca0035 | |||
| c159f4cdb8 | |||
| 23060cca74 | |||
| 233cc5d6e6 | |||
| 914a813b8f | |||
| c6b87b74ae | |||
| 82601e104e | |||
| 73ceae2aa8 | |||
| 3caa276573 | |||
| 61f6afd66c | |||
| e0361a3bda | |||
| 3b54b45427 | |||
| 0a60601272 | |||
| d96d61fa49 | |||
| b484302afd | |||
| db4ae19c0a | |||
| febb8914b4 | |||
| 4acdd1e571 | |||
| f241c5581a | |||
| 3cc5dd7652 | |||
| 48c31dc1b3 | |||
| 63da0a4727 | |||
| dcef196f25 | |||
| 38bac60c88 | |||
| a6107e4998 | |||
| 7896b634a8 | |||
| ed7f0f461f | |||
| 71021e60cf | |||
| 2e79aacecc | |||
| db86edbc21 | |||
| c9ec9e3393 | |||
| b554f87d51 | |||
| a45aa84318 | |||
| 8123965bf2 | |||
| eef1d9a86b | |||
| db2e969fc3 | |||
| 971b5ec7bc | |||
| f5631acb2c | |||
| 33df09f541 | |||
| 88eecb1c71 | |||
| 1cc9469d2d | |||
| aae4d42229 |
@@ -0,0 +1,66 @@
|
||||
# Orchestrator
|
||||
|
||||
Map of `.agents/`, and how to pick who does what.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.agents/
|
||||
ORCHESTRATOR.md you are here
|
||||
context/ how things are — read before deciding
|
||||
rules/ how things must be — binding
|
||||
checklists/ gates to run before you claim done
|
||||
skills/ procedures an agent loads on demand
|
||||
agents/ specialist agent definitions
|
||||
templates/ component and page starting points
|
||||
scripts/ deterministic helpers (prefer these over prose)
|
||||
```
|
||||
|
||||
`context/` describes reality, `rules/` constrain change. When they disagree,
|
||||
reality won — update the rule and say so.
|
||||
|
||||
## Specialists
|
||||
|
||||
Each agent has one responsibility, one set of rules, and its own worktree. Full
|
||||
definitions in [`agents/`](agents/).
|
||||
|
||||
| Agent | Owns | Loads skills |
|
||||
| ---------------------------------------------------------- | --------------------------------------------------- | -------------------------------------- |
|
||||
| [`astro-architect`](agents/astro-architect.md) | project scaffold, config, routing, layouts | `astro-page` |
|
||||
| [`design-system-keeper`](agents/design-system-keeper.md) | tokens, the single palette, type scale | `design-tokens` |
|
||||
| [`component-builder`](agents/component-builder.md) | one component per task, from templates | `astro-component`, `design-tokens` |
|
||||
| [`page-migrator`](agents/page-migrator.md) | one page per task, HTML → `.astro` | `astro-page`, `content-migration` |
|
||||
| [`motion-designer`](agents/motion-designer.md) | transitions, islands with animation | `motion` |
|
||||
| [`content-i18n-migrator`](agents/content-i18n-migrator.md) | strings out of `app.js` into content collections | `content-migration` |
|
||||
| [`verification-engineer`](agents/verification-engineer.md) | keeping `verify.mjs` meaningful across the refactor | `verify-contract`, `visual-regression` |
|
||||
| [`reviewer`](agents/reviewer.md) | merge gate; reads diffs, never writes features | all |
|
||||
|
||||
## Working agreement
|
||||
|
||||
1. **One task, one worktree, one agent.** See
|
||||
[`rules/git-worktrees.md`](rules/git-worktrees.md).
|
||||
2. **Read `context/` first.** Especially `design-system.md` and
|
||||
`verification.md`. Most wrong answers here come from assuming the CSS is
|
||||
already coherent.
|
||||
3. **Templates over invention.** `templates/components/` and `templates/pages/`
|
||||
exist so ten parallel agents produce one house style, not ten.
|
||||
4. **The gate is `pnpm run verify` plus the relevant checklist.** Green tests
|
||||
with deleted assertions is a failed task.
|
||||
5. **Report what you did not do.** Partial work with an honest boundary is
|
||||
useful; silent narrowing is not.
|
||||
|
||||
## Task flow
|
||||
|
||||
```
|
||||
plans/astro-refactor/task-NN.md ← the brief
|
||||
↓
|
||||
git worktree add ../af-task-NN ← isolation
|
||||
↓
|
||||
agent loads .agents/agents/<role>.md + its skills
|
||||
↓
|
||||
checklists/before-*.md ← self-gate
|
||||
↓
|
||||
pnpm run verify ← hard gate
|
||||
↓
|
||||
reviewer agent on the diff ← merge gate
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: astro-architect
|
||||
description:
|
||||
Owns the Astro scaffold — config, routing, layouts, build pipeline, and the
|
||||
publishing decision. Use for task 01 and any later change to astro.config.mjs,
|
||||
package.json, or the deploy path. Do not use for component or page work.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own the foundation. Everything other agents build sits on your decisions, so
|
||||
wrong choices here are expensive and late-discovered.
|
||||
|
||||
**Read first**: `.agents/context/architecture.md`,
|
||||
`.agents/context/publishing.md`, `.agents/rules/astro.md`. **Load skill**:
|
||||
`astro-page`.
|
||||
|
||||
## You own
|
||||
|
||||
`astro.config.mjs`, `package.json`, `tsconfig.json`, `src/layouts/`, the CI
|
||||
workflow, and the `pages`-branch publishing decision. You are the **only**
|
||||
writer of these. Other agents report problems with them; they do not edit.
|
||||
|
||||
## Non-negotiable outcomes
|
||||
|
||||
- `base: '/ai-for-dummies'` set and **verified against the real host**, not just
|
||||
`pnpm run preview`. Base-path bugs are the most likely production-only
|
||||
failure.
|
||||
- Every existing URL resolves identically, trailing slash included.
|
||||
- Zero JS by default. Astro ships none unless a component asks.
|
||||
- `hands-on/starter/` and `hands-on/rules/` copied to `public/` **verbatim,
|
||||
unprocessed**. They are lab fixtures; the exercise is that they are plain
|
||||
files.
|
||||
- No UI framework, no CSS framework, no runtime dependencies.
|
||||
|
||||
## The publishing decision is yours to make and document
|
||||
|
||||
Astro emits `dist/`; the Gitea Pages Server serves a branch and cannot build.
|
||||
Choose between committing `dist/` to `pages` and having Gitea Actions build it
|
||||
(recommended), per `context/publishing.md`. Then **rewrite
|
||||
`docs/operations-guide.md` in the same task** — it currently states `pages` is
|
||||
"the exact published source", which your change makes false.
|
||||
|
||||
Known trap: this Gitea's act-runner registration lives in an `emptyDir`, so a
|
||||
pod restart silently kills CI. Put that in the runbook.
|
||||
|
||||
## Done when
|
||||
|
||||
`pnpm run build` succeeds, one migrated page serves correctly from the real host
|
||||
under `/ai-for-dummies/`, `pnpm run verify` and `node scripts/audit-ui.mjs` are
|
||||
green, and the operations guide matches reality.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: component-builder
|
||||
description:
|
||||
Builds one Astro component per task from the project templates. Use for the
|
||||
component-extraction tasks (05-09). Do not use for page migration, token
|
||||
changes, or verify.mjs.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You build **one component per task**, from a template, in your own worktree.
|
||||
Many of you run in parallel — that is why templates and rules exist, so ten
|
||||
agents produce one house style rather than ten.
|
||||
|
||||
**Read first**: `.agents/rules/componentization.md`, `.agents/rules/astro.md`,
|
||||
`.agents/rules/theming.md`. **Load skills**: `astro-component`, `design-tokens`.
|
||||
|
||||
## Scope discipline
|
||||
|
||||
Your task names one component. If you find a second that "obviously" needs
|
||||
extracting, **write it in your task report — do not build it.** Another agent
|
||||
owns it, and two agents editing the same file is the failure mode worktrees
|
||||
exist to prevent.
|
||||
|
||||
You may not edit `tokens.css`, `verify.mjs`, `astro.config.mjs`, or
|
||||
`src/content/config.ts`. If you need a change there, report it.
|
||||
|
||||
## Rules that bite
|
||||
|
||||
- No raw hex, no px font sizes, no ad-hoc breakpoints. Tokens only.
|
||||
- **Never reshape CSS to slip past `check-tokens.mjs`** — e.g. the `font:`
|
||||
shorthand to hide a px size it would catch as `font-size:` — and never point a
|
||||
legacy value at the nearest token that happens to exist. Both are silent
|
||||
redesigns. Keep the true value and mark it
|
||||
`/* token-gap: <reason>; owner design-system-keeper */`, which waives the
|
||||
finding and queues it. You may not add tokens. See `.agents/rules/gates.md`.
|
||||
- No `client:*` unless genuinely interactive, with written justification.
|
||||
- Every ARIA attribute from the markup you replace survives. `verify.mjs`
|
||||
asserts several by name.
|
||||
- Keep the `gap:1px` over a coloured parent trick where the original used it.
|
||||
- Under ~120 lines of markup. More means two components.
|
||||
|
||||
## Done when
|
||||
|
||||
`.agents/checklists/before-component.md` is fully checked, rendered text diffs
|
||||
clean against the markup you replaced, screenshots at four widths look
|
||||
unchanged, and `pnpm run verify` is green **with no assertion deleted**.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: content-i18n-migrator
|
||||
description:
|
||||
Moves bilingual copy out of app.js and catalog.js into typed Astro content
|
||||
collections without altering a string. Use for tasks 03-04 and any later
|
||||
content relocation. Do not use for markup or styling.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own `src/content/` and `src/content/config.ts`, and you are their only
|
||||
writer. Your job is a lossless move, not an edit.
|
||||
|
||||
**Read first**: `.agents/rules/content-i18n.md`. **Load skill**:
|
||||
`content-migration`.
|
||||
|
||||
## What you are moving
|
||||
|
||||
~50 `{ en, pt }` keys from `app.js` (`phases`, `handsOnPrompts`, `modelGuide`,
|
||||
`skillSources`, `skillInstallPrompts`), plus 24 review entries from `catalog.js`
|
||||
and `submitted-catalog.js`.
|
||||
|
||||
These are hand-written translations with deliberate tone. **Copy them
|
||||
mechanically. Never retype.** Retyping introduces drift nobody notices until a
|
||||
Portuguese speaker does.
|
||||
|
||||
## Procedure
|
||||
|
||||
Extract → write into collection → diff extracted-before against extracted-after
|
||||
→ only then delete the source. If the diff is not empty, you changed content.
|
||||
Fix it before continuing.
|
||||
|
||||
Both locales required in the schema. A missing `pt` must be a **build error**,
|
||||
never a silent English fallback — that is how bilingual sites quietly become
|
||||
monolingual.
|
||||
|
||||
## The trap that will catch you
|
||||
|
||||
`skill-reviews/improved/**/SKILL.md` is generated from `catalog.js` by
|
||||
`scripts/build-skill-review.mjs` and the output is **committed**. Move
|
||||
`catalog.js` and the generator keeps running against nothing — silently. Either
|
||||
re-point it or replace it, and update `package.json`, `README.md`,
|
||||
`docs/operations-guide.md`, and the review desk footer, all of which reference
|
||||
it.
|
||||
|
||||
Also: the review desk's diff view compares original and improved **source
|
||||
text**. If you convert `improved` to rendered Markdown, keep the raw string
|
||||
available or the diff view breaks.
|
||||
|
||||
## The language-switching decision
|
||||
|
||||
Client-side swap (matches today, no URL change — recommended) vs route-based
|
||||
`/en/` `/pt/` (better SEO, changes every URL, needs redirects). Surface it, get
|
||||
a decision, record it. Either way `<html lang>` tracks the active language.
|
||||
|
||||
## Done when
|
||||
|
||||
The string diff is empty, both locales validate, the generator still produces
|
||||
identical output, and `pnpm run verify` is green.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: design-system-keeper
|
||||
description:
|
||||
Owns src/styles/tokens.css — the palette, type scale, and breakpoints. Use for
|
||||
task 02 and any later token change or check-tokens failure. Do not use for
|
||||
building components.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own the token layer and are its only writer. Your job is to make "maintain
|
||||
the same styles" true and verifiable.
|
||||
|
||||
**Read first**: `.agents/context/design-system.md` — it documents three drifting
|
||||
palettes and a broken `@font-face`. **Load skills**: `design-tokens`,
|
||||
`visual-regression`.
|
||||
|
||||
## The two decisions you must surface, not silently make
|
||||
|
||||
1. **Three palettes → one.** `--ink` exists as `#172f42`, `#122534`, `#173044`;
|
||||
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
||||
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
||||
visibly different — screenshot both and get a human decision.
|
||||
2. ~~**The fonts have never rendered.**~~ **Settled 2026-09-05 — do not
|
||||
reopen.** The malformed `@font-face` was escalated and the human chose the
|
||||
real fonts. Manrope and DM Mono are self-hosted in `public/fonts/`, wired
|
||||
through `public/fonts/fonts.css`, which `BaseLayout.astro` links and the
|
||||
legacy root `styles.css` `@import`s. **Do not delete these faces and do not
|
||||
replace the stacks with `Arial`/`ui-monospace`** — that instruction is
|
||||
obsolete. You may add `--font-sans` / `--font-mono` tokens pointing at them.
|
||||
|
||||
## You own
|
||||
|
||||
`src/styles/tokens.css`, `src/styles/base.css`, and
|
||||
`.agents/scripts/check-tokens.mjs`.
|
||||
|
||||
Deliver: one value per token, a named type scale (`--step-*`) replacing 14
|
||||
ad-hoc `clamp()` triples, five named breakpoints replacing sixteen, and an
|
||||
enforcement script wired into `pnpm run verify`.
|
||||
|
||||
## Preserve the house style
|
||||
|
||||
Flat colour blocks, `1px` hairlines, near-zero radius, tight negative tracking
|
||||
on display type, `Georgia, serif` emphasis spans, and the `gap:1px` over a
|
||||
coloured parent trick used for grid separators. That last one is deliberate —
|
||||
never convert it to `border`.
|
||||
|
||||
## Done when
|
||||
|
||||
Every token has exactly one value, `check-tokens.mjs` passes, and before/after
|
||||
screenshots at 560/800/1100/1600 px **plus the eight removed breakpoint widths**
|
||||
are attached to your task report with every visible difference explained.
|
||||
@@ -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.
|
||||
- 150–250ms 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.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: page-migrator
|
||||
description:
|
||||
Migrates one hand-written HTML page to an Astro route with identical URL,
|
||||
content, and JS budget. Use for the page-migration tasks (10-16). Do not use
|
||||
for component extraction or config changes.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You migrate **one page per task**. The bar is that a visitor cannot tell.
|
||||
|
||||
**Read first**: `.agents/context/architecture.md`,
|
||||
`.agents/context/verification.md`. **Load skills**: `astro-page`,
|
||||
`content-migration`, `visual-regression`.
|
||||
|
||||
## Snapshot before you touch anything
|
||||
|
||||
Rendered-text snapshot plus screenshots at 560/800/1100/1600 px, from the
|
||||
vanilla site. Without a before, you cannot prove an after. This is not optional
|
||||
and it is the step agents skip.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- **URL identical**, trailing slash included. Internal links go through
|
||||
`import.meta.env.BASE_URL`, never a hand-written absolute path.
|
||||
- **Query params survive** — the review desk uses `?author=`, `?skill=`,
|
||||
`?view=`, `?file=`, `?compare=`, `?render=` and they are shared externally.
|
||||
- **JS budget does not grow.** A page that shipped zero JS still ships zero.
|
||||
Seven of ten pages are in that category.
|
||||
- Every `data-*` hook, ARIA attribute, `<title>`, and `<meta>` preserved.
|
||||
- Reuse existing components before creating new ones.
|
||||
- Do **not** port `responsive.css` wholesale. Take what the page needs; prove
|
||||
the rest is dead and delete it.
|
||||
|
||||
## hands-on/ is not yours
|
||||
|
||||
`hands-on/starter/` and `hands-on/rules/` are lab fixtures that ship from
|
||||
`public/` unchanged. Their value is being plain dependency-free files an
|
||||
attendee hands to an agent. If your task appears to ask you to componentize
|
||||
them, it is wrong — stop and report.
|
||||
|
||||
## Done when
|
||||
|
||||
`.agents/checklists/before-page.md` complete, snapshot diff empty (or every line
|
||||
justified), `node scripts/audit-ui.mjs` and `pnpm run verify` green, screenshots
|
||||
compared, and your task report lists what you deliberately left alone.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: verification-engineer
|
||||
description:
|
||||
Keeps scripts/verify.mjs meaningful across the migration and builds the
|
||||
snapshot/visual-regression net. Use for tasks 18-19 and whenever a verify
|
||||
assertion needs re-pointing. The only role permitted to reduce coverage.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own `scripts/verify.mjs`, `scripts/audit-ui.mjs`, `.agents/snapshots/`, and
|
||||
the visual-regression tooling. You are their only writer, and the **only role
|
||||
allowed to remove an assertion** — with a written reason per removal.
|
||||
|
||||
**Read first**: `.agents/context/verification.md`. **Load skills**:
|
||||
`verify-contract`, `visual-regression`.
|
||||
|
||||
## Why the role exists
|
||||
|
||||
42 assertions pin this site's real content. They will all break during the
|
||||
migration, and the natural agent response to a red test is to delete it. That
|
||||
turns a content-loss bug into a green build. You are the check on that.
|
||||
|
||||
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease across the
|
||||
migration.
|
||||
|
||||
## Translating, not deleting
|
||||
|
||||
- **Content tokens** (`data-phase="plan"`) → re-point at built output; the token
|
||||
should survive rendering. If it does not, a component dropped content.
|
||||
- **Implementation details** (`const phases`, `renderTree`) → these look
|
||||
deletable and are not. They pin a feature. Replace with an output-level
|
||||
assertion of the same feature.
|
||||
- **Asset versions** (`app.js?v=…`) → assert the built HTML references a hashed
|
||||
asset.
|
||||
|
||||
## Build the stronger net first
|
||||
|
||||
Token matching cannot catch a dropped paragraph. Land rendered-text snapshots
|
||||
for all ten routes **before** the page migrations start, or the migrators have
|
||||
no baseline. This is early, blocking work.
|
||||
|
||||
## Fix the audit gap
|
||||
|
||||
`audit-ui.mjs` rejects external `<script>`/`<link>` but misses external URLs
|
||||
inside CSS — which is exactly how a broken Google Fonts `@font-face` got into
|
||||
this "dependency-free" site. Add `@import`, `src: url(https:…)`, and
|
||||
`url(https:…)` detection.
|
||||
|
||||
## Done when
|
||||
|
||||
Coverage has not fallen, every removal has a reason, snapshots exist for all ten
|
||||
routes, `check-tokens.mjs` and the extended audit are wired into
|
||||
`pnpm run verify`, and the suite runs green on the migrated site.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Checklist: before you call a component done
|
||||
|
||||
- [ ] It appears (or will appear) in **three** places, or has a name a person
|
||||
says out loud
|
||||
- [ ] Lives in the right folder: `primitives/`, `blocks/`, or `islands/`
|
||||
- [ ] Typed `interface Props`; every field intentional; no `any`
|
||||
- [ ] **No raw hex, px font sizes, or ad-hoc breakpoints** — tokens only
|
||||
- [ ] Ships zero JS, or has a `client:*` directive with a written justification
|
||||
- [ ] Markup under ~120 lines
|
||||
- [ ] Native elements: `<button>` for actions, `<a>` for navigation
|
||||
- [ ] Keyboard: tab to it, operate with Enter/Space, focus ring visible
|
||||
- [ ] Interactive state exposed via ARIA (`aria-pressed`, `aria-current`), not
|
||||
just colour
|
||||
- [ ] Any motion respects `prefers-reduced-motion` and animates only
|
||||
`transform`/`opacity`
|
||||
- [ ] Renders correctly at 560 / 800 / 1100 / 1600 px
|
||||
- [ ] Both `en` and `pt` strings present; no hard-coded copy
|
||||
- [ ] `pnpm run verify` green with no assertion deleted
|
||||
@@ -0,0 +1,15 @@
|
||||
# Checklist: before you merge a task branch
|
||||
|
||||
- [ ] `origin/main` **merged in** (never rebased), conflicts resolved in the
|
||||
worktree
|
||||
- [ ] `pnpm run verify` and `node scripts/audit-ui.mjs` both green
|
||||
- [ ] **Assertion count in `verify.mjs` did not fall**
|
||||
(`grep -c 'throw new Error' scripts/verify.mjs`)
|
||||
- [ ] Only files in your task's scope changed — `git diff --stat origin/main`
|
||||
matches the brief
|
||||
- [ ] No contended file edited unless you own it (see `rules/git-worktrees.md`)
|
||||
- [ ] No `.serena/`, `__pycache__/`, `dist/`, or scratch files staged
|
||||
- [ ] Commit message: what, why, and what you deliberately did not do
|
||||
- [ ] Task report written: verified behaviours, known gaps, follow-ups
|
||||
- [ ] `reviewer` agent has read the diff
|
||||
- [ ] Worktree and branch cleaned up after merge
|
||||
@@ -0,0 +1,20 @@
|
||||
# Checklist: before you call a page migrated
|
||||
|
||||
- [ ] URL identical to the old one, **trailing slash included**
|
||||
- [ ] Any query params the old page honoured still work (`?author=`, `?skill=`,
|
||||
`?view=`, `?file=`, `?compare=`, `?render=`)
|
||||
- [ ] Rendered text content diffs clean against the pre-migration snapshot — or
|
||||
every difference is listed and justified
|
||||
- [ ] Same number of JS bytes or fewer; a previously-zero-JS page still ships
|
||||
zero
|
||||
- [ ] `<title>`, `<meta name="description">`, and `<meta name="viewport">`
|
||||
preserved
|
||||
- [ ] One `<h1>`; heading levels do not skip
|
||||
- [ ] Every ARIA attribute from the original survived (`verify.mjs` asserts
|
||||
several)
|
||||
- [ ] No external `<script>`, `<link>`, `@import`, or `url()` —
|
||||
`node scripts/audit-ui.mjs` green
|
||||
- [ ] Internal links go through `BASE_URL`, not hand-written absolute paths
|
||||
- [ ] Screenshots at 560 / 800 / 1100 / 1600 px compared against the old page
|
||||
- [ ] `<html lang>` correct and still switching with the language toggle
|
||||
- [ ] `pnpm run verify` green with no assertion deleted
|
||||
@@ -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.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Context: full-guide language switching
|
||||
|
||||
## Decision
|
||||
|
||||
The Astro full guide keeps the current client-side language switch on its
|
||||
existing `/full-guide/` URL. It server-renders both locale variants and the
|
||||
language-toggle island shows the selected variant after `client:idle` hydration.
|
||||
|
||||
This deliberately preserves the current no-URL-change contract, including links
|
||||
shared without a locale segment, and avoids a route/redirect and publishing
|
||||
change. The cost is duplicated localized HTML and both locales in the response.
|
||||
That is acceptable for this small guide and avoids sending duplicated string
|
||||
data through every interactive island.
|
||||
|
||||
## Markup and island contract for task 15d
|
||||
|
||||
- Render each static localized fragment twice. Put `data-language-content="en"`
|
||||
or `data-language-content="pt"` on its outer element. English is visible in
|
||||
server HTML; the toggle uses the native `hidden` attribute for the inactive
|
||||
locale.
|
||||
- Add `<LanguageToggle />` to the guide top bar. Astro's `client:idle` directive
|
||||
is only valid for framework components; this `.astro` island defers its
|
||||
browser setup with `requestIdleCallback` (and a timeout fallback) instead. Do
|
||||
not hydrate the page or use `client:load`; the control is deliberately
|
||||
idle-priority.
|
||||
- The island owns the `ai-for-dummies-language` localStorage key. Every read and
|
||||
write remains inside `try`/`catch`, because previews may disable storage.
|
||||
- On each selection the island sets `<html lang>` to `en` or `pt-BR`, updates
|
||||
its `[data-lang]` buttons' `.active` class and `aria-pressed` state, updates
|
||||
`[data-language-content]`, then dispatches `ai-for-dummies:languagechange` on
|
||||
`window`. The event detail is `{ language: 'en' | 'pt' }`.
|
||||
- The guide selector island (15a) must read `document.documentElement.lang` when
|
||||
it hydrates and listen for that event. On receipt it must re-render the
|
||||
currently active phase and all active selector panels from their collection
|
||||
data. This preserves today’s `applyLanguage` behaviour without coupling the
|
||||
toggle to page selectors.
|
||||
|
||||
This is a page-local contract: the existing `/rules/` toggle continues using its
|
||||
own `rules-language` key and must not be changed as part of full-guide
|
||||
migration.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Context: the design system (as it actually is)
|
||||
|
||||
Read this before touching CSS. Everything here was extracted from the current
|
||||
files, not assumed.
|
||||
|
||||
## There is no single palette. There are three.
|
||||
|
||||
The same semantic names carry different values depending on which stylesheet
|
||||
loaded them:
|
||||
|
||||
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
||||
| ---------- | -------------------------------- | ------------------------------------------ | ----------------------- |
|
||||
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
||||
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
||||
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
||||
| `--line` | `#d8dee2` | `#d0d5d2` | `#d5dde1` |
|
||||
| `--blue` | `#527f9f` | `#215675` | `#5683a1` |
|
||||
| `--gold` | `#efc76b` | `#ebbf58` | `#efc86d` |
|
||||
| `--accent` | `#7c78a8` | — | — |
|
||||
| `--deep` | `#102536` | — | — |
|
||||
| `--red` | — | `#a7483f` (chapters only) | — |
|
||||
| `--violet` | — | `#6b668f` (review desk only) | — |
|
||||
|
||||
Most deltas are a few units per channel — drift, not intent. `--blue` is the
|
||||
exception: `#527f9f` vs `#215675` is a visible difference and may be deliberate.
|
||||
|
||||
**Decision required before any component work** (task 02). Options:
|
||||
|
||||
- **Canonicalize** to one palette. Recommended. The sub-perceptual deltas
|
||||
collapse; only `--blue` needs a human's eye on a before/after screenshot.
|
||||
- **Keep three named surfaces** (`--surface-guide`, `--surface-chapter`,
|
||||
`--surface-lab`) if the drift turns out to be intentional per section.
|
||||
|
||||
Do not "just pick one" silently in the middle of another task. This is its own
|
||||
reviewed change with visual diffs attached.
|
||||
|
||||
## The typography — fixed 2026-09-05
|
||||
|
||||
`styles.css` line 1 used to read:
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: Manrope;
|
||||
src: url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap');
|
||||
}
|
||||
```
|
||||
|
||||
`src:` in an `@font-face` must point at a font binary. That URL returns a CSS
|
||||
stylesheet, so no browser could load a face from it. For the whole life of the
|
||||
site, every `font-family:Manrope,Arial,sans-serif` rendered as **Arial** and
|
||||
every `font:… 'DM Mono',monospace` rendered as the **generic monospace** face —
|
||||
`'DM Mono'` was never declared as a family at all.
|
||||
|
||||
**This was escalated and the human chose the real fonts.** Manrope and DM Mono
|
||||
are now self-hosted in `public/fonts/`, latin and latin-ext subsets only, under
|
||||
the SIL Open Font License. One `fonts.css` serves both trees: Astro links it
|
||||
from `BaseLayout.astro`, the legacy root `styles.css` `@import`s it. Self-hosted
|
||||
rather than linked from Google because `scripts/audit-ui.mjs` rejects any
|
||||
external `<link>`/`<script>`, and because the site is presented in workshop
|
||||
rooms with unreliable networks.
|
||||
|
||||
**This changed how every page renders**, deliberately. It is the one sanctioned
|
||||
visual change in the migration. Screenshots taken before 2026-09-05 show Arial
|
||||
and are no longer a valid baseline.
|
||||
|
||||
`Georgia, serif` for emphasis (`h1 em`, `.hero em`) is untouched and still real.
|
||||
|
||||
## Type scale
|
||||
|
||||
`Georgia, serif` is used deliberately for emphasis (`h1 em`, `.hero em`) and is
|
||||
real — it is a system font, so it does render. Keep it.
|
||||
|
||||
Sizes are all `clamp()`, roughly:
|
||||
|
||||
| Role | Value |
|
||||
| --------------- | --------------------------------------------------------- |
|
||||
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
||||
| Section `h2` | `clamp(36px,5vw,65px)` |
|
||||
| Sub-head | `clamp(24px,3vw,38px)` |
|
||||
| Pull-quote | `clamp(22px,3vw,36px)` |
|
||||
| Body | `15px/1.6` … `18px` |
|
||||
| Eyebrow / label | `10–11px` 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`.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Context: publishing, and what a build step changes
|
||||
|
||||
## How it works today
|
||||
|
||||
The Gitea Pages Server serves the **`pages` branch tree directly**. There is no
|
||||
build. `main` and `pages` end up with byte-identical trees; `pages` exists only
|
||||
because the Pages Server publishes a branch, not a directory.
|
||||
|
||||
Live at `https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/`.
|
||||
|
||||
Full procedure, including the fact that `merge --ff-only main` fails (the
|
||||
histories diverged), is in
|
||||
[`../../docs/operations-guide.md`](../../docs/operations-guide.md).
|
||||
|
||||
## What Astro changes
|
||||
|
||||
Astro emits `dist/`. The Pages Server cannot run a build, so **something has to
|
||||
put built output on `pages`**. Pick one, deliberately, in task 01:
|
||||
|
||||
| Option | How | Cost |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **A. Build locally, commit `dist/` to `pages`** | `pnpm run build`, copy `dist/*` into the `pages` worktree, commit | `pages` stops being "the exact source". Diffs become unreadable. Publishing depends on one workstation. Simple, no new infra. |
|
||||
| **B. Gitea Actions builds and pushes `pages`** | workflow on `main` → `pnpm install --frozen-lockfile && pnpm run build` → force-push `dist/` to `pages` | `pages` becomes a machine-owned branch (force-push is fine _because_ nothing else writes it). Needs the act-runner to be healthy. **Recommended.** |
|
||||
| **C. Serve from a container instead** | drop Pages Server for an nginx pod behind the existing ingress | most control, most infra, changes the URL story |
|
||||
|
||||
**Recommended: B**, with A as the documented manual fallback for when the runner
|
||||
is down. Note the known failure mode: this Gitea's act-runner registration lives
|
||||
in an `emptyDir`, so a pod restart silently kills CI until re-registered. The
|
||||
runbook must say "if the site stopped updating, check the runner first".
|
||||
|
||||
Whichever you pick, `docs/operations-guide.md` must be rewritten in the same
|
||||
task — it currently promises `pages` is "the exact published source", and that
|
||||
stops being true under A and B.
|
||||
|
||||
## Base path
|
||||
|
||||
The site is served from a **subdirectory**: `/ai-for-dummies/`. Astro needs
|
||||
`base: '/ai-for-dummies'` in `astro.config.mjs`, and every internal link must go
|
||||
through `import.meta.env.BASE_URL` or Astro's `<a href={...}>` helpers rather
|
||||
than a hand-written absolute `/models/`.
|
||||
|
||||
This is the single most likely source of "works locally, 404s in production" in
|
||||
this migration. Verify it on the real host, not just `pnpm run preview`.
|
||||
|
||||
## Verification before you call it published
|
||||
|
||||
```bash
|
||||
curl -sS -o /dev/null -w '%{http_code}\n' \
|
||||
"https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/?v=$(git rev-parse --short HEAD)"
|
||||
```
|
||||
|
||||
The `?v=` cache-buster matters: the Pages Server caches, and a stale 200 looks
|
||||
exactly like a successful deploy. Check one nested route
|
||||
(`/ai-for-dummies/skills-review/`) and one static asset too — the base-path bug
|
||||
shows up on assets first.
|
||||
@@ -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 `pnpm run verify`, sees red,
|
||||
and "fixes" it by deleting the assertion. The suite goes green and the site
|
||||
loses a section. **Deleting an assertion is a change that requires review, the
|
||||
same as deleting a feature.**
|
||||
|
||||
## How the contract must evolve
|
||||
|
||||
Three kinds of assertion, three different fates:
|
||||
|
||||
| Kind | Example | Fate |
|
||||
| ------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Content presence** | `'data-phase="plan"'` in `full-guide/index.html` | Re-point at built output (`dist/`) — the token should survive rendering. If it does not, the component dropped content. |
|
||||
| **Implementation detail** | `'const phases'`, `'renderTree'` in `app.js` | Obsolete. Replace with an assertion about _behaviour or output_, never delete outright. |
|
||||
| **Cache-busting version** | `'app.js?v=20260904-vote-widget'` | Obsolete — Astro hashes assets. Replace with "the built HTML references a hashed asset". |
|
||||
|
||||
**Rule: the assertion count must not fall.** Every removed token is replaced by
|
||||
one that pins the same user-visible fact against the new architecture. The
|
||||
verification engineer owns this and is the only role allowed to reduce coverage,
|
||||
with a written reason per removal.
|
||||
|
||||
## The stronger check to add
|
||||
|
||||
Token-matching is brittle. During the migration, add a **rendered-output diff**:
|
||||
snapshot the current site's DOM text content per route, then assert the Astro
|
||||
build produces the same text. That catches dropped paragraphs the way token
|
||||
matching cannot.
|
||||
|
||||
```bash
|
||||
# before migrating a page, from the vanilla site:
|
||||
node .agents/scripts/snapshot-route.mjs /models/ > .agents/snapshots/models.txt
|
||||
# after: same script against dist/, diff must be empty (or reviewed)
|
||||
```
|
||||
|
||||
See [`../skills/verify-contract/SKILL.md`](../skills/verify-contract/SKILL.md).
|
||||
|
||||
## Also in the suite
|
||||
|
||||
`scripts/audit-ui.mjs` asserts every page has a viewport meta and **no external
|
||||
`<script>`/`<link>`**. Keep it and extend it: it currently misses external URLs
|
||||
inside CSS (`@font-face src`, `@import`, `url()`), which is exactly how the
|
||||
broken Google Fonts request in `styles.css:1` got in.
|
||||
@@ -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.
|
||||
@@ -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: **150–250ms** 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,123 @@
|
||||
# Rule: quality gates
|
||||
|
||||
Three tiers. Each is scoped so that **many agents committing in parallel
|
||||
worktrees stay fast** — the whole point is that a gate you are tempted to skip
|
||||
is not a gate.
|
||||
|
||||
| Tier | Hook | Scope | Budget | Runs |
|
||||
| ---- | ------------------ | ------------------------------------- | ------- | -------------------- |
|
||||
| 1 | `pre-commit` | **staged files only** (lint-staged) | < 3s | every commit |
|
||||
| 2 | `pre-push` | whole project: build + verify + audit | < 90s | every push |
|
||||
| 3 | CI (Gitea Actions) | tier 2 + lint, on a clean install | minutes | every push to `main` |
|
||||
|
||||
Tier 1 must stay under a few seconds. If it creeps, move the check to tier 2. An
|
||||
agent that waits 40s per commit will start passing `--no-verify`, and then you
|
||||
have no gate at all.
|
||||
|
||||
## Tier 1 — pre-commit (lint-staged)
|
||||
|
||||
Formats and lints **only what you staged**. Scoped by construction, so ten
|
||||
parallel worktrees do ten small jobs, not ten full-project sweeps.
|
||||
|
||||
- Prettier + ESLint on `*.{js,mjs,ts,astro}`
|
||||
- Prettier + Stylelint on `*.css`
|
||||
- `check-tokens.mjs` on changed `.astro`/`.css` — catches raw hex before it
|
||||
lands
|
||||
- Prettier on `*.{json,md}`
|
||||
|
||||
## Tier 2 — pre-push
|
||||
|
||||
The real gate:
|
||||
|
||||
```
|
||||
astro check types
|
||||
astro build it compiles
|
||||
verify.mjs 42 content assertions — count must not fall
|
||||
audit-ui.mjs no external runtime dependencies
|
||||
check-tokens.mjs full sweep
|
||||
```
|
||||
|
||||
## Tier 3 — CI
|
||||
|
||||
Tier 2 plus `pnpm run lint`, run against a clean
|
||||
`pnpm install --frozen-lockfile` — which is the part a local worktree cannot
|
||||
prove.
|
||||
|
||||
Screenshot comparison against `.agents/snapshots/` is **not** wired in yet.
|
||||
`visual-regression.mjs` can only capture baselines, not diff them, so running it
|
||||
in CI would overwrite the baselines and pass unconditionally. It also needs
|
||||
Playwright, which is not a project dependency. Add the step back when the script
|
||||
grows a compare mode.
|
||||
|
||||
## Bypassing
|
||||
|
||||
`--no-verify` is allowed exactly once: a work-in-progress commit **on your own
|
||||
task branch that you will amend or squash away**. It is never allowed on a
|
||||
commit you intend to merge, and the pre-push gate has no bypass.
|
||||
|
||||
If a gate is wrong, fix the gate in its own commit. Do not route around it.
|
||||
|
||||
## Never restructure code to slip past a checker
|
||||
|
||||
A checker is a proxy for a rule. Passing the proxy while breaking the rule is
|
||||
worse than failing, because failure is visible and this is not.
|
||||
|
||||
`check-tokens.mjs` matches `font-size: Npx`. Writing the same value as the
|
||||
`font:` shorthand passes it. Task 07 did exactly that, in **two** components,
|
||||
with a comment saying so. Both hardcoded values survived into a "green" branch.
|
||||
|
||||
### Do not substitute a near-miss token either
|
||||
|
||||
The second way to break this is subtler, and both tasks 10 and 11 did it: keep
|
||||
the gate happy by pointing a legacy value at the closest token that already
|
||||
exists. `#e5eeeb` became `var(--paper)`. Diff-**added** green became
|
||||
`var(--accent)` — purple. `12px` and `14px` both became `var(--step-1)`, 15px.
|
||||
|
||||
That is a silent redesign, and it is _worse_ than leaving the raw value in,
|
||||
because a raw hex is at least honest about being unresolved.
|
||||
|
||||
### What to do instead: mark the gap
|
||||
|
||||
`tokens.css` has one owner (`design-system-keeper`) so that "add a token" is a
|
||||
decision, not a side effect. You may not add one. You **can** keep the true
|
||||
value and stay green — mark it:
|
||||
|
||||
```css
|
||||
/* token-gap: no --step-* covers 12px; owner design-system-keeper */
|
||||
font-size: 12px;
|
||||
```
|
||||
|
||||
The marker waives that one finding. It needs a real reason after the colon; a
|
||||
bare `token-gap:` is rejected. Every marked value is listed on each run, so the
|
||||
debt stays visible rather than disappearing.
|
||||
|
||||
Write it in your task report as well: selector, legacy value, owning file.
|
||||
Marking a gap is not resolving it — it keeps the site truthful until whoever
|
||||
owns the token layer decides.
|
||||
|
||||
## Parallelism
|
||||
|
||||
- Hooks are **per-worktree**. Git's `index.lock` is per-worktree, so parallel
|
||||
commits do not contend.
|
||||
- The heavy tier-2 gate takes a **lock** (`.git/af-gate.lock`, shared across
|
||||
worktrees) so ten agents pushing at once do not run ten concurrent builds and
|
||||
thrash the machine. Waiters queue; they do not fail.
|
||||
- `pnpm install --frozen-lockfile` in a fresh worktree is cheap: pnpm hardlinks
|
||||
from the shared content-addressable store, so a second worktree costs seconds
|
||||
and almost no disk instead of another 225 MB. No `--prefer-offline` needed.
|
||||
|
||||
## The silent-failure mode you must know about
|
||||
|
||||
Husky sets `core.hooksPath` to `.husky/_`, and **`.husky/_` is generated by
|
||||
`pnpm install`, not committed**. A fresh `git worktree add` therefore has hooks
|
||||
configured but the directory missing — so **hooks silently do not run**. Every
|
||||
commit passes. Nothing is checked.
|
||||
|
||||
`.agents/scripts/worktree.sh start` runs the install and then verifies. Check
|
||||
manually any time you did not use it:
|
||||
|
||||
```bash
|
||||
.agents/scripts/verify-hooks.sh
|
||||
```
|
||||
|
||||
Run this first in any worktree you did not create with the script.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Rule: git worktrees
|
||||
|
||||
This project teaches worktrees. It should use them properly.
|
||||
|
||||
## One task, one worktree, one agent
|
||||
|
||||
```bash
|
||||
# from the main checkout
|
||||
git worktree add ../af-task-07 -b refactor/task-07-route-cards
|
||||
cd ../af-task-07
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
Naming: directory `../af-task-NN`, branch `refactor/task-NN-<slug>`. Both
|
||||
derived from the task file so the mapping is never ambiguous.
|
||||
|
||||
## Why isolation matters here
|
||||
|
||||
The migration runs many agents in parallel over the same small set of files
|
||||
(`tokens.css`, `verify.mjs`, `astro.config.mjs` are contended). Worktrees give
|
||||
each agent its own working directory over one object store — cheap, and no agent
|
||||
can see another's half-finished state.
|
||||
|
||||
The failure mode without them: two agents both "fix" `verify.mjs`, and the
|
||||
second overwrites the first's assertions.
|
||||
|
||||
## Contended files
|
||||
|
||||
These are touched by many tasks. Whoever owns them per the plan is the **only**
|
||||
writer; everyone else opens an issue in their task report instead of editing:
|
||||
|
||||
| File | Owner |
|
||||
| ---------------------------------- | ----------------------- |
|
||||
| `src/styles/tokens.css` | `design-system-keeper` |
|
||||
| `scripts/verify.mjs` | `verification-engineer` |
|
||||
| `astro.config.mjs`, `package.json` | `astro-architect` |
|
||||
| `src/content/config.ts` | `content-i18n-migrator` |
|
||||
|
||||
## Never rebase a task branch
|
||||
|
||||
Not onto `main`, not onto another task branch, not "just to tidy up". A rebase
|
||||
rewrites every commit with a new SHA, so a branch that others branched from — or
|
||||
that has already been merged — turns into a duplicate history that no longer
|
||||
shares an ancestor with the original. Task 03 did this and flattened task 01's
|
||||
merge into four look-alike commits; it is the same divergent-history trap that
|
||||
broke the `pages` branch.
|
||||
|
||||
To pick up new work from `main`, **merge it in**: `git merge origin/main`. The
|
||||
extra merge commit is the price of a history that stays true, and it is cheap.
|
||||
|
||||
## Before you start
|
||||
|
||||
1. `git fetch origin` — then branch from the ref your task file names as its
|
||||
base. Do not assume that base is `main`.
|
||||
2. Read your task file end to end before writing anything.
|
||||
3. Confirm your task's dependencies are merged. Task files list them.
|
||||
|
||||
## Before you finish
|
||||
|
||||
1. `pnpm run verify` green — without deleting assertions.
|
||||
2. The relevant checklist in [`../checklists/`](../checklists/) complete.
|
||||
3. `git fetch origin && git merge origin/main`, resolve conflicts in your
|
||||
worktree. **Merge — never rebase.**
|
||||
4. Task report: what changed, what you verified, **what you did not do**.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
cd - # back to the main checkout
|
||||
git worktree remove ../af-task-07
|
||||
git branch -d refactor/task-07-route-cards
|
||||
```
|
||||
|
||||
Stale worktrees hold locks and confuse the next agent. `git worktree list`
|
||||
should be short.
|
||||
|
||||
## Never
|
||||
|
||||
- Never work directly on `main`.
|
||||
- Never force-push a shared branch. The `pages` branch is the sole exception,
|
||||
and only if CI owns it (see
|
||||
[`../context/publishing.md`](../context/publishing.md)).
|
||||
- Never `git add -A` from the repository root. This repo has untracked local
|
||||
scratch (`.serena/`, `scripts/inspect.py`) that must not be swept into a
|
||||
commit.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Rule: theming
|
||||
|
||||
Binding for every colour, font size, spacing value, and breakpoint.
|
||||
|
||||
**Read [`../context/design-system.md`](../context/design-system.md) first.** The
|
||||
current CSS has three drifting palettes and a broken `@font-face`. This rule
|
||||
describes the target; that file describes what you are migrating from.
|
||||
|
||||
## One token layer
|
||||
|
||||
Every value comes from `src/styles/tokens.css`. If a component needs a value
|
||||
that is not a token, either it is a genuine one-off (justify it in a comment) or
|
||||
the token layer is missing something (add it there, not inline).
|
||||
|
||||
```css
|
||||
/* forbidden */
|
||||
color: #172f42;
|
||||
color: rgba(23, 47, 66, 0.6);
|
||||
|
||||
/* required */
|
||||
color: var(--ink);
|
||||
color: var(--muted);
|
||||
```
|
||||
|
||||
No raw hex outside `tokens.css`. `.agents/scripts/check-tokens.mjs` enforces it;
|
||||
wire it into `pnpm run verify`.
|
||||
|
||||
## Semantic names, not literal ones
|
||||
|
||||
`--ink`, `--paper`, `--muted`, `--line`, `--accent`, `--gold`, `--blue`,
|
||||
`--deep` are the existing vocabulary. Keep it — it is already semantic and the
|
||||
team reads it fluently. Do not rename to `--color-neutral-900`.
|
||||
|
||||
If a genuine second surface is needed, extend semantically (`--surface-lab`,
|
||||
`--ink-inverse`), never numerically.
|
||||
|
||||
## Type scale
|
||||
|
||||
Replace the 14 ad-hoc `clamp()` triples with named steps:
|
||||
|
||||
```css
|
||||
--step-display: clamp(56px, 9vw, 126px); /* h1 */
|
||||
--step-6: clamp(36px, 5vw, 65px); /* section h2 */
|
||||
--step-5: clamp(24px, 3vw, 38px); /* sub-head */
|
||||
--step-4: clamp(22px, 3vw, 36px); /* pull-quote */
|
||||
--step-1: 15px; /* body */
|
||||
--step-0: 11px; /* eyebrow / label */
|
||||
```
|
||||
|
||||
The eyebrow treatment (`10–11px` 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.
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
// Fails when a raw colour, px font-size, or ad-hoc breakpoint appears outside
|
||||
// the token layer. A rule nobody checks is a suggestion — wire this into
|
||||
// `pnpm run verify`.
|
||||
//
|
||||
// ESCAPE HATCH — `token-gap:`. Some legacy values have no token yet, and only
|
||||
// `design-system-keeper` may add one. Without an escape, an agent told both
|
||||
// "keep the site identical" and "get the gate green" has to break one of them,
|
||||
// and tasks 10 and 11 both broke the first: `#e5eeeb` became `var(--paper)`,
|
||||
// diff-added green became `var(--accent)` purple. Substituting a near-miss
|
||||
// token is a silent redesign; it is worse than a raw value, because the raw
|
||||
// value is at least honest about what it is.
|
||||
//
|
||||
// So: mark the line, keep the true value, stay green.
|
||||
//
|
||||
// /* token-gap: no --step-* covers 12px; owner design-system-keeper */
|
||||
// font-size: 12px;
|
||||
//
|
||||
// Marked values are counted and listed on every run — they are a visible debt
|
||||
// queue, not a way to make the finding disappear. The marker needs a reason;
|
||||
// a bare `token-gap:` does not count.
|
||||
//
|
||||
// Usage: node .agents/scripts/check-tokens.mjs [srcDir]
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join, extname } from 'node:path';
|
||||
|
||||
// lint-staged appends staged file paths; a bare run sweeps `src`.
|
||||
const ARGS = process.argv.slice(2);
|
||||
const TOKEN_FILES = ['tokens.css', 'base.css'];
|
||||
const ALLOWED_BREAKPOINTS = ['560px', '800px', '1100px', '1600px', '2200px'];
|
||||
|
||||
const walk = (dir) =>
|
||||
readdirSync(dir).flatMap((name) => {
|
||||
const path = join(dir, name);
|
||||
return statSync(path).isDirectory() ? walk(path) : [path];
|
||||
});
|
||||
|
||||
const targets = ARGS.length
|
||||
? ARGS.flatMap((arg) => (statSync(arg).isDirectory() ? walk(arg) : [arg]))
|
||||
: walk('src');
|
||||
|
||||
const findings = [];
|
||||
const gaps = [];
|
||||
|
||||
// A finding is waived when its own line, or the line above it, carries a
|
||||
// `token-gap:` marker with a reason after the colon.
|
||||
const MARKER = /token-gap:([^\n]*)/;
|
||||
// The reason is what is left after the marker once the comment terminator and
|
||||
// punctuation are stripped. `/* token-gap: */` is not a reason.
|
||||
const reason = (line) => {
|
||||
const found = MARKER.exec(line ?? '');
|
||||
if (!found) return null;
|
||||
const text = found[1]
|
||||
.replace(/\*\/\s*$/, '')
|
||||
.replace(/[\s*/]+$/, '')
|
||||
.trim();
|
||||
return /[a-z0-9]/i.test(text) ? [null, text] : null;
|
||||
};
|
||||
const waiver = (lines, index) =>
|
||||
reason(lines[index]) || (index > 0 ? reason(lines[index - 1]) : null);
|
||||
|
||||
for (const path of targets) {
|
||||
if (!['.astro', '.css'].includes(extname(path))) continue;
|
||||
if (TOKEN_FILES.some((allowed) => path.endsWith(allowed))) continue;
|
||||
|
||||
const lines = readFileSync(path, 'utf8').split('\n');
|
||||
lines.forEach((line, index) => {
|
||||
const at = `${path}:${index + 1}`;
|
||||
const waived = waiver(lines, index);
|
||||
const record = (finding) => {
|
||||
if (waived) gaps.push(`${at}: ${finding.slice(at.length + 2)} [${waived[1]}]`);
|
||||
else findings.push(finding);
|
||||
};
|
||||
|
||||
// Raw hex — the drifted-palette failure mode this whole layer exists to stop.
|
||||
const hex = line.match(/#[0-9a-fA-F]{3,8}\b/g);
|
||||
if (hex) record(`${at}: raw hex ${hex.join(', ')} — use a token from tokens.css`);
|
||||
|
||||
// rgb()/hsl() literals are the same problem wearing a different hat.
|
||||
if (/\b(rgba?|hsla?)\(\s*\d/.test(line)) record(`${at}: raw colour function — use a token`);
|
||||
|
||||
// Hard-coded font sizes bypass the type scale.
|
||||
const fontSize = line.match(/font-size:\s*\d+(\.\d+)?px/);
|
||||
if (fontSize) record(`${at}: hard-coded ${fontSize[0]} — use var(--step-*)`);
|
||||
|
||||
// Ad-hoc breakpoints are how sixteen of them accumulated last time.
|
||||
const media = line.match(/@media[^{]*?\(\s*(?:max|min)-width:\s*(\d+px)/);
|
||||
if (media && !ALLOWED_BREAKPOINTS.includes(media[1]))
|
||||
record(
|
||||
`${at}: breakpoint ${media[1]} is not a named one (${ALLOWED_BREAKPOINTS.join(', ')})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (gaps.length) {
|
||||
console.log(`token check: ${gaps.length} marked token-gap(s) awaiting design-system-keeper:\n`);
|
||||
gaps.forEach((gap) => console.log(` ${gap}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (findings.length) {
|
||||
console.error(`token check failed — ${findings.length} finding(s):\n`);
|
||||
findings.forEach((finding) => console.error(` ${finding}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
if (existsSync('dist')) {
|
||||
const builtCss = walk('dist').filter((p) => p.endsWith('.css'));
|
||||
const tokensBuilt = builtCss.find((p) => /[\\/]tokens\.[^\\/]+\.css$/.test(p));
|
||||
|
||||
if (!tokensBuilt) {
|
||||
console.error('token check failed — tokens.css was not built into dist/');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tokensContent = readFileSync(tokensBuilt, 'utf8');
|
||||
if (!tokensContent.includes('#527f9f')) {
|
||||
console.error(
|
||||
'token check failed — built tokens.css does not contain the canonical --blue value #527f9f',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const htmlFiles = walk('dist').filter(
|
||||
(p) =>
|
||||
p.endsWith('.html') &&
|
||||
!p.includes('/hands-on/') &&
|
||||
!p.includes('\\hands-on\\') &&
|
||||
!p.includes('/submitted-skills/') &&
|
||||
!p.includes('\\submitted-skills\\'),
|
||||
);
|
||||
|
||||
const tokenChunkName = basename(tokensBuilt);
|
||||
for (const html of htmlFiles) {
|
||||
const content = readFileSync(html, 'utf8');
|
||||
if (!content.includes(tokenChunkName)) {
|
||||
console.error(
|
||||
`token check failed — ${html} does not load the token layer (${tokenChunkName})`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('token check passed');
|
||||
@@ -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)`);
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tier 2 gate: types, build, content contracts, dependency audit, tokens.
|
||||
# Invoked by .husky/pre-push, and safe to run by hand at any time.
|
||||
#
|
||||
# Parallel-safe: takes a lock in the SHARED git dir, so ten agents pushing from
|
||||
# ten worktrees queue instead of running ten concurrent Astro builds and
|
||||
# thrashing the machine. Waiters block; they do not fail.
|
||||
set -euo pipefail
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
cd "$root"
|
||||
|
||||
# --git-common-dir resolves to the ONE shared .git across all worktrees, which
|
||||
# is exactly the scope we want the lock to cover.
|
||||
common=$(git rev-parse --git-common-dir)
|
||||
lock="$common/af-gate.lock"
|
||||
|
||||
exec 9>"$lock"
|
||||
if ! flock -n 9; then
|
||||
echo "gate: another worktree is running the gate — waiting for it…"
|
||||
flock 9
|
||||
fi
|
||||
|
||||
started=$(date +%s)
|
||||
step() { printf '\n\033[1m▸ %s\033[0m\n' "$1"; }
|
||||
|
||||
# Fail loudly rather than passing vacuously when the toolchain is not installed.
|
||||
if [ ! -d node_modules ]; then
|
||||
echo "gate: node_modules missing — run 'pnpm install --frozen-lockfile' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# This project is pnpm-only. An agent that runs `npm install` out of habit gets
|
||||
# a second, divergent dependency tree and a lockfile nobody reads — the same
|
||||
# class of failure that cost two sessions during phase 0. Catch it here.
|
||||
if [ -f package-lock.json ] || [ -f yarn.lock ] || [ -f bun.lock ] || [ -f bun.lockb ]; then
|
||||
echo "gate: a non-pnpm lockfile is present. This project uses pnpm only." >&2
|
||||
echo " Delete it, then run 'pnpm install --frozen-lockfile'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "types"
|
||||
pnpm exec astro check
|
||||
|
||||
step "build"
|
||||
pnpm run build
|
||||
|
||||
step "content contracts"
|
||||
pnpm run verify
|
||||
|
||||
# The assertion count is the thing agents are most tempted to "fix" downward.
|
||||
# Compare against origin/main and refuse a silent reduction.
|
||||
step "assertion coverage"
|
||||
current=$(grep -c 'throw new Error' scripts/verify.mjs)
|
||||
baseline=$(git show origin/main:scripts/verify.mjs 2>/dev/null | grep -c 'throw new Error' || echo "$current")
|
||||
if [ "$current" -lt "$baseline" ]; then
|
||||
echo "gate: verify.mjs coverage fell from $baseline to $current assertions." >&2
|
||||
echo " Only verification-engineer may reduce it, with a reason per removal." >&2
|
||||
echo " See .agents/context/verification.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " $current assertions (baseline $baseline)"
|
||||
|
||||
step "runtime dependency audit"
|
||||
node scripts/audit-ui.mjs
|
||||
|
||||
step "design tokens"
|
||||
node .agents/scripts/check-tokens.mjs
|
||||
|
||||
printf '\n\033[32mgate passed\033[0m in %ss\n' "$(( $(date +%s) - started ))"
|
||||
@@ -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|/$||'
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch one refactor task in its own worktree, on the CLI its brief routes it to.
|
||||
#
|
||||
# .agents/scripts/launch.sh 01 scaffold
|
||||
# .agents/scripts/launch.sh 02 tokens --base refactor/task-01-scaffold
|
||||
# .agents/scripts/launch.sh 07 primitives --cli mm --fg
|
||||
#
|
||||
# Routing comes from plans/astro-refactor/MODEL-ROUTING.md. Override with --cli.
|
||||
# All four CLIs are launched with their permission prompts disabled: these run
|
||||
# unattended inside a worktree, and a blocked edit or bash call just hangs.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
number=${1:?usage: launch.sh <task-number> <slug> [--base ref] [--cli codex|agy|mm|oc] [--fg]}
|
||||
slug=${2:?slug, e.g. scaffold}
|
||||
shift 2
|
||||
|
||||
base="main"
|
||||
cli=""
|
||||
foreground=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--base) base=$2; shift 2 ;;
|
||||
--cli) cli=$2; shift 2 ;;
|
||||
--fg) foreground=1; shift ;;
|
||||
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Model routing. Codex takes the long iterate-until-green loops, Gemini the two
|
||||
# tasks that need whole-corpus context plus visual judgement, MiniMax the rest.
|
||||
if [ -z "$cli" ]; then
|
||||
case "$number" in
|
||||
01|03|15|16|19) cli=codex ;;
|
||||
02|18) cli=agy ;;
|
||||
*) cli=mm ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
dir="../af-task-${number}"
|
||||
branch="refactor/task-${number}-${slug}"
|
||||
plan="plans/astro-refactor/task-${number}-${slug}.md"
|
||||
log="$(pwd)/.agents/logs/task-${number}-${slug}.log"
|
||||
|
||||
[ -f "$plan" ] || { echo "no plan at $plan — check the number and slug" >&2; exit 1; }
|
||||
mkdir -p .agents/logs
|
||||
|
||||
# The brief names its own agent; pull it out so the prompt can point at the file.
|
||||
# [a-z0-9-] not [a-z-]: content-i18n-migrator has digits in it.
|
||||
agent=$(sed -n 's/.*\*\*Agent\*\*: `\([a-z0-9-]*\)`.*/\1/p' "$plan" | head -1)
|
||||
[ -n "$agent" ] || { echo "could not read the agent name out of $plan" >&2; exit 1; }
|
||||
[ -f ".agents/agents/${agent}.md" ] || { echo "no such agent: .agents/agents/${agent}.md" >&2; exit 1; }
|
||||
|
||||
if [ -d "$dir" ]; then
|
||||
echo "worktree $dir already exists — reusing it"
|
||||
else
|
||||
git worktree add "$dir" -b "$branch" "$base"
|
||||
fi
|
||||
|
||||
# pnpm install works only once task 01 has produced a lockfile. The pre-existing root
|
||||
# package.json carries two scripts and no dependencies, so before task 01 there
|
||||
# is no toolchain to install and no hooks to verify.
|
||||
if [ -f "$dir/pnpm-lock.yaml" ]; then
|
||||
( cd "$dir" && pnpm install --frozen-lockfile && .agents/scripts/verify-hooks.sh )
|
||||
fi
|
||||
|
||||
prompt=$(cat <<PROMPT
|
||||
You are the \`${agent}\` specialist on the ai-for-dummies Astro refactor.
|
||||
You are working alone in the git worktree at $(cd "$dir" && pwd), on branch ${branch}.
|
||||
|
||||
Read these, in this order, before you write anything:
|
||||
|
||||
1. AGENTS.md — project entry point and the "never touch" list
|
||||
2. .agents/agents/${agent}.md — your role, what you own, what you must not do
|
||||
3. ${plan} — your task brief: scope, steps, done-when, do-not
|
||||
4. every file the brief and your agent definition tell you to read
|
||||
(.agents/context/*, .agents/rules/*, .agents/skills/*, .agents/checklists/*)
|
||||
|
||||
Then do the task. Rules that override your own judgement:
|
||||
|
||||
- Stay inside the scope in the brief. Do not do work belonging to another task.
|
||||
- Honour every "Do not" line in the brief and in your agent definition.
|
||||
- The site must look and read exactly as it does today. This is a refactor,
|
||||
not a redesign. If you find something that looks like a bug in the current
|
||||
design, write it down in the final report — do not fix it.
|
||||
- Never delete or weaken an assertion in scripts/verify.mjs to make a suite
|
||||
green. Only the verification-engineer may reduce assertion count, with a
|
||||
written reason per removal.
|
||||
- Commit as you go with conventional-commit messages. The commit-msg and
|
||||
pre-commit hooks are live; if a hook rejects you, fix the cause, never
|
||||
bypass with --no-verify.
|
||||
- Work through the done-when checklist at the end and actually run each check.
|
||||
|
||||
Finish by printing: what you changed, which done-when boxes are genuinely
|
||||
ticked, which are not and why, and anything the next task needs to know.
|
||||
PROMPT
|
||||
)
|
||||
|
||||
echo "task : ${number} ${slug}"
|
||||
echo "agent : ${agent}"
|
||||
echo "cli : ${cli}"
|
||||
echo "worktree: ${dir} (branch ${branch}, from ${base})"
|
||||
echo "log : ${log}"
|
||||
echo
|
||||
|
||||
run() {
|
||||
case "$cli" in
|
||||
codex)
|
||||
codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check \
|
||||
-C "$dir" "$prompt"
|
||||
;;
|
||||
agy)
|
||||
( cd "$dir" && agy --dangerously-skip-permissions \
|
||||
--model gemini-3.1-pro-high --print-timeout 4h -p "$prompt" )
|
||||
;;
|
||||
mm)
|
||||
( cd "$dir" && mm --dangerously-skip-permissions \
|
||||
--model opus -p "$prompt" )
|
||||
;;
|
||||
oc)
|
||||
# Claude Code against a local-Ollama-backed model, through the headroom
|
||||
# hub. Unproven on this repo — give it the task whose failure is cheapest.
|
||||
( cd "$dir" && OLLAMA_CLAUDE_MODEL="${OC_MODEL:-glm-5.3:cloud}" \
|
||||
ollama-claude --dangerously-skip-permissions -p "$prompt" )
|
||||
;;
|
||||
*) echo "unknown cli: $cli" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$foreground" = 1 ]; then
|
||||
run 2>&1 | tee "$log"
|
||||
else
|
||||
export cli dir prompt
|
||||
nohup bash -c "$(declare -f run); run" >"$log" 2>&1 &
|
||||
echo "launched in background, pid $!"
|
||||
echo "follow: tail -f $log"
|
||||
fi
|
||||
@@ -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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/�?39;/g, "'").replace(/ /g, ' ')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
console.log(text);
|
||||
@@ -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
|
||||
# `pnpm install` and is NOT committed. A fresh `git worktree add` therefore has
|
||||
# hooks configured and the directory missing — so every hook silently does
|
||||
# nothing and every commit passes unchecked.
|
||||
#
|
||||
# Run this in any worktree you did not create with worktree.sh.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
fail=0
|
||||
|
||||
path=$(git config --get core.hooksPath || true)
|
||||
if [ -z "$path" ]; then
|
||||
echo "✗ core.hooksPath is unset — husky was never installed here"
|
||||
fail=1
|
||||
else
|
||||
echo "✓ core.hooksPath = $path"
|
||||
fi
|
||||
|
||||
if [ ! -d "${path:-.husky/_}" ]; then
|
||||
echo "✗ ${path:-.husky/_}/ does not exist — HOOKS ARE NOT RUNNING"
|
||||
fail=1
|
||||
else
|
||||
echo "✓ ${path} exists"
|
||||
fi
|
||||
|
||||
for hook in pre-commit commit-msg pre-push; do
|
||||
if [ -f ".husky/$hook" ]; then
|
||||
echo "✓ .husky/$hook present"
|
||||
else
|
||||
echo "✗ .husky/$hook missing"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
echo "✗ node_modules missing — lint-staged and astro check cannot run"
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo
|
||||
echo "Fix: pnpm install --frozen-lockfile (its prepare script regenerates .husky/_)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "hooks are live in this worktree"
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
// Keep vanilla screenshots before a page changes; compare them against the
|
||||
// migrated preview when a visual diff needs investigation.
|
||||
|
||||
import { mkdirSync } from 'node:fs';
|
||||
|
||||
const routes = [
|
||||
'/',
|
||||
'/full-guide/',
|
||||
'/summary/',
|
||||
'/models/',
|
||||
'/agents/',
|
||||
'/skills/',
|
||||
'/rules/',
|
||||
'/skills-review/',
|
||||
'/hands-on/starter/',
|
||||
'/hands-on/rules/',
|
||||
];
|
||||
const widths = [560, 800, 1100, 1600];
|
||||
const hasReducedMotion = process.argv.includes('--reduced-motion');
|
||||
const base = process.env.VISUAL_BASE_URL ?? 'http://localhost:4173';
|
||||
const output =
|
||||
process.env.VISUAL_OUTPUT_DIR ??
|
||||
`.agents/snapshots/${hasReducedMotion ? 'before-reduced-motion' : 'before'}`;
|
||||
|
||||
mkdirSync(output, { recursive: true });
|
||||
|
||||
let chromium;
|
||||
try {
|
||||
({ chromium } = await import('playwright'));
|
||||
} catch {
|
||||
throw new Error(
|
||||
'visual regression requires Playwright; install its project dependency and run pnpm exec playwright install chromium',
|
||||
);
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(process.env.VISUAL_BROWSER_PATH ? { executablePath: process.env.VISUAL_BROWSER_PATH } : {}),
|
||||
});
|
||||
try {
|
||||
for (const route of routes) {
|
||||
for (const width of widths) {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
||||
if (hasReducedMotion) await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
// The legacy site intentionally calls optional services that may be down;
|
||||
// visual capture is about the rendered page, not their network lifetime.
|
||||
await page.goto(new URL(route, base).toString(), { waitUntil: 'domcontentloaded' });
|
||||
const name = route.replace(/^\/+|\/+$/g, '').replaceAll('/', '_') || 'index';
|
||||
await page.screenshot({ path: `${output}/${name}-${width}.png`, fullPage: true });
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log(`captured ${routes.length * widths.length} screenshots in ${output}`);
|
||||
@@ -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" && pnpm install --frozen-lockfile && .agents/scripts/verify-hooks.sh )
|
||||
|
||||
echo
|
||||
echo "worktree : $dir"
|
||||
echo "branch : $branch"
|
||||
echo "brief : $plan"
|
||||
echo
|
||||
echo "next: cd $dir && read the brief end to end before writing anything"
|
||||
;;
|
||||
finish)
|
||||
# Never remove a worktree with uncommitted work in it.
|
||||
if [ -n "$(git -C "$dir" status --porcelain)" ]; then
|
||||
echo "refusing: $dir has uncommitted changes" >&2
|
||||
git -C "$dir" status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
git worktree remove "$dir"
|
||||
echo "removed $dir — branch $branch kept; delete it after the merge lands"
|
||||
;;
|
||||
*)
|
||||
echo "unknown action: $action (expected start|finish)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: astro-component
|
||||
description:
|
||||
Build one Astro component for the ai-for-dummies site from the project
|
||||
templates. Use when creating a primitive, block, or island, or when splitting
|
||||
existing markup into a component.
|
||||
---
|
||||
|
||||
# Building an Astro component
|
||||
|
||||
## Decide it should exist
|
||||
|
||||
Three occurrences, or a name a person says out loud. Two is not enough — see
|
||||
[`../../rules/componentization.md`](../../rules/componentization.md).
|
||||
|
||||
Then place it:
|
||||
|
||||
- `primitives/` — no domain knowledge (Eyebrow, Rule, Callout, CodeBlock)
|
||||
- `blocks/` — composed, page-agnostic, takes props (RouteCard, PhasePanel)
|
||||
- `islands/` — interactive, justified, a leaf
|
||||
|
||||
## Start from a template
|
||||
|
||||
```bash
|
||||
cp .agents/templates/components/static-block.astro src/components/blocks/RouteCard.astro
|
||||
# interactive? use island.astro instead
|
||||
```
|
||||
|
||||
Templates exist so ten parallel agents produce one house style. Do not start
|
||||
from a blank file.
|
||||
|
||||
## Write it
|
||||
|
||||
```astro
|
||||
---
|
||||
interface Props {
|
||||
number: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
href: string;
|
||||
}
|
||||
const { number, title, summary, href } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
<b>{number}</b>
|
||||
<h2>{title}</h2>
|
||||
<p>{summary}</p>
|
||||
<a href={href}>Open chapter →</a>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 22px;
|
||||
background: var(--paper);
|
||||
}
|
||||
b {
|
||||
color: var(--accent);
|
||||
font: var(--font-eyebrow);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Rules that bite most often here:
|
||||
|
||||
- **No raw hex.** Tokens only. `check-tokens.mjs` will fail you.
|
||||
- **No `client:*`** unless it is genuinely interactive, and say why in the PR.
|
||||
- Keep the `gap:1px` over a coloured parent trick where the original used it —
|
||||
it is the house style, not a bug.
|
||||
- Preserve every ARIA attribute from the markup you are replacing.
|
||||
|
||||
## Prove it
|
||||
|
||||
1. Render it at 560 / 800 / 1100 / 1600 px.
|
||||
2. Tab to it. Focus ring visible (`outline: 3px solid #a7483f`).
|
||||
3. Diff its rendered text against the markup it replaced.
|
||||
4. Run
|
||||
[`../../checklists/before-component.md`](../../checklists/before-component.md).
|
||||
5. `pnpm run verify` — green, with no assertion deleted.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: astro-page
|
||||
description:
|
||||
Migrate one hand-written HTML page of the ai-for-dummies site to an Astro
|
||||
route without changing its URL, content, or JS budget. Use for any page-level
|
||||
migration task.
|
||||
---
|
||||
|
||||
# Migrating a page to Astro
|
||||
|
||||
## Snapshot first, migrate second
|
||||
|
||||
The snapshot is the only objective evidence that no content was lost.
|
||||
|
||||
```bash
|
||||
pnpm run serve & # vanilla site on :4173
|
||||
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ \
|
||||
> .agents/snapshots/models.txt
|
||||
```
|
||||
|
||||
Screenshot the same route at 560 / 800 / 1100 / 1600 px as well.
|
||||
|
||||
## Migrate
|
||||
|
||||
```bash
|
||||
cp .agents/templates/pages/chapter.astro src/pages/models.astro
|
||||
```
|
||||
|
||||
Then, in order:
|
||||
|
||||
1. Move markup into the layout + components. Reuse existing components before
|
||||
creating new ones.
|
||||
2. Move copy into `src/content/`. Both `en` and `pt`, copied literally — never
|
||||
retyped.
|
||||
3. Move page CSS into component `<style>` blocks. Do **not** port
|
||||
`responsive.css` wholesale; take what this page needs and prove the rest
|
||||
dead.
|
||||
4. Keep every `data-*` hook. `verify.mjs` asserts many of them by name
|
||||
(`data-phase`, `data-tree`, `data-route`, `data-model-provider`, …).
|
||||
5. Keep every ARIA attribute and the `<title>` / `<meta name="description">`.
|
||||
|
||||
## The URL must not change
|
||||
|
||||
Served from `/ai-for-dummies/`, so `base` is set in `astro.config.mjs`. Never
|
||||
hand-write an internal absolute path; use `import.meta.env.BASE_URL`.
|
||||
|
||||
Trailing slashes matter. `/models/` must not become `/models`.
|
||||
|
||||
If the page honours query params (the review desk uses `?author=`, `?skill=`,
|
||||
`?view=`, `?file=`, `?compare=`, `?render=`), they must still work — they are
|
||||
documented in the page footer and shared externally.
|
||||
|
||||
## Prove it
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
node .agents/scripts/snapshot-route.mjs dist/models/index.html > /tmp/after.txt
|
||||
diff .agents/snapshots/models.txt /tmp/after.txt # empty, or justify every line
|
||||
node scripts/audit-ui.mjs
|
||||
pnpm run verify
|
||||
```
|
||||
|
||||
Then screenshots at the same four widths, and
|
||||
[`../../checklists/before-page.md`](../../checklists/before-page.md).
|
||||
|
||||
## JS budget
|
||||
|
||||
A page that shipped zero JS must still ship zero. Check the build output. If
|
||||
your migration added a `client:load` to a static page, you did it wrong.
|
||||
@@ -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,74 @@
|
||||
---
|
||||
name: design-tokens
|
||||
description:
|
||||
Extract, name, and enforce the design token layer for the ai-for-dummies site.
|
||||
Use when touching any colour, font size, spacing value, or breakpoint, when
|
||||
consolidating the three drifted palettes, or when a check-tokens failure needs
|
||||
resolving.
|
||||
---
|
||||
|
||||
# Design tokens
|
||||
|
||||
## Before anything
|
||||
|
||||
Read [`../../context/design-system.md`](../../context/design-system.md). This
|
||||
site has **three drifting palettes** and a **broken `@font-face`**. Both are
|
||||
traps. If you have not read that file, you will "preserve the styles" by copying
|
||||
a bug.
|
||||
|
||||
## Extracting
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
files=['styles.css','chapters.css','landing.css','rules/styles.css','skills/styles.css',
|
||||
'skills-review/styles.css','hands-on/starter/styles.css','hands-on/rules/styles.css']
|
||||
seen={}
|
||||
for f in files:
|
||||
for m in re.finditer(r'--([a-z-]+):\s*([^;}]+)', open(f).read()):
|
||||
seen.setdefault(m.group(1),{}).setdefault(m.group(2).strip(),[]).append(f)
|
||||
for k,v in sorted(seen.items()):
|
||||
print(f"--{k}:")
|
||||
for val,fs in v.items(): print(f" {val:12} <- {', '.join(fs)}")
|
||||
PY
|
||||
```
|
||||
|
||||
Re-run this after any consolidation. Every token should print exactly one value.
|
||||
|
||||
## Consolidating a drifted token
|
||||
|
||||
1. List every value and where it is used (above).
|
||||
2. Compute the perceptual delta. Sub-perceptual (a few units per channel) →
|
||||
canonicalize freely. Visible (`--blue`: `#527f9f` vs `#215675`) → screenshot
|
||||
both, get a human decision, record it in the task file.
|
||||
3. Pick the canonical value. Prefer the one used on the most-visited surface.
|
||||
4. Replace, then screenshot every affected page at 560/800/1100/1600 px.
|
||||
5. Attach before/after images to the task report. "It looked fine to me" is not
|
||||
evidence.
|
||||
|
||||
## Naming
|
||||
|
||||
Semantic, matching the existing vocabulary — `--ink`, `--paper`, `--muted`,
|
||||
`--line`, `--accent`, `--gold`, `--blue`, `--deep`. Never numeric scales. New
|
||||
surfaces extend semantically: `--surface-lab`, `--ink-inverse`.
|
||||
|
||||
## Type scale and breakpoints
|
||||
|
||||
Collapse the 14 ad-hoc `clamp()` triples to named steps and the 16 breakpoints
|
||||
to five, per [`../../rules/theming.md`](../../rules/theming.md). When collapsing
|
||||
a breakpoint, **screenshot at the old value** — that is where the regression is.
|
||||
|
||||
## Enforcing
|
||||
|
||||
```bash
|
||||
node .agents/scripts/check-tokens.mjs # fails on raw hex outside tokens.css
|
||||
```
|
||||
|
||||
Wire it into `pnpm run verify`. A rule nobody checks is a suggestion.
|
||||
|
||||
## The font decision
|
||||
|
||||
Do not self-host Manrope/DM Mono as part of a refactor task. The intended fonts
|
||||
have never rendered, so adopting them is a visual redesign. Default to matching
|
||||
what renders today (Arial / generic monospace) and delete the dead `@font-face`.
|
||||
If a human wants the real fonts, that is its own task with its own screenshots.
|
||||
@@ -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,73 @@
|
||||
---
|
||||
name: visual-regression
|
||||
description:
|
||||
Prove a refactor did not change how the site looks. Use before and after any
|
||||
page migration, token consolidation, or breakpoint change on ai-for-dummies.
|
||||
---
|
||||
|
||||
# Visual regression
|
||||
|
||||
"Maintain the same styles" is a testable claim. Test it.
|
||||
|
||||
## Capture
|
||||
|
||||
The repo already has a Playwright pattern (`scripts/inspect.py`). Extend it
|
||||
rather than inventing one.
|
||||
|
||||
```python
|
||||
from playwright.sync_api import sync_playwright
|
||||
ROUTES = ['/', '/full-guide/', '/summary/', '/models/', '/agents/',
|
||||
'/skills/', '/rules/', '/skills-review/',
|
||||
'/hands-on/starter/', '/hands-on/rules/']
|
||||
WIDTHS = [560, 800, 1100, 1600]
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
for route in ROUTES:
|
||||
for w in WIDTHS:
|
||||
page = browser.new_page(viewport={'width': w, 'height': 900})
|
||||
page.goto(f'{BASE}{route}', wait_until='networkidle')
|
||||
page.screenshot(path=f'{OUT}/{route.strip("/").replace("/","_") or "index"}-{w}.png',
|
||||
full_page=True)
|
||||
page.close()
|
||||
browser.close()
|
||||
```
|
||||
|
||||
Run once against the vanilla site (`pnpm run serve`), once against
|
||||
`pnpm run preview`. Keep both sets.
|
||||
|
||||
## Compare
|
||||
|
||||
```bash
|
||||
for f in before/*.png; do
|
||||
compare -metric AE "$f" "after/$(basename $f)" null: 2>&1 # ImageMagick
|
||||
echo " <- $(basename $f)"
|
||||
done
|
||||
```
|
||||
|
||||
Pixel-exact is not the bar — antialiasing differs. Judge by eye where the metric
|
||||
is non-trivial, and attach the pair to the task report.
|
||||
|
||||
## The three widths that catch the most
|
||||
|
||||
- **560px** — where the 16 ad-hoc breakpoints collapse to `--bp-sm`. Highest
|
||||
risk in the whole migration.
|
||||
- **800px** — the most common existing breakpoint; layout flips here.
|
||||
- **1600px** — `min-width` rules that only fire on large screens are the ones
|
||||
nobody notices are broken.
|
||||
|
||||
Also screenshot at the **old** breakpoint values you removed (520, 530, 600,
|
||||
620, 720, 850, 880, 900), not just the new ones. Regressions hide exactly there.
|
||||
|
||||
## What a real difference looks like
|
||||
|
||||
Expect and accept: sub-pixel text shifts, antialiasing.
|
||||
|
||||
Investigate: anything that moves by more than ~2px, any colour change (that is a
|
||||
token bug), any element that appears or disappears (that is content loss — stop
|
||||
and check the snapshot diff).
|
||||
|
||||
## Reduced motion
|
||||
|
||||
Capture one pass with `prefers_reduced_motion='reduce'`. Animations must land in
|
||||
their correct end state, not vanish.
|
||||
@@ -0,0 +1,40 @@
|
||||
AI For Dummies — Agents and trees
|
||||
← ROUTE MAP
|
||||
02 / AGENTS & TREES
|
||||
field guide ↗
|
||||
Subagent workflow
|
||||
One branch
|
||||
per
|
||||
hand.
|
||||
Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent.
|
||||
The tree
|
||||
Split at
|
||||
the
|
||||
seam.
|
||||
MAIN / ORCHESTRATOR
|
||||
├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence
|
||||
FRAME
|
||||
Orchestrator
|
||||
Owns scope, task graph, boundaries, and integration.
|
||||
HAND OFF
|
||||
Worker
|
||||
Owns one coherent slice and one worktree.
|
||||
PROVE
|
||||
Verifier
|
||||
Re-runs gates and reports remaining gaps.
|
||||
Handoff
|
||||
Context that
|
||||
can
|
||||
travel.
|
||||
01
|
||||
Brief
|
||||
Goal, owned files, dependencies, non-goals, acceptance.
|
||||
02
|
||||
Isolation
|
||||
One branch and worktree per independent change.
|
||||
03
|
||||
Evidence
|
||||
Commands, result, changed files, screenshots, gaps.
|
||||
Previous: models →
|
||||
Rules case study →
|
||||
Try the rules lab →
|
||||
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 118 KiB |